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