← back to Dw Yolo Loop

showroom-lines-tag.js

89 lines

'use strict';
/**
 * Showroom Lines — bulk tag + metafield (LIVE write, Steve-authorized 2026-06-16).
 * Tags each showroom-line product "Showroom Line" and sets custom.showroom_line=true.
 * - Reads IDs from showroom-lines-worklist.json (the read-only scan output).
 * - Idempotent + RESUMABLE: completed IDs appended to showroom-lines-done.txt;
 *   a restart skips them. Safe to Ctrl-C and rerun.
 * - Reversible (remove the tag / metafield).
 * - $0: Shopify Admin API, no per-call charge.
 */
const fs = require('fs');
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const VERSION = '2024-10';
const ENV = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1];
if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const ENDPOINT = `https://${SHOP}/admin/api/${VERSION}/graphql.json`;
const DONE_FILE = __dirname + '/showroom-lines-done.txt';
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

async function gql(query, variables) {
  for (let attempt = 0; attempt < 8; attempt++) {
    let res;
    try {
      res = await fetch(ENDPOINT, { method: 'POST',
        headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
        body: JSON.stringify({ query, variables }) });
    } catch (e) { await sleep(2000 * (attempt + 1)); continue; }
    if (res.status === 429) { await sleep(2000 * (attempt + 1)); continue; }
    const json = await res.json();
    if (json.errors) {
      if (JSON.stringify(json.errors).includes('THROTTLED')) { await sleep(2000 * (attempt + 1)); continue; }
      throw new Error(JSON.stringify(json.errors));
    }
    const avail = json.extensions?.cost?.throttleStatus?.currentlyAvailable ?? 4000;
    if (avail < 500) await sleep(1200);
    return json.data;
  }
  throw new Error('exhausted retries');
}

const DEF = `mutation { metafieldDefinitionCreate(definition: {
  name: "Showroom Line", namespace: "custom", key: "showroom_line",
  type: "boolean", ownerType: PRODUCT,
  description: "Single-variant $4.25 memo-sample product sold as a showroom line."
}) { createdDefinition { id } userErrors { code message } } }`;

const MUT = `mutation($id: ID!) {
  t: tagsAdd(id: $id, tags: ["Showroom Line"]) { userErrors { message } }
  m: metafieldsSet(metafields: [{ ownerId: $id, namespace: "custom", key: "showroom_line", type: "boolean", value: "true" }]) { userErrors { message } }
}`;

(async () => {
  // 1) ensure the metafield definition exists (idempotent — ignore "taken")
  try {
    const d = await gql(DEF, {});
    const errs = d.metafieldDefinitionCreate.userErrors || [];
    if (errs.length && !/taken|exists/i.test(JSON.stringify(errs))) console.warn('def warn:', JSON.stringify(errs));
    else console.log('metafield definition custom.showroom_line ready');
  } catch (e) { console.warn('def step:', e.message); }

  // 2) load worklist + resume set
  const wl = JSON.parse(fs.readFileSync(__dirname + '/showroom-lines-worklist.json', 'utf8'));
  const allIds = wl.ids;
  const done = new Set(fs.existsSync(DONE_FILE) ? fs.readFileSync(DONE_FILE, 'utf8').split('\n').filter(Boolean) : []);
  const todo = allIds.filter(id => !done.has(id));
  console.log(`worklist=${allIds.length}  alreadyDone=${done.size}  todo=${todo.length}`);

  const doneStream = fs.createWriteStream(DONE_FILE, { flags: 'a' });
  let ok = 0, fail = 0;
  const t0 = Date.now();
  for (let i = 0; i < todo.length; i++) {
    const id = todo[i];
    try {
      const d = await gql(MUT, { id });
      const e = [...(d.t?.userErrors || []), ...(d.m?.userErrors || [])];
      if (e.length) { fail++; if (fail <= 20) console.warn('userErr', id, JSON.stringify(e)); }
      else { ok++; doneStream.write(id + '\n'); }
    } catch (e) { fail++; if (fail <= 20) console.warn('fail', id, e.message); }
    if ((i + 1) % 200 === 0) {
      const rate = (i + 1) / ((Date.now() - t0) / 1000);
      const eta = Math.round((todo.length - i - 1) / rate / 60);
      console.log(`  ${i + 1}/${todo.length}  ok=${ok} fail=${fail}  ${rate.toFixed(1)}/s  ~${eta}min left`);
    }
  }
  doneStream.end();
  console.log(`\nDone: ${ok} tagged, ${fail} failed, ${done.size} pre-done. Total showroom-tagged ≈ ${ok + done.size}.`);
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });