← back to Rebel Walls Push
scripts/list-rollbolt.js
107 lines
#!/usr/bin/env node
/**
* TK-10029 (280 roll/bolt review): READ-ONLY audit.
* Pulls every Rebel Walls product whose variant carries a roll/bolt unit label
* (the cohort HELD out of the Scope-B per-m² relabel) and groups them by exact
* label, with title + SKU + product id. No writes. $0.
*
* Usage: node scripts/list-rollbolt.js
* node scripts/list-rollbolt.js --json # machine-readable
*/
const https = require('https');
const fs = require('fs');
const SECRETS_ENV = '/Users/macstudio3/Projects/secrets-manager/.env';
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API_VERSION = '2024-10';
const TOKEN = (() => {
const env = fs.readFileSync(SECRETS_ENV, 'utf8');
const m = env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m);
if (!m) throw new Error('SHOPIFY_FULL_ACCESS_TOKEN not found');
return m[1].trim();
})();
const JSON_OUT = process.argv.includes('--json');
// The held roll/bolt unit-of-sale cohort (mirrors relabel-units.js HELD_ROLLBOLT_LABELS,
// plus the interim canonical-adjacent ones we DON'T count here — we want only the
// genuine roll/bolt unit labels that need Steve's per-m²-or-not ruling).
const ROLLBOLT_LABELS = new Set([
'roll', 'single roll', 'sold per roll', 'sold per bolt (20.5in x 33ft)',
]);
const sleep = ms => new Promise(r => setTimeout(r, ms));
function gqlOnce(query, vars) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ query, variables: vars || {} });
const req = https.request({
hostname: DOMAIN, path: `/admin/api/${API_VERSION}/graphql.json`, method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
}, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error(`non-JSON (${res.statusCode}): ${String(d).slice(0, 80)}`)); } }); });
req.on('error', reject); req.write(body); req.end();
});
}
// Throttle-aware: a Shopify cost throttle is a 200 with top-level `errors` (THROTTLED),
// NOT a parse failure — so treat data-less/THROTTLED responses as retryable too.
async function gql(q, v) {
let last;
for (let a = 1; a <= 8; a++) {
try {
const r = await gqlOnce(q, v);
if (r && r.data) return r;
const throttled = (r.errors || []).some(e => /throttl/i.test(e.message || '') || e.extensions?.code === 'THROTTLED');
last = new Error(`no data${throttled ? ' (THROTTLED)' : ''}: ${JSON.stringify(r.errors || r).slice(0, 120)}`);
} catch (e) { last = e; }
if (a < 8) await sleep(Math.min(10000, 500 * 2 ** (a - 1)));
}
throw last;
}
async function fetchAll() {
const products = []; let cursor = null;
while (true) {
const r = await gql(`query($after: String) {
products(first: 100, query: "vendor:Rebel Walls", after: $after) {
pageInfo { hasNextPage endCursor }
edges { node { id title
variants(first: 10) { edges { node { id sku title selectedOptions { name value } } } } } }
} }`, { after: cursor });
const page = r.data.products;
for (const e of page.edges) products.push(e.node);
if (!page.pageInfo.hasNextPage) break;
cursor = page.pageInfo.endCursor; await sleep(300);
}
return products;
}
(async () => {
const all = await fetchAll();
const groups = {}; // exact-label -> [{title, id, sku}]
for (const p of all) {
const titleLower = p.title.toLowerCase();
if (titleLower.includes(' sample') || titleLower.endsWith('sample')) continue; // skip memo samples
// find the roll/bolt-labeled variant (record ONCE per product, by the matched value)
let matched = null, sku = null;
for (const v of p.variants.edges.map(e => e.node)) {
for (const o of v.selectedOptions) {
if (ROLLBOLT_LABELS.has(o.value.toLowerCase())) { matched = o.value; sku = v.sku; break; }
}
if (matched) break;
}
if (!matched) continue;
(groups[matched] = groups[matched] || []).push({ title: p.title, id: p.id, sku });
}
if (JSON_OUT) { console.log(JSON.stringify(groups, null, 2)); return; }
const labels = Object.keys(groups).sort((a, b) => groups[b].length - groups[a].length);
let total = 0;
for (const label of labels) {
const rows = groups[label]; total += rows.length;
console.log(`\n=== "${label}" (${rows.length}) ===`);
for (const r of rows.sort((a, b) => a.title.localeCompare(b.title))) {
console.log(` ${(r.sku || '(no sku)').padEnd(22)} ${r.title}`);
}
}
console.log(`\n[list-rollbolt] TOTAL held roll/bolt products: ${total} (across ${labels.length} label(s))`);
})().catch(e => { console.error('[list-rollbolt] FATAL', e.message); process.exit(1); });