← 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); });