← back to Wolfgordon Crawl
audit-live-state.js
65 lines
#!/usr/bin/env node
// Read-only audit of LIVE Wolf Gordon products on Shopify: status, image count,
// description presence, and the set of metafields present. No writes.
const fs = require('fs');
const os = require('os');
function loadToken() {
if (process.env.SHOPIFY_ADMIN_TOKEN) return process.env.SHOPIFY_ADMIN_TOKEN;
const p = os.homedir() + '/Projects/secrets-manager/.env';
const line = fs.readFileSync(p, 'utf8').split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN='));
return line.slice('SHOPIFY_ADMIN_TOKEN='.length).replace(/^["']|["']$/g, '').trim();
}
const TOKEN = loadToken();
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
const GQL = `https://${SHOP}/admin/api/${API}/graphql.json`;
const wait = ms => new Promise(r => setTimeout(r, ms));
async function gql(q, v) {
await wait(300);
const res = await fetch(GQL, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) });
const j = await res.json();
if (j.errors) throw new Error(JSON.stringify(j.errors));
return j.data;
}
const Q = `query($c:String){
products(first:80, query:"vendor:'Wolf Gordon'", after:$c){
pageInfo{ hasNextPage endCursor }
edges{ node{
id handle status descriptionHtml createdAt
mediaCount{ count }
metafields(first:30){ edges{ node{ namespace key } } }
variants(first:3){ edges{ node{ sku price } } }
} }
}
}`;
(async () => {
let cursor = null, pages = 0;
const stat = {}; let total = 0, zeroImg = 0, oneImg = 0, multiImg = 0, noDesc = 0, hasDesc = 0;
const mfCount = {}; // namespace.key -> count present
const zeroImgSkus = []; const activeZeroImg = [];
do {
const d = await gql(Q, { c: cursor });
for (const e of d.products.edges) {
const n = e.node; total++;
stat[n.status] = (stat[n.status] || 0) + 1;
const mc = n.mediaCount?.count || 0;
if (mc === 0) zeroImg++; else if (mc === 1) oneImg++; else multiImg++;
const dh = (n.descriptionHtml || '').replace(/<[^>]+>/g, '').trim();
if (dh.length < 20) noDesc++; else hasDesc++;
const mfs = n.metafields.edges.map(m => `${m.node.namespace}.${m.node.key}`);
for (const k of mfs) mfCount[k] = (mfCount[k] || 0) + 1;
const sku = n.variants.edges[0]?.node?.sku || '';
if (mc === 0) { zeroImgSkus.push({ sku, handle: n.handle, status: n.status }); if (n.status === 'ACTIVE') activeZeroImg.push(sku); }
}
cursor = d.products.pageInfo.hasNextPage ? d.products.pageInfo.endCursor : null;
pages++;
} while (cursor && pages < 80);
console.log(JSON.stringify({ total, status: stat, images: { zeroImg, oneImg, multiImg }, desc: { noDesc, hasDesc }, metafieldsPresent: mfCount, activeZeroImgCount: activeZeroImg.length, pages }, null, 2));
fs.writeFileSync('/tmp/wg-zero-img-skus.json', JSON.stringify(zeroImgSkus, null, 2));
console.log('zero-image SKUs written to /tmp/wg-zero-img-skus.json (' + zeroImgSkus.length + ')');
})().catch(e => { console.error('ERR', e.message); process.exit(1); });