← back to Dw Yolo Loop
scripts/orphan-products/orphan-products-scan.mjs
20 lines
import fs from 'fs';
const T=(fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1];
const URL='https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v){for(let a=0;a<6;a++){const r=await fetch(URL,{method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v||{}})});if(r.status===429){await sleep(2000);continue;}const j=await r.json();if(j.errors&&JSON.stringify(j.errors).match(/THROTTLED/)){await sleep(2000);continue;}return j;}throw new Error('throttled');}
// sweep ACTIVE products, count collection membership (authoritative collections connection)
let cur=null,total=0,orphan=0,pages=0; const byVendor={}, samples=[]; const t0=Date.now();
do{
const j=await gql(`query($c:String){products(first:100,after:$c,query:"status:active"){pageInfo{hasNextPage endCursor} nodes{handle vendor collections(first:1){nodes{id}}}}}`,{c:cur});
const pg=j.data.products;
for(const n of pg.nodes){ total++; if(n.collections.nodes.length===0){ orphan++; byVendor[n.vendor]=(byVendor[n.vendor]||0)+1; if(samples.length<12)samples.push(n.handle+' ['+n.vendor+']'); } }
pages++; if(pages%100===0)process.stderr.write(` ...${pages}p ${total} prod, orphan=${orphan}\n`);
cur=pg.pageInfo.hasNextPage?pg.pageInfo.endCursor:null;
}while(cur);
console.log(`scanned ${total} ACTIVE in ${((Date.now()-t0)/1000).toFixed(0)}s | ORPHAN (0 collections): ${orphan} (${(100*orphan/total).toFixed(1)}%)`);
const top=Object.entries(byVendor).sort((a,b)=>b[1]-a[1]).slice(0,12);
console.log('top vendors:'); top.forEach(([k,v])=>console.log(` ${v} ${k}`));
console.log('samples:'); samples.forEach(s=>console.log(' '+s));
fs.writeFileSync('/tmp/orphan-prod.json',JSON.stringify({total,orphan,byVendor,samples}));