← 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); });