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