[object Object]

← back to Dw Yolo Loop

Kravet dedup: archived 18 duplicate products live + 301 redirects to keeper handles (keeper-ACTIVE guard, read-back verified, 0 err)

d8390e117c161c070c2e2dde693b17905b3b1cd1 · 2026-06-15 10:57:55 -0700 · Steve Abrams

Files touched

Diff

commit d8390e117c161c070c2e2dde693b17905b3b1cd1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 15 10:57:55 2026 -0700

    Kravet dedup: archived 18 duplicate products live + 301 redirects to keeper handles (keeper-ACTIVE guard, read-back verified, 0 err)
---
 scripts/kravet-dedup-archive.js | 52 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 52 insertions(+)

diff --git a/scripts/kravet-dedup-archive.js b/scripts/kravet-dedup-archive.js
new file mode 100644
index 0000000..a435f0a
--- /dev/null
+++ b/scripts/kravet-dedup-archive.js
@@ -0,0 +1,52 @@
+#!/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);});

← 5cb00ac Trending 2026: hard cap, raise to newest 1000 SKUs (no Artmu  ·  back to Dw Yolo Loop  ·  Schumacher add-roll: idempotent skip + accumulate revert-log 11048a7 →