← back to Majilite Jewelry Cases
scripts/remove-jewelry-shopify.mjs
67 lines
#!/usr/bin/env node
// TK-11086 (Steve 2026-09-01): "stop showing the jewelry photo across the dw network".
// Detaches the glass-counter JEWELRY render (pushed live by push-shopify.mjs) from each DW Shopify
// product it was attached to. The image IDs come straight from data/push.log (the LIVE PUSH record).
//
// GATED: this is a customer-facing production write across the LIVE DW store -> DRY-RUN by default;
// real deletes require --apply (Steve-gated). Reversible: the render still exists on disk under
// output/<dir>/glass-counter.png and can be re-attached via push-shopify.mjs --apply.
//
// Flags: --apply (LIVE DELETE) --limit N (canary) --only <handle-substr>
// SAFETY: before deleting each image it GETs the image and refuses unless the image is still present
// on that product (so a stale/already-removed id is a no-op, never a wrong delete).
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const SHOP = 'designer-laboratory-sandbox.myshopify.com'; // LIVE DW store (legacy misnomer)
const API = '2024-10';
const TOKEN = (fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8')
.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim().replace(/"/g, '');
const args = process.argv.slice(2);
const has = f => args.includes(f);
const val = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
const APPLY = has('--apply');
const LIMIT = parseInt(val('--limit', '0'), 10);
const ONLY = val('--only', '');
const TARGETS = path.join(ROOT, 'data/tk11086-jewelry-removal-targets.jsonl');
const MAP = JSON.parse(fs.readFileSync(path.join(ROOT, 'data/shopify-map.json'), 'utf8'));
const byHandle = new Map(MAP.map(r => [r.handle, r]));
let rows = fs.readFileSync(TARGETS, 'utf8').trim().split('\n').map(l => JSON.parse(l));
if (ONLY) rows = rows.filter(r => r.handle.includes(ONLY));
if (LIMIT) rows = rows.slice(0, LIMIT);
const api = (method, url, body) => {
const opt = { method, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' } };
if (body) opt.body = JSON.stringify(body);
return fetch(`https://${SHOP}/admin/api/${API}/${url}`, opt);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
console.log(`Store: ${SHOP} (LIVE) API ${API}`);
console.log(`Targets: ${rows.length} | mode: ${APPLY ? 'APPLY (LIVE DELETE)' : 'DRY-RUN (no writes)'}`);
let ok = 0, gone = 0, missing = 0, err = 0;
for (const r of rows) {
const m = byHandle.get(r.handle);
if (!m || !m.shopify_id) { console.log(` ? ${r.handle}: no product id in map — skip`); missing++; continue; }
const pid = String(m.shopify_id).replace(/\D/g, '');
// verify the image still exists on the product (never blind-delete)
const g = await api('GET', `products/${pid}/images.json`);
if (!g.ok) { console.log(` ! ${r.handle}: images GET ${g.status}`); err++; await sleep(300); continue; }
const imgs = (await g.json()).images || [];
const hit = imgs.find(i => String(i.id) === String(r.image_id));
if (!hit) { console.log(` = ${r.handle}: image ${r.image_id} already gone`); gone++; await sleep(300); continue; }
if (!APPLY) { console.log(` + ${r.handle}: WOULD delete image ${r.image_id}`); ok++; await sleep(120); continue; }
const d = await api('DELETE', `products/${pid}/images/${r.image_id}.json`);
if (d.ok) { console.log(` - ${r.handle}: deleted image ${r.image_id}`); ok++; }
else { console.log(` ! ${r.handle}: DELETE ${d.status}`); err++; }
await sleep(350);
}
console.log(`\n${APPLY ? 'LIVE DELETE' : 'DRY-RUN'} done. ${APPLY ? 'deleted' : 'would-delete'}=${ok} already_gone=${gone} missing_map=${missing} errors=${err}`);