← back to Tk10630 Sku Suffix Canary
enumerate.mjs
65 lines
// READ-ONLY enumerator + planner for the Hollywood/Momentum SKU consolidation.
// Pages every live Hollywood Wallcoverings product, derives its ORIGINAL line code,
// and emits a per-product change plan (plan.json). Writes NOTHING to Shopify.
import { gql } from './shopify.mjs';
import { deriveBase, planProduct } from './lib.mjs';
import { writeFileSync } from 'node:fs';
const VENDOR = 'Hollywood Wallcoverings';
const PAGE = `query($cursor:String){
products(first:40, query:"vendor:\\"${VENDOR}\\"", after:$cursor){
pageInfo{ hasNextPage endCursor }
nodes{
id handle title status
pn: metafield(namespace:"global", key:"product_number"){ id value }
mfG: metafield(namespace:"global", key:"dw_sku"){ id value }
mfD: metafield(namespace:"dwc", key:"dw_sku"){ id value }
mfC: metafield(namespace:"custom", key:"dw_sku"){ id value }
variants(first:10){ nodes{ id title sku selectedOptions{ name value } inventoryItem{ id } } }
}
}
}`;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const plan = [], skipped = [];
let cursor = null, pages = 0, total = 0, clean = 0, fatal = null;
const summarize = () => ({
scanned: total, need_change: plan.length, already_clean: clean,
skipped: skipped.length,
skip_no_code: skipped.filter(s => s.reason === 'no-original-code').length,
skip_conflict: skipped.filter(s => s.reason === 'variant-base-conflict').length,
variant_writes: plan.reduce((a, p) => a + p.varChanges.length, 0),
metafield_writes: plan.reduce((a, p) => a + p.mfChanges.length, 0),
sample_suffix_fixes: plan.reduce((a, p) => a + p.varChanges.filter(c => c.option === 'sample' && /-yard$/.test(c.from || '')).length, 0),
dwhd_bases_removed: plan.reduce((a, p) => a + p.varChanges.filter(c => /^DWHD-/i.test(c.from || '')).length, 0),
dwhw_metafields_removed: plan.reduce((a, p) => a + p.mfChanges.filter(c => /^DWHW/i.test(c.from || '')).length, 0),
});
const flush = () => writeFileSync('plan.json', JSON.stringify({ summary: summarize(), plan, skipped, fatal, cursor }, null, 2));
while (true) {
let res;
try {
for (let attempt = 0; ; attempt++) {
try { res = await gql(PAGE, { cursor }); break; }
catch (e) { if (/THROTTLED|throttle/i.test(e.message) && attempt < 8) { await sleep(2000 * (attempt + 1)); continue; } throw e; }
}
} catch (e) { fatal = { atPage: pages, atTotal: total, cursor, message: e.message }; process.stderr.write(`FATAL p${pages}: ${e.message}\n`); break; }
const { nodes, pageInfo } = res.data.products;
for (const p of nodes) {
total++;
const base = deriveBase(p);
if (!base) { skipped.push({ id: p.id, handle: p.handle, reason: 'no-original-code' }); continue; }
if (typeof base === 'object') { skipped.push({ id: p.id, handle: p.handle, reason: 'variant-base-conflict', bases: base.conflict }); continue; }
const { varChanges, mfChanges } = planProduct(p, base);
if (varChanges.length || mfChanges.length) plan.push({ id: p.id, handle: p.handle, status: p.status, base, varChanges, mfChanges });
else clean++;
}
pages++; cursor = pageInfo.endCursor;
if (pages % 5 === 0) { flush(); process.stderr.write(` ...${total} scanned | ${plan.length} change, ${clean} clean, ${skipped.length} skip\n`); }
if (!pageInfo.hasNextPage) break;
}
flush();
console.log(JSON.stringify({ ...summarize(), fatal }, null, 2));