← back to Wolfgordon Crawl

audit-pushed-cohort.js

60 lines

#!/usr/bin/env node
// Read-only: for the catalog rows that have a recorded shopify_product_id,
// fetch the LIVE product and report image count, description presence, and
// metafield richness. Identifies the bare-shell cohort created by today's push.
const fs = require('fs'); const os = require('os');
const { pool } = require('./scraper-utils');

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(280);
  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;
}

(async () => {
  const rows = (await pool.query(
    `SELECT dw_sku, mfr_sku, shopify_product_id FROM wolf_gordon_catalog
     WHERE shopify_product_id IS NOT NULL AND shopify_product_id<>'' ORDER BY updated_at DESC`)).rows;
  console.log('catalog rows w/ pid:', rows.length);
  let active = 0, draft = 0, archived = 0, missing = 0;
  let zeroImg = 0, oneImg = 0, multiImg = 0, noDesc = 0, hasDesc = 0, fewMf = 0;
  const bareShells = []; // active, <=1 img OR no desc OR <=1 metafield
  // batch by nodes()
  for (let i = 0; i < rows.length; i += 50) {
    const batch = rows.slice(i, i + 50);
    const ids = batch.map(r => `"gid://shopify/Product/${r.shopify_product_id}"`).join(',');
    const d = await gql(`{ nodes(ids:[${ids}]){ ... on Product { id status descriptionHtml mediaCount{count} metafields(first:40){edges{node{namespace key}}} variants(first:2){edges{node{sku}}} } } }`);
    for (let k = 0; k < d.nodes.length; k++) {
      const n = d.nodes[k]; const r = batch[k];
      if (!n) { missing++; continue; }
      if (n.status === 'ACTIVE') active++; else if (n.status === 'DRAFT') draft++; else archived++;
      const mc = n.mediaCount?.count || 0;
      if (mc === 0) zeroImg++; else if (mc === 1) oneImg++; else multiImg++;
      const dh = (n.descriptionHtml || '').replace(/<[^>]+>/g, '').trim();
      const dOk = dh.length >= 20; if (dOk) hasDesc++; else noDesc++;
      const nmf = n.metafields.edges.length; if (nmf <= 2) fewMf++;
      const sku = n.variants.edges[0]?.node?.sku || r.dw_sku;
      if (n.status === 'ACTIVE' && (mc <= 1 || !dOk || nmf <= 2)) {
        bareShells.push({ dw_sku: r.dw_sku, mfr_sku: r.mfr_sku, pid: r.shopify_product_id, mc, descLen: dh.length, nmf });
      }
    }
    process.stderr.write(`\r  fetched ${Math.min(i + 50, rows.length)}/${rows.length}`);
  }
  process.stderr.write('\n');
  console.log(JSON.stringify({ active, draft, archived, missing, zeroImg, oneImg, multiImg, noDesc, hasDesc, fewMf, bareShellCount: bareShells.length }, null, 2));
  fs.writeFileSync('/tmp/wg-bare-shells.json', JSON.stringify(bareShells, null, 2));
  console.log('bare-shell cohort written to /tmp/wg-bare-shells.json');
  await pool.end();
})().catch(e => { console.error('ERR', e.message); process.exit(1); });