← back to Imageless Rescue

scripts/width-from-catalog.js

38 lines

'use strict';
// Fill global.width for active-gate width-violators from the per-vendor *_catalog
// tables (DB lookup, NO scraping, NO images). Additive, idempotent, resumable, dry-run.
const fs=require('fs'),os=require('os'),path=require('path'),https=require('https');
const DRY=process.argv.includes('--dry-run');
const LIMIT=process.argv.includes('--limit')?parseInt(process.argv[process.argv.indexOf('--limit')+1],10):Infinity;
function tok(){if(process.env.SHOPIFY_ADMIN_TOKEN)return process.env.SHOPIFY_ADMIN_TOKEN.replace(/['"]/g,'').trim();try{return fs.readFileSync(path.join(os.homedir(),'Projects/secrets-manager/.env'),'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m)[1].replace(/['"]/g,'').trim()}catch{return null}}
const T=tok(),STORE='designer-laboratory-sandbox.myshopify.com',API='2024-10',sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gql(q,v){return new Promise(res=>{const b=JSON.stringify({query:q,variables:v});const r=https.request({hostname:STORE,path:`/admin/api/${API}/graphql.json`,method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json','Content-Length':Buffer.byteLength(b)}},x=>{let d='';x.on('data',c=>d+=c);x.on('end',()=>{try{res(JSON.parse(d))}catch{res({})}})});r.on('error',()=>res({}));r.write(b);r.end()})}
async function G(q,v){for(let a=0;a<6;a++){const r=await gql(q,v);if(r.errors&&JSON.stringify(r.errors).toUpperCase().includes('THROTTL')){await sleep(2000*(a+1));continue}return r}return{}}
const PROD=`query($id:ID!){product(id:$id){id title vendor mfc:metafield(namespace:"custom",key:"manufacturer_sku"){value} mfg:metafield(namespace:"global",key:"manufacturer_sku"){value} variants(first:5){nodes{sku}}}}`;
const SET=`mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{message}}}`;
// extract a clean width like 36", 20.5 in, 54 Inches from a noisy spec string
function cleanWidth(raw){if(!raw)return null;const m=String(raw).match(/(\d+(?:\.\d+)?)\s*(?:in\b|inch|inches|")/i);if(m)return `${m[1]} Inches`;const cm=String(raw).match(/(\d+(?:\.\d+)?)\s*cm\b/i);if(cm)return `${cm[1]} cm`;return null;}
const map=new Map();for(const l of fs.readFileSync(path.join(__dirname,'..','data','width-map.csv'),'utf8').split('\n')){if(!l)continue;const i=l.indexOf(',');if(i>0)map.set(l.slice(0,i).toLowerCase(),l.slice(i+1));}
(async()=>{
  const d=require('../data/active-gate-violations-2026-06-21.json');
  const vio=(d.violations||[]).filter(x=>(x.types||[]).includes('specs'));
  const LED=path.join(__dirname,'..','data','width-catalog-ledger.jsonl');const done=new Set();
  if(fs.existsSync(LED))for(const l of fs.readFileSync(LED,'utf8').split('\n')){if(l.trim())try{done.add(JSON.parse(l).id)}catch{}}
  const queue=vio.filter(x=>!done.has(x.id)).slice(0,LIMIT===Infinity?undefined:LIMIT);
  console.log(`width-from-catalog ${DRY?'[DRY] ':''}violators=${vio.length} done=${done.size} thisRun=${queue.length} mapSize=${map.size}`);
  const st={filled:0,noSku:0,noMatch:0,noClean:0,err:0};
  for(const x of queue){
    const pr=await G(PROD,{id:x.id});const p=pr.data&&pr.data.product;if(!p){st.err++;continue}
    const skus=[(p.mfc&&p.mfc.value),(p.mfg&&p.mfg.value),...(p.variants.nodes||[]).map(v=>(v.sku||'').replace(/-sample$/i,''))].filter(Boolean).map(s=>s.toLowerCase().trim());
    let raw=null,hitSku=null;for(const s of [...new Set(skus)]){if(map.has(s)){raw=map.get(s);hitSku=s;break}}
    if(!skus.length){st.noSku++;continue}
    if(!raw){st.noMatch++;if(!DRY)fs.appendFileSync(LED,JSON.stringify({id:x.id,result:'no-catalog-match',skus})+'\n');continue}
    const w=cleanWidth(raw);
    if(!w){st.noClean++;if(!DRY)fs.appendFileSync(LED,JSON.stringify({id:x.id,result:'no-clean-width',raw})+'\n');continue}
    if(DRY){st.filled++;if(st.filled<=12)console.log(`  • ${p.title.slice(0,42)} [${hitSku}] → "${w}"  (raw="${String(raw).slice(0,40)}")`);continue}
    const mr=await G(SET,{mf:[{ownerId:x.id,namespace:'global',key:'width',type:'single_line_text_field',value:w}]});
    if(mr.data&&mr.data.metafieldsSet&&!mr.data.metafieldsSet.userErrors.length){st.filled++;fs.appendFileSync(LED,JSON.stringify({id:x.id,width:w,sku:hitSku,result:'filled'})+'\n');if(st.filled%50===0)console.log(`  ✓ ${st.filled} widths set`)}else st.err++;
  }
  console.log(`\n=== ${DRY?'DRY ':''}SUMMARY ===`,JSON.stringify(st));
})().catch(e=>{console.error('FATAL',e);process.exit(1)});