← back to Dwjs Consolidation 2026 04 23
reactivate.js
93 lines
#!/usr/bin/env node
// Reactivate 490 qualifying ARCHIVED products (image + priced bolt + sample + body)
// Status → ACTIVE via productChangeStatus mutation.
const https = require('https');
const fs = require('fs');
const { isProtected } = require('/Users/macstudio3/Projects/_shared/archive-guard');
const STORE='designer-laboratory-sandbox.myshopify.com';
const TOKEN=(process.env.SHOPIFY_ADMIN_TOKEN || '');
const LOG = '/Users/macstudio3/Projects/dwjs-consolidation-2026-04-23/reactivate.log';
const log = m => { const l=`[${new Date().toISOString().slice(0,19)}] ${m}\n`; process.stdout.write(l); fs.appendFileSync(LOG,l); };
function gql(body, retry=0) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const req = https.request({ hostname:STORE, path:'/admin/api/2024-10/graphql.json', method:'POST',
headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json','Content-Length':Buffer.byteLength(data)} },
res => { let c=''; res.on('data',d=>c+=d); res.on('end', async ()=>{
try {
const j = JSON.parse(c);
const tri = j?.extensions?.cost?.throttleStatus;
const isThr = j.errors && /throttle/i.test(JSON.stringify(j.errors));
if (isThr && retry<5) { await new Promise(r=>setTimeout(r,2000*(retry+1))); return resolve(gql(body,retry+1)); }
if (tri && tri.currentlyAvailable < 300) await new Promise(r=>setTimeout(r,800));
resolve(j);
} catch { if (retry<3) setTimeout(()=>resolve(gql(body,retry+1)),2000); else resolve({error:c}); }
}); }
);
req.on('error', err => { if (retry<3) setTimeout(()=>resolve(gql(body,retry+1)),2000); else reject(err); });
req.setTimeout(30000, ()=>{ req.destroy(); if (retry<3) resolve(gql(body,retry+1)); else reject(new Error('t')); });
req.write(data); req.end();
});
}
// Rebuild qualifiers from audit jsonl
const products = {};
for (const l of fs.readFileSync('/Users/macstudio3/Projects/dwjs-consolidation-2026-04-23/audit_completeness.jsonl','utf8').split('\n').filter(Boolean)) {
const o = JSON.parse(l);
if (o.id?.includes('/Product/')) products[o.id] = {...o, variants:[]};
else if (o.id?.includes('/ProductVariant/') && o.__parentId) {
const p = products[o.__parentId];
if (p) p.variants.push({sku:o.sku||'',title:o.title||'',price:parseFloat(o.price||'0')});
}
}
const targets = [];
for (const p of Object.values(products)) {
if (p.status !== 'ARCHIVED') continue;
const img = !!(p.images && p.images.url);
const bolt = p.variants.find(v => !/sample/i.test(v.sku+v.title));
const sample = p.variants.find(v => /sample/i.test(v.sku+v.title));
const body = (p.bodyHtml||'').trim().length >= 10;
if (img && bolt && bolt.price>0 && sample && body) {
// archive-guard: never re-activate intentionally-archived SKUs
const allSkus = p.variants.map(v => v.sku).filter(Boolean);
if (allSkus.some(s => isProtected(s))) {
log(`SKIP archive-guard ${p.id}: protected SKU in ${JSON.stringify(allSkus)}`);
continue;
}
targets.push(p);
}
}
log(`qualifying archived products: ${targets.length}`);
(async () => {
let ok=0, fail=0, progress=0;
const concurrency = 3;
let idx=0, inFlight=0;
await new Promise(resolve => {
const next = () => {
while (inFlight < concurrency && idx < targets.length) {
const p = targets[idx++];
inFlight++;
(async () => {
const r = await gql({
query: `mutation($id: ID!) { productChangeStatus(productId:$id, status:ACTIVE) { product { id status } userErrors { field message } } }`,
variables: { id: p.id }
});
const errs = r?.data?.productChangeStatus?.userErrors || [];
const topErrs = r?.errors || [];
if (errs.length || topErrs.length) { fail++; log(`fail ${p.id}: ${JSON.stringify([...errs, ...topErrs])}`); }
else ok++;
})().finally(()=>{
inFlight--; progress++;
if (progress % 25 === 0) log(`progress ${progress}/${targets.length} ok=${ok} fail=${fail}`);
if (idx < targets.length || inFlight) next();
else { log(`DONE ok=${ok} fail=${fail}`); resolve(); }
});
}
};
next();
});
})().catch(e => { log('FATAL ' + e.message); process.exit(1); });