← back to Dw Image Shrink

gate-imageless-all.js

46 lines

// Store-wide: set ANY active product with NO photo (featuredImage == null) to
// DRAFT, so nothing shows imageless while uploads are cap-blocked. Reversible:
// every drafted product logged to gated-imageless-all.jsonl (id, handle) for
// one-pass re-activation once images can be attached again.
// DRY-RUN by default; --apply to write.  node gate-imageless-all.js [--apply]
import fs from 'fs';
import https from 'https';
const SHOP='designer-laboratory-sandbox.myshopify.com';
const ATOK=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=').slice(1).join('=').replace(/["' ]/g,'');
const DIR='/Users/macstudio3/Projects/dw-image-shrink';
const APPLY=process.argv.includes('--apply');
const LOG=DIR+'/gated-imageless-all.jsonl';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gql(query,variables){return new Promise((res,rej)=>{const body=JSON.stringify({query,variables});const req=https.request({method:'POST',host:SHOP,path:'/admin/api/2024-10/graphql.json',headers:{'X-Shopify-Access-Token':ATOK,'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)}},r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{try{res(JSON.parse(d))}catch(e){res({errors:[{message:d.slice(0,120)}]})}});});req.on('error',rej);req.write(body);req.end();});}

// PHASE 1 — enumerate active products with NO featuredImage
const Q=(c)=>`{ products(first: 250, query: "status:active"${c?`, after: "${c}"`:''}) { pageInfo{ hasNextPage endCursor } edges{ node{ id handle featuredImage{ id } } } } }`;
const imageless=[]; let cursor=null, scanned=0, page=0;
while(true){
  let r; for(let a=0;a<6;a++){ r=await gql(Q(cursor)); if(r.errors&&/throttl/i.test(JSON.stringify(r.errors))){await sleep(2000);continue;} break; }
  if(r.errors){ console.error('ENUM ERR',JSON.stringify(r.errors).slice(0,160)); break; }
  const conn=r.data.products;
  for(const e of conn.edges){ scanned++; if(!e.node.featuredImage) imageless.push({id:e.node.id,handle:e.node.handle}); }
  page++; if(page%20===0) console.log(`  scanned ${scanned} active | imageless ${imageless.length}`);
  if(!conn.pageInfo.hasNextPage) break; cursor=conn.pageInfo.endCursor;
  const c=r.extensions&&r.extensions.cost&&r.extensions.cost.throttleStatus; await sleep(c&&c.currentlyAvailable<600?800:120);
}
console.log(`\n${APPLY?'APPLYING':'DRY-RUN'}: ${scanned} active products, ${imageless.length} with NO photo → draft`);
if(!APPLY){ fs.writeFileSync(DIR+'/imageless-active-preview.json',JSON.stringify(imageless.slice(0,50),null,2)); console.log('  (dry-run; sample → imageless-active-preview.json)'); process.exit(0); }

// PHASE 2 — set each to draft
const done=new Set(); if(fs.existsSync(LOG)) for(const l of fs.readFileSync(LOG,'utf8').split('\n')) if(l){try{done.add(JSON.parse(l).id)}catch(e){}}
const out=fs.createWriteStream(LOG,{flags:'a'});
const M=`mutation($id:ID!){ productUpdate(input:{id:$id, status:DRAFT}){ product{ id status } userErrors{ message } } }`;
let ok=0,err=0;
for(const p of imageless){
  if(done.has(p.id)) continue;
  let r; for(let a=0;a<6;a++){ r=await gql(M,{id:p.id}); if(r.errors&&/throttl/i.test(JSON.stringify(r.errors))){await sleep(2000);continue;} break; }
  if(r.errors||!r.data||!r.data.productUpdate||r.data.productUpdate.userErrors.length){ err++; }
  else { out.write(JSON.stringify({id:p.id,handle:p.handle,prev:'active',ts:new Date().toISOString()})+'\n'); ok++; }
  if((ok+err)%100===0) console.log(`  drafted ${ok} / err ${err}`);
  const c=r.extensions&&r.extensions.cost&&r.extensions.cost.throttleStatus; await sleep(c&&c.currentlyAvailable<600?900:250);
}
out.end();
console.log(`DONE: ${ok} products set to draft, ${err} errors. Reversible via ${LOG}.`);