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