← back to Tk10630 Sku Suffix Canary

apply-backfill.mjs

43 lines

// Apply the Option C backfill from generator-backfill-plan.json. DRY default; --apply writes.
// Per product (serial): dw_sku metafields (global/dwc/custom)=number + variant SKUs=number-suf.
// Resolves fresh IDs per product. Resumable via done-backfill.jsonl. --only / --limit for staging.
import { readFileSync, appendFileSync, existsSync } from 'node:fs';
import { gql } from './shopify.mjs';
const APPLY = process.argv.includes('--apply');
const LIMIT = Number((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || Infinity);
const ONLY = (process.argv.find(a => a.startsWith('--only=')) || '').split('=')[1] || null;
const DONE = 'done-backfill.jsonl';
const plan = JSON.parse(readFileSync('generator-backfill-plan.json', 'utf8')).filter(p => !p.COLLISION);
const done = new Set();
if (existsSync(DONE)) for (const l of readFileSync(DONE, 'utf8').split('\n')) if (l.trim()) done.add(JSON.parse(l).handle);
let work = plan.filter(p => !done.has(p.handle));
if (ONLY) work = work.filter(p => p.handle.includes(ONLY));
if (work.length > LIMIT) work = work.slice(0, LIMIT);
console.log(`[backfill] mode=${APPLY ? 'LIVE' : 'DRY'} plan=${plan.length} todo=${work.length}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function q(query, v) { for (let a = 0; ; a++) { try { return (await gql(query, v)).data; } catch (e) { if (/THROTTLED|<html/i.test(e.message) && a < 8) { await sleep(1500 * (a + 1)); continue; } throw e; } } }
const INV = `mutation($id:ID!,$sku:String!){ inventoryItemUpdate(id:$id, input:{sku:$sku}){ userErrors{message} } }`;
const MFS = `mutation($m:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$m){ userErrors{message} } }`;

let ok = 0, err = 0;
for (const p of work) {
  try {
    const d = await q(`query{ productByHandle(handle:"${p.handle}"){ id variants(first:6){ nodes{ sku inventoryItem{ id } } } } }`);
    const prod = d.productByHandle; if (!prod) { err++; console.log('✗ gone', p.handle); continue; }
    const errs = [];
    // variant SKUs: match current sku -> target from plan.varChanges
    for (const c of p.varChanges) {
      const v = prod.variants.nodes.find(x => x.sku === c.from);
      if (!v) continue; // already changed or not found
      if (APPLY) { const r = await q(INV, { id: v.inventoryItem.id, sku: c.to }); if (r.inventoryItemUpdate.userErrors.length) errs.push('var:' + JSON.stringify(r.inventoryItemUpdate.userErrors)); }
    }
    // dw_sku metafields -> the number
    const m = ['global', 'dwc', 'custom'].map(ns => ({ ownerId: prod.id, namespace: ns, key: 'dw_sku', type: 'single_line_text_field', value: String(p.number) }));
    if (APPLY) { const r = await q(MFS, { m }); if (r.metafieldsSet.userErrors.length) errs.push('mf:' + JSON.stringify(r.metafieldsSet.userErrors)); }
    if (errs.length) { err++; console.log('✗', p.handle, errs.join(';')); }
    else { ok++; if (APPLY) appendFileSync(DONE, JSON.stringify({ handle: p.handle, number: p.number }) + '\n'); }
  } catch (e) { err++; console.log('✗', p.handle, e.message.slice(0, 90)); }
  if ((ok + err) % 50 === 0) process.stderr.write(`  ${ok + err}/${work.length}\n`);
}
console.log(`[backfill] DONE ok=${ok} err=${err} ${APPLY ? '(written)' : '(dry)'}`);