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