← back to Tk10630 Sku Suffix Canary

verify-mfr-rest.mjs

48 lines

// AIRTIGHT via REST (Link-header pagination — no GraphQL cursor bug) for the exact
// active product-ID list, then GraphQL nodes(ids) batches for the mfr check. READ-ONLY.
import { readFileSync } from 'node:fs';
import { gql } from './shopify.mjs';
import { FABRICATED } from './lib.mjs';

const E = Object.fromEntries(readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`, 'utf8').split('\n').map(l => l.match(/^([A-Z0-9_]+)=(.*)$/)).filter(Boolean).map(m => [m[1], m[2].replace(/^["']|["']$/g, '')]));
const STORE = E.SHOPIFY_STORE_DOMAIN, TOKEN = E.SHOPIFY_ADMIN_TOKEN, API = '2024-10';
const sleep = ms => new Promise(r => setTimeout(r, ms));

// 1) REST paginate active Hollywood product ids (reliable Link-header pagination)
const ids = [];
let url = `https://${STORE}/admin/api/${API}/products.json?vendor=${encodeURIComponent('Hollywood Wallcoverings')}&status=active&limit=250&fields=id`;
while (url) {
  const r = await fetch(url, { headers: { 'X-Shopify-Access-Token': TOKEN } });
  if (r.status === 429) { await sleep(2000); continue; }
  const j = await r.json();
  for (const p of j.products || []) ids.push(`gid://shopify/Product/${p.id}`);
  const link = r.headers.get('link') || '';
  const m = link.match(/<([^>]+)>;\s*rel="next"/);
  url = m ? m[1] : null;
  process.stderr.write(`  REST: ${ids.length} active ids\n`);
}
const uniq = [...new Set(ids)];

// 2) GraphQL nodes(ids) batches for the mfr check
const realCode = s => !!s && /^[A-Z]{2,6}-?\d/i.test(s) && !FABRICATED.test(s);
// A real mfr code: not null, not a multi-word English NAME, and structured like a
// SKU (contains a digit OR a hyphen joining uppercase code tokens, e.g. PI-NATIVE-BLACK).
const realMfr = s => { if (!s) return false; const t = s.trim(); if (t.split(/\s+/).length >= 3) return false; if (FABRICATED.test(t)) return false; return /\d/.test(t) || /^[A-Z0-9]+(-[A-Z0-9]+)+$/i.test(t); };
let ok = 0; const missing = [];
for (let i = 0; i < uniq.length; i += 50) {
  const chunk = uniq.slice(i, i + 50).map(x => `"${x}"`).join(',');
  let data;
  for (let a = 0; ; a++) { try { ({ data } = await gql(`query{ nodes(ids:[${chunk}]){ ... on Product{ id handle title
    g:metafield(namespace:"global",key:"dw_sku"){value} d:metafield(namespace:"dwc",key:"dw_sku"){value} c:metafield(namespace:"custom",key:"dw_sku"){value}
    mfr:metafield(namespace:"custom",key:"manufacturer_sku"){value} dmfr:metafield(namespace:"dwc",key:"manufacturer_sku"){value}
    variants(first:6){ nodes{ sku } } } } }`)); break; } catch (e) { if (/THROTTLED/i.test(e.message) && a < 8) { await sleep(2000 * (a + 1)); continue; } throw e; } }
  for (const p of data.nodes) {
    if (!p) continue;
    const codes = [p.g?.value, p.d?.value, p.c?.value, ...p.variants.nodes.map(v => (v.sku || '').replace(/-(sample|yard|roll)$/i, ''))];
    if (codes.some(realCode) || realMfr(p.mfr?.value) || realMfr(p.dmfr?.value)) ok++;
    else missing.push({ handle: p.handle, title: p.title, mfr: p.mfr?.value || p.dmfr?.value });
  }
}
console.log(JSON.stringify({ exact_unique_active: uniq.length, active_with_real_mfr: ok, active_NO_real_mfr: missing.length }, null, 2));
if (missing.length) console.log('MISSING:', missing.map(m => `${m.handle} [mfr=${m.mfr}]`).join('\n  '));