← back to Mfr Recovery 2026 08 23
rollback-durable-push.mjs
61 lines
#!/usr/bin/env node
/**
* rollback-durable-push.mjs — TK-10827 companion rollback for durable-push-mfr.mjs
*
* Reads a restore-map produced by durable-push-mfr.mjs --apply
* (out/durable-restore-<ts>.jsonl) and REVERSES each recorded write on BOTH surfaces,
* restoring every field to its `old` value:
* - surface=kamatera_db → ssh psql UPDATE shopify_products SET mfr_sku=<old> WHERE id=..
* - surface=shopify_metafield→ metafieldsSet back to <old>, or metafieldDelete when old is null
*
* DEFAULT is --dry-run: prints exactly what it would revert, writes nothing.
*
* Usage:
* node rollback-durable-push.mjs out/durable-restore-1756...jsonl # DRY-RUN
* node rollback-durable-push.mjs out/durable-restore-1756...jsonl --apply # GATED revert
*/
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';
import { gql } from '../designerwallcoverings/scripts/lib/shopify.mjs';
const file = process.argv[2];
const APPLY = process.argv.includes('--apply');
const KAM = 'root@45.61.58.125';
if (!file || !fs.existsSync(file)) { console.error('usage: node rollback-durable-push.mjs <restore-map.jsonl> [--apply]'); process.exit(1); }
const recs = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
const dbRecs = recs.filter(r => r.surface === 'kamatera_db');
const shRecs = recs.filter(r => r.surface === 'shopify_metafield');
console.log(`Rollback ${file} · db=${dbRecs.length} shopify=${shRecs.length} · ${APPLY ? 'APPLY (GATED)' : 'DRY-RUN'}\n`);
// ── Kamatera DB revert (reverse order is irrelevant; keyed by id) ────────────────
if (dbRecs.length) {
for (const r of dbRecs) console.log(` ${APPLY ? '→ DB REVERT' : '· DB PLAN'} id=${r.id} '${r.new}' -> '${r.old}'`);
if (APPLY) {
const stmts = dbRecs.map(r => `UPDATE shopify_products SET mfr_sku='${String(r.old ?? '').replace(/'/g, "''")}' WHERE id=${r.id} AND vendor='${r.vendor}';`);
const body = `BEGIN; ${stmts.join(' ')} COMMIT;`;
try { execFileSync('ssh', [KAM, `psql -d dw_unified -c "${body.replace(/"/g, '\\"')}"`], { encoding: 'utf8' }); console.log(' DB revert txn committed.'); }
catch (e) { console.error(` ⚠ DB revert FAILED: ${String(e.message || e).slice(0, 200)}`); }
}
}
// ── Shopify metafield revert ─────────────────────────────────────────────────────
async function shopRevert() {
for (const r of shRecs) {
for (const s of r.set) {
console.log(` ${APPLY ? '→ SH REVERT' : '· SH PLAN'} ${r.handle} ${s.ns}.${s.key} '${s.new}' -> ${s.old === null ? 'DELETE' : `'${s.old}'`}`);
if (!APPLY) continue;
if (s.old === null) {
await gql(`mutation($mf:[MetafieldIdentifierInput!]!){ metafieldsDelete(metafields:$mf){ userErrors{ message } } }`,
{ mf: [{ ownerId: r.ownerId, namespace: s.ns, key: s.key }] });
} else {
await gql(`mutation($m:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$m){ userErrors{ message } } }`,
{ m: [{ ownerId: r.ownerId, namespace: s.ns, key: s.key, type: 'single_line_text_field', value: s.old }] });
}
}
}
}
await shopRevert();
console.log(`\n${APPLY ? 'Rollback complete.' : 'DRY-RUN — add --apply (GATED) to revert.'}`);