← back to Designer Wallcoverings
audits/designtex-uom-fix/live-gap-audit.mjs
111 lines
#!/usr/bin/env node
// Fresh LIVE Shopify audit of every Designtex product: UOM drift + activation-gate completeness.
// READ-ONLY. Writes designtex-live-gap-<date>.json. $0 (Shopify GraphQL reads, no metered AI).
import fs from 'fs';
import os from 'os';
import path from 'path';
const env = fs.readFileSync(path.join(os.homedir(), 'Projects/secrets-manager/.env'), 'utf8');
const tok = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].trim();
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com', VER = '2024-10';
async function gql(query, variables) {
const r = await fetch(`https://${DOMAIN}/admin/api/${VER}/graphql.json`, {
method: 'POST',
headers: { 'X-Shopify-Access-Token': tok, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
return r.json();
}
const Q = `query($cursor:String){
products(first:50, after:$cursor, query:"vendor:Designtex"){
pageInfo{ hasNextPage endCursor }
edges{ node{
id title handle status productType
featuredImage{ id }
images(first:1){ edges{ node{ id } } }
bodyHtml: descriptionHtml
tags
variants(first:5){ edges{ node{ id title sku price } } }
uom: metafield(namespace:"global", key:"unit_of_measure"){ value }
width: metafield(namespace:"global", key:"width"){ value }
widthB: metafield(namespace:"custom", key:"width"){ value }
repeat: metafield(namespace:"global", key:"repeat"){ value }
content: metafield(namespace:"global", key:"Content"){ value }
contentB: metafield(namespace:"global", key:"Contents"){ value }
fire: metafield(namespace:"global", key:"FLAMMABILITY"){ value }
cleaning: metafield(namespace:"global", key:"Cleaning"){ value }
} }
}
}`;
const all = [];
let cursor = null, page = 0;
do {
const res = await gql(Q, { cursor });
if (res.errors) { console.error(JSON.stringify(res.errors)); process.exit(1); }
const conn = res.data.products;
for (const e of conn.edges) all.push(e.node);
cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
page++;
await new Promise((r) => setTimeout(r, 300));
} while (cursor);
const rows = all.map((n) => {
const sku = n.variants.edges.find((v) => v.node.sku && !/-Sample$/.test(v.node.sku))?.node.sku
|| n.variants.edges[0]?.node.sku || '';
const hasSample = n.variants.edges.some((v) => /-Sample$/.test(v.node.sku || ''));
const hasImage = !!(n.featuredImage || n.images.edges.length);
const uom = n.uom?.value || '';
return {
id: n.id, title: n.title, handle: n.handle, status: n.status, productType: n.productType,
sku, prefix: (sku.match(/^([A-Z]+)-/) || [])[1] || '',
uom,
uom_drift: uom && uom !== 'YARD' && !/Yard/i.test(uom),
has_image: hasImage,
has_width: !!(n.width?.value || n.widthB?.value),
has_repeat: !!n.repeat?.value,
has_content: !!(n.content?.value || n.contentB?.value),
has_fire: !!n.fire?.value,
has_cleaning: !!n.cleaning?.value,
has_desc: !!(n.bodyHtml && n.bodyHtml.replace(/<[^>]+>/g, '').trim().length > 20),
has_sample: hasSample,
n_tags: (n.tags || []).length,
};
});
const f = (pred) => rows.filter(pred).length;
const summary = {
generated_at: new Date().toISOString(),
total: rows.length,
active: f((r) => r.status === 'ACTIVE'),
draft: f((r) => r.status === 'DRAFT'),
archived: f((r) => r.status === 'ARCHIVED'),
prefixes: rows.reduce((a, r) => { a[r.prefix || '(none)'] = (a[r.prefix || '(none)'] || 0) + 1; return a; }, {}),
uom: {
yard: f((r) => /YARD|Yard/i.test(r.uom)),
drift: f((r) => r.uom_drift),
blank: f((r) => !r.uom),
},
gaps: {
no_image: f((r) => !r.has_image),
no_width: f((r) => !r.has_width),
no_repeat: f((r) => !r.has_repeat),
no_content: f((r) => !r.has_content),
no_fire: f((r) => !r.has_fire),
no_cleaning: f((r) => !r.has_cleaning),
no_desc: f((r) => !r.has_desc),
no_sample: f((r) => !r.has_sample),
lt2_tags: f((r) => r.n_tags < 2),
},
active_uom_drift: f((r) => r.status === 'ACTIVE' && r.uom_drift),
draft_uom_drift: f((r) => r.status === 'DRAFT' && r.uom_drift),
};
const date = new Date().toISOString().slice(0, 10);
const out = path.join(path.dirname(new URL(import.meta.url).pathname), `designtex-live-gap-${date}.json`);
fs.writeFileSync(out, JSON.stringify({ summary, rows }, null, 2));
console.log(JSON.stringify(summary, null, 2));
console.log('\nWrote', out);