← back to Dw Silver Mylar Rename

apply.mjs

55 lines

// Apply the title/descriptionHtml swaps to LIVE Shopify. No option changes (scan found 0).
// Usage: node apply.mjs --ids gid1,gid2      (canary subset)
//        node apply.mjs --all                (everything not already done)
//        node apply.mjs --all --exclude gid1,gid2
import { readFileSync, appendFileSync, existsSync } from 'node:fs';
import { gql, has } from './lib.mjs';

const plan = JSON.parse(readFileSync(new URL('./data/plan.json', import.meta.url)));
const args = process.argv.slice(2);
const idsArg = args.includes('--ids') ? args[args.indexOf('--ids') + 1].split(',') : null;
const exclArg = args.includes('--exclude') ? new Set(args[args.indexOf('--exclude') + 1].split(',')) : new Set();
const all = args.includes('--all');
if (!idsArg && !all) { console.error('pass --ids <gid,gid> or --all'); process.exit(1); }

const LOG = new URL('./data/apply-results.jsonl', import.meta.url);
// resume-safe: skip gids already successfully applied
const done = new Set();
if (existsSync(LOG)) for (const l of readFileSync(LOG, 'utf8').split('\n').filter(Boolean)) {
  try { const r = JSON.parse(l); if (r.ok) done.add(r.gid); } catch {}
}

let targets = plan;
if (idsArg) targets = plan.filter(p => idsArg.includes(p.gid));
targets = targets.filter(p => !exclArg.has(p.gid) && !done.has(p.gid));

const M = `mutation($input: ProductInput!){
  productUpdate(input:$input){ product{ id title descriptionHtml } userErrors{ field message } }
}`;

let ok = 0, fail = 0; const failures = [];
console.log(`applying to ${targets.length} products...`);
for (const p of targets) {
  const input = { id: p.gid };
  if (p.changes.title) input.title = p.changes.title.to;
  if (p.changes.descriptionHtml) input.descriptionHtml = p.changes.descriptionHtml.to;
  try {
    const d = await gql(M, { input });
    const ue = d.productUpdate.userErrors;
    if (ue && ue.length) throw new Error('userErrors: ' + JSON.stringify(ue));
    const prod = d.productUpdate.product;
    // verify no residual token in returned fields
    const residual = has(prod.title) || has(prod.descriptionHtml);
    const rec = { ts: new Date().toISOString(), gid: p.gid, status: p.status, ok: !residual,
      title_changed: !!p.changes.title, body_changed: !!p.changes.descriptionHtml, residual };
    appendFileSync(LOG, JSON.stringify(rec) + '\n');
    if (residual) { fail++; failures.push({ gid: p.gid, reason: 'residual token after write' }); }
    else ok++;
  } catch (e) {
    fail++; failures.push({ gid: p.gid, reason: String(e.message || e) });
    appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), gid: p.gid, ok: false, error: String(e.message || e) }) + '\n');
  }
}
console.log(`DONE  ok=${ok}  fail=${fail}`);
if (failures.length) { console.log('FAILURES:'); for (const f of failures) console.log('  ', f.gid, f.reason); }