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