← back to Dw Contact Us Pages

scripts/assign-template.mjs

69 lines

#!/usr/bin/env node
// assign-template.mjs — set templateSuffix="contact-us" on every target product. TK-11925.
//   node scripts/assign-template.mjs              # dry-run
//   node scripts/assign-template.mjs --apply
//   node scripts/assign-template.mjs --rollback --apply
// Preimage (old templateSuffix, exactly as read) -> data/ledger-template.jsonl
import { join } from 'node:path';
import { DATA_DIR, TICKET, VENDORS, COHORT_FLAG, parseArgs, banner, gql, loadTargets, appendJsonl, readJsonl, logReversible, payloadErrors, assertFreshTargets } from './lib.mjs';

const a = parseArgs();
const SUFFIX = 'contact-us';
const LEDGER = join(DATA_DIR, 'ledger-template.jsonl');
banner(a.rollback ? 'assign-template --rollback' : 'assign-template', a.apply);

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

let work;
if (a.rollback) {
  const rows = readJsonl(LEDGER).filter((r) => r.applied);
  const latest = new Map();
  for (const r of rows) if (!latest.has(r.productId)) latest.set(r.productId, r); // FIRST record = true preimage
  // Restore EXACTLY what was read: null stays null (ProductInput.templateSuffix is nullable),
  // '' stays '' (140 Ralph Lauren products carried '' before). Kimi review fix.
  work = [...latest.values()].map((r) => ({ id: r.productId, handle: r.handle, to: r.before === undefined ? null : r.before }));
} else {
  work = loadTargets()
    .filter((p) => p.templateSuffix !== SUFFIX)
    .map((p) => ({ id: p.id, handle: p.handle, to: SUFFIX, before: p.templateSuffix }));
}

console.log(`plan: ${work.length} productUpdate calls -> templateSuffix ${a.rollback ? '(restored)' : `"${SUFFIX}"`}`);
if (work.length) {
  for (const w of work.slice(0, 3)) console.log(`  e.g. ${w.handle}: ${JSON.stringify(w.before ?? '')} -> ${JSON.stringify(w.to)}`);
  if (work.length > 3) console.log(`  … +${work.length - 3} more`);
}
if (!a.apply) { console.log('\nDRY-RUN: nothing was written.'); process.exit(0); }
assertFreshTargets(a);

// PREIMAGE FIRST (TK-11669): every planned product's before-value is ledgered BEFORE the first
// write, so a kill between a write and its ledger row can never orphan an un-restorable change.
// Restoring a preimage for a product that was never changed is a no-op.
if (!a.rollback) {
  const runId = new Date().toISOString();
  for (const w of work) appendJsonl(LEDGER, { ts: runId, runId, productId: w.id, handle: w.handle, before: w.before, after: w.to, applied: true, phase: 'preimage' });
  console.log(`  preimage: ${work.length} rows appended to ${LEDGER} BEFORE any write`);
}

let ok = 0, fail = 0;
for (const w of work) {
  let d; try { d = await gql(M, { input: { id: w.id, templateSuffix: w.to } }); } catch (e) { d = { _err: String(e.message || e) }; }
  const errs = d._err ? [{ message: d._err.slice(0, 300) }] : payloadErrors(d.productUpdate, 'productUpdate');
  if (errs.length) { fail++; console.error(`  FAIL ${w.handle}: ${JSON.stringify(errs)}`); }
  else {
    ok++;
    if (ok % 50 === 0) process.stderr.write(`\r  ${ok}/${work.length}   `);
  }
}
console.log(`\ndone: ${ok} ok, ${fail} failed`);
if (ok && !a.rollback) logReversible({
  action: `${TICKET} templateSuffix -> contact-us on ${ok} products (${VENDORS.join(' / ')})`,
  blast: ok,
  undo: `cd ~/Projects/dw-contact-us-pages && node scripts/assign-template.mjs${COHORT_FLAG} --rollback --apply`,
  verify: `cd ~/Projects/dw-contact-us-pages && node scripts/verify.mjs${COHORT_FLAG}`,
});
process.exit(fail ? 1 : 0);