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