← 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.'}`);