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