← back to Dw Yolo Loop
scripts/kravet-dedup-archive.js
53 lines
#!/usr/bin/env node
/* Archive the 18 verified duplicate products (/tmp/kravet_archive_list.csv) and add a 301 redirect
from each archived handle -> its keeper handle. PER-ROW GUARD: the KEEPER must be ACTIVE first
(never archive a dup if it would leave zero live). Idempotent (skips already-archived). Reads back
to verify status=ARCHIVED. Requires APPLY=1 to write. NO deletes — archive is reversible. */
const https=require('https'), fs=require('fs');
const T=process.env.T, S='designer-laboratory-sandbox.myshopify.com', APPLY=process.env.APPLY==='1';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gqlOnce(q,v){return new Promise((res,rej)=>{const rq=https.request(`https://${S}/admin/api/2024-10/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json'}},x=>{let b='';x.on('data',d=>b+=d);x.on('end',()=>{try{res(JSON.parse(b))}catch(e){rej(b.slice(0,200))}})});rq.on('error',rej);rq.setTimeout(30000,()=>rq.destroy(new Error('timeout')));rq.write(JSON.stringify({query:q,variables:v||{}}));rq.end();});}
async function gql(q,v){for(let i=0;i<4;i++){try{return await gqlOnce(q,v);}catch(e){if(i===3)throw e;await sleep(1200*(i+1));}}}
const rows=fs.readFileSync('/tmp/kravet_archive_list.csv','utf8').trim().split('\n').slice(1).map(l=>{
const [archive_id,from_handle,to_handle,vendor,sku]=l.split(','); return {archive_id,from_handle,to_handle,vendor,sku};});
const UPD=`mutation($id:ID!){ productUpdate(input:{id:$id,status:ARCHIVED}){ product{ id status } userErrors{ field message } } }`;
const RED=`mutation($r:UrlRedirectInput!){ urlRedirectCreate(urlRedirect:$r){ urlRedirect{ id path target } userErrors{ field message } } }`;
(async()=>{
const log=['vendor,sku,archive_id,from_handle,to_handle,result,redirect'];
let done=0, skip=0, err=0;
console.log(`MODE: ${APPLY?'APPLY (archiving live)':'DRY'} | products: ${rows.length}\n`);
for(const r of rows){
const agid=`gid://shopify/Product/${r.archive_id}`;
// 1) status of the dup + keeper-active guard (find keeper by handle)
let s; try{ s=await gql(`{ dup:product(id:"${agid}"){ status }
keep:productByHandle(handle:"${r.to_handle}"){ id status } }`); }
catch(e){ console.log(` ${r.sku} FETCH-ERR ${e}`); err++; continue; }
const dup=s.data?.dup, keep=s.data?.keep;
if(!dup){ console.log(` ${r.sku} dup-missing skip`); log.push(`${r.vendor},${r.sku},${r.archive_id},${r.from_handle},${r.to_handle},DUP-MISSING,`); skip++; continue; }
if(dup.status==='ARCHIVED'){ console.log(` ${r.sku} already-archived`); log.push(`${r.vendor},${r.sku},${r.archive_id},${r.from_handle},${r.to_handle},ALREADY-ARCHIVED,`); skip++; continue; }
if(!keep || keep.status!=='ACTIVE'){ console.log(` ${r.sku} ⚠ KEEPER not ACTIVE (${keep?.status||'missing'}) — SKIP (won't leave zero live)`); log.push(`${r.vendor},${r.sku},${r.archive_id},${r.from_handle},${r.to_handle},KEEPER-NOT-ACTIVE-SKIP,`); skip++; continue; }
if(!APPLY){ console.log(` WOULD archive ${r.sku} ${r.from_handle} -> keep ${r.to_handle}`); log.push(`${r.vendor},${r.sku},${r.archive_id},${r.from_handle},${r.to_handle},WOULD-ARCHIVE,would-redirect`); continue; }
// 2) archive
const u=await gql(UPD,{id:agid}); const ue=u.data?.productUpdate?.userErrors||[];
if(ue.length){ console.log(` ${r.sku} ARCHIVE-ERR ${JSON.stringify(ue)}`); log.push(`${r.vendor},${r.sku},${r.archive_id},${r.from_handle},${r.to_handle},ERR,`); err++; continue; }
const okv=u.data.productUpdate.product?.status==='ARCHIVED';
// 3) redirect old handle -> keeper handle (skip if from==to)
let red='none';
if(r.from_handle && r.from_handle!==r.to_handle){
const rr=await gql(RED,{r:{path:`/products/${r.from_handle}`,target:`/products/${r.to_handle}`}});
const rue=rr.data?.urlRedirectCreate?.userErrors||[];
red = rue.length ? ('redirect-err:'+rue.map(e=>e.message).join(';')) : 'redirected';
}
console.log(` ${okv?'✓':'✗'} ${r.sku} archived | ${red}`);
if(okv) done++; else err++;
log.push(`${r.vendor},${r.sku},${r.archive_id},${r.from_handle},${r.to_handle},${okv?'ARCHIVED':'VERIFY-FAIL'},${red}`);
await sleep(350);
}
fs.writeFileSync('/tmp/kravet_archive_done.csv',log.join('\n'));
console.log(`\n=== ${APPLY?'APPLIED':'DRY'} === archived:${done} skipped:${skip} err:${err}`);
console.log(`log -> /tmp/kravet_archive_done.csv`);
})().catch(e=>{console.error(e);process.exit(1);});