← back to Carnegie Reprice
rebuild-gql.mjs
127 lines
// rebuild-gql — Carnegie per-color rebuild via Shopify GraphQL productSet.
// ONE atomic mutation per product = product + 2 variants + all metafields + images.
// Cost-throttled (reads throttleStatus), resumable (ledger), single-flight (lock), storage-guarded.
// node rebuild-gql.mjs --one DWAG-379296 # build one, print URL (for verify)
// node rebuild-gql.mjs --all # full run (skips ledgered dw_skus)
// node rebuild-gql.mjs --status
import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import path from 'node:path';
const DIR = new URL('.',import.meta.url).pathname;
const ENV = `${process.env.HOME}/Projects/secrets-manager/.env`;
const env = k => { const m=fs.readFileSync(ENV,'utf8').split('\n').find(l=>l.startsWith(k+'=')); return m?m.slice(k.length+1).trim().replace(/^["']|["']$/g,''):''; };
const TOKEN=env('SHOPIFY_ADMIN_TOKEN'); let SHOP=env('SHOPIFY_STORE_DOMAIN')||env('SHOPIFY_STORE'); if(SHOP&&!SHOP.includes('.'))SHOP+='.myshopify.com';
const GQL=`https://${SHOP}/admin/api/2024-10/graphql.json`;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const PSQL=['/opt/homebrew/opt/postgresql@14/bin/psql','/usr/local/opt/postgresql@14/bin/psql','psql'].find(p=>{try{execFileSync(p,['--version'],{stdio:'ignore'});return 1}catch{return 0}})||'psql';
const q1=sql=>execFileSync(PSQL,['postgresql:///dw_unified?host=/tmp','-At','-c',sql],{encoding:'utf8',maxBuffer:128*1024*1024}).trim();
const LEDGER=path.join(DIR,'rebuild-gql-ledger.jsonl'), LOCK=path.join(DIR,'.rebuild-gql.lock'), HALT=path.join(DIR,'rebuild-gql.halt');
const args=process.argv.slice(2); const argVal=f=>{const i=args.indexOf(f);return i>=0?args[i+1]:null;};
// cost-based throttle: keep the GraphQL bucket healthy
let bucket=1000;
async function gql(query,variables){
for(let attempt=0;attempt<7;attempt++){
if(bucket<350) await sleep(1200); // let the bucket restore (~100/s)
const r=await fetch(GQL,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query,variables})});
if(r.status===429||r.status===430){ await sleep(2000*(attempt+1)); continue; }
const j=await r.json();
const ts=j.extensions?.cost?.throttleStatus; if(ts) bucket=ts.currentlyAvailable;
if(j.errors && /throttl/i.test(JSON.stringify(j.errors))){ await sleep(2000*(attempt+1)); continue; }
return j;
}
throw new Error('gql throttled/exhausted');
}
// --- metafield-definition guard: skip any (ns,key) whose store definition is NOT single_line_text_field ---
let BADDEF=new Set();
async function loadBadDefs(){
const j=await gql(`{ metafieldDefinitions(ownerType: PRODUCT, first: 250){ nodes{ namespace key type{ name } } } }`);
for(const d of (j.data?.metafieldDefinitions?.nodes||[])) if(d.type?.name!=='single_line_text_field') BADDEF.add(d.namespace+'