← back to Dw Yolo Loop

scripts/gmc-feed-guard/export-leak-list.mjs

44 lines

// export-leak-list.mjs — READ-ONLY. Same scan as gmc-feed-guard but captures the
// FULL offender list (not capped at 20) to a CSV for the feed-exclusion remediation.
// Emits: handle, vendor, productId, minPrice, maxPrice, variantCount. $0, no writes to Shopify.
import fs from 'fs';
const T=(fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1];
const API='https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const GOOG='gid://shopify/Publication/29646651457';
const OUT=process.env.HOME+'/Projects/dw-yolo-loop/scripts/gmc-feed-guard/data/leak-list.csv';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v){
  for(let a=0;a<7;a++){
    try{ const r=await fetch(API,{method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v||{}}),signal:AbortSignal.timeout(20000)});
      if(r.status===429||r.status>=500){await sleep(2500*(a+1));continue;}
      const txt=await r.text(); let j; try{j=JSON.parse(txt);}catch(e){await sleep(2500*(a+1));continue;}
      if(j.errors&&JSON.stringify(j.errors).match(/THROTTLED/)){await sleep(2500*(a+1));continue;}
      return j;
    }catch(e){ await sleep(2000*(a+1)); }
  }
  throw new Error('exhausted retries');
}
let cur=null,scanned=0,leaks=0; const rows=[['handle','vendor','productId','minPrice','maxPrice','variantCount']]; const byVendor={}; const t0=Date.now();
do{
  const d=await gql(`query($c:String){products(first:150,after:$c,query:"status:active"){pageInfo{hasNextPage endCursor} nodes{id handle vendor onGoogle:publishedOnPublication(publicationId:"${GOOG}") variants(first:60){nodes{price}}}}}`,{c:cur});
  const pg=d.data.products;
  for(const p of pg.nodes){ scanned++;
    const prices=p.variants.nodes.map(v=>parseFloat(v.price)).filter(x=>!isNaN(x));
    if(!prices.length) continue;
    const mn=Math.min(...prices), mx=Math.max(...prices);
    if(mn<=4.25 && p.onGoogle && mx>4.25){
      leaks++;
      rows.push([p.handle, (p.vendor||'').replace(/,/g,' '), p.id, mn, mx, prices.length]);
      byVendor[p.vendor||'(none)']=(byVendor[p.vendor||'(none)']||0)+1;
    }
  }
  process.stderr.write(`\r  scanned=${scanned} leaks=${leaks}`);
  cur=pg.pageInfo.hasNextPage?pg.pageInfo.endCursor:null;
}while(cur);
fs.writeFileSync(OUT, rows.map(r=>r.join(',')).join('\n'));
process.stderr.write('\n');
console.log(`[export-leak-list] scanned=${scanned} leaks=${leaks} elapsed_s=${((Date.now()-t0)/1000)|0}`);
console.log('CSV:',OUT);
console.log('top leaking vendors:');
Object.entries(byVendor).sort((a,b)=>b[1]-a[1]).slice(0,15).forEach(([v,n])=>console.log(`  ${n.toString().padStart(5)}  ${v}`));