← back to Dw Unbuyable Recovery Pilot

tk11042-quote-tag/rollback-all.mjs

36 lines

#!/usr/bin/env node
/**
 * TK-11042 rollback — restore each product's EXACT prior tags string from
 * prestate. Byte-for-byte restore, so it also undoes nothing else.
 *   node rollback-all.mjs --tranche=1            # dry-run
 *   node rollback-all.mjs --tranche=1 --live
 */
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const DIR = path.dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
const LIVE = args.includes('--live');
const TRANCHE = (args.find(a => a.startsWith('--tranche=')) || '--tranche=1').split('=')[1];
const env = Object.fromEntries(
  fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8')
    .split('\n').filter(l => l.includes('='))
    .map(l => [l.slice(0, l.indexOf('=')).trim(), l.slice(l.indexOf('=') + 1).trim()]));
const SHOP = (env.SHOPIFY_STORE_DOMAIN || env.SHOPIFY_STORE).replace(/^https?:\/\//, '');
const hdr = { 'X-Shopify-Access-Token': env.SHOPIFY_ADMIN_TOKEN, 'Content-Type': 'application/json' };
const REST = `https://${SHOP}/admin/api/2024-10`;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const f = path.join(DIR, 'data', `prestate-t${TRANCHE}.jsonl`);
if (!fs.existsSync(f)) { console.error('no prestate for tranche', TRANCHE); process.exit(1); }
const rows = fs.readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
let n = 0;
for (const r of rows) {
  if (!LIVE) { console.log('WOULD RESTORE', r.handle, '->', r.tagsBefore.slice(0, 60) + '…'); continue; }
  const u = await fetch(`${REST}/products/${r.id}.json`, {
    method: 'PUT', headers: hdr,
    body: JSON.stringify({ product: { id: r.id, tags: r.tagsBefore } }) });
  if (!u.ok) { console.log('FAIL', r.handle, u.status); continue; }
  n++; console.log('restored', r.handle); await sleep(600);
}
console.log(LIVE ? `restored ${n}/${rows.length}` : `dry-run: ${rows.length} would be restored`);