← back to Dwjs Consolidation 2026 04 23

qty_backfill.js

62 lines

#!/usr/bin/env node
// Backfill qty metafields on any DWJS product/variant missing them.
// product-level: global.v_prods_quantity_order_min=2, global.v_prods_quantity_order_units=2
// variant-level: custom.v_prod_quantity_order_min = 2 (bolt) or 1 (sample)
//                custom.v_prods_quantity_order_units = 2 (bolt) or 1 (sample)
const https = require('https');
const fs = require('fs');

const STORE='designer-laboratory-sandbox.myshopify.com';
const TOKEN=(process.env.SHOPIFY_ADMIN_TOKEN || '');
const LOG = '/Users/macstudio3/Projects/dwjs-consolidation-2026-04-23/qty_backfill.log';
const log = m => { const l=`[${new Date().toISOString().slice(0,19)}] ${m}\n`; process.stdout.write(l); fs.appendFileSync(LOG,l); };

function gql(b, retry=0){return new Promise((res,rej)=>{const d=JSON.stringify(b);const r=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(d)}},R=>{let c='';R.on('data',x=>c+=x);R.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<6){await new Promise(r=>setTimeout(r,2000*(retry+1)));return res(gql(b,retry+1))}if(tri&&tri.currentlyAvailable<300)await new Promise(r=>setTimeout(r,800));res(j)}catch{if(retry<3)setTimeout(()=>res(gql(b,retry+1)),2000);else res({error:c.slice(0,500)})}})});r.on('error',err=>{if(retry<3)setTimeout(()=>res(gql(b,retry+1)),2000);else rej(err)});r.setTimeout(45000,()=>{r.destroy();if(retry<3)res(gql(b,retry+1));else rej(new Error('t'))});r.write(d);r.end()});}

// Load the audit jsonl
const products = new Map();
for (const l of fs.readFileSync('/tmp/qty_audit_DWJSall.jsonl','utf8').split('\n').filter(Boolean)) {
  const o = JSON.parse(l);
  if (o.id?.includes('/Product/')) products.set(o.id, {
    id: o.id,
    m1: o.m1?.value || null,
    m2: o.m2?.value || null,
    variants: [],
  });
  else if (o.id?.includes('/ProductVariant/') && o.__parentId) {
    const p = products.get(o.__parentId);
    if (p) p.variants.push({ id: o.id, sku: o.sku||'', title: o.title||'', v1: o.v1?.value||null, v2: o.v2?.value||null });
  }
}

// Build list of metafields to write
const needed = [];
for (const p of products.values()) {
  if (p.m1 !== '2') needed.push({ ownerId: p.id, namespace: 'global', key: 'v_prods_quantity_order_min', type:'single_line_text_field', value:'2' });
  if (p.m2 !== '2') needed.push({ ownerId: p.id, namespace: 'global', key: 'v_prods_quantity_order_units', type:'single_line_text_field', value:'2' });
  for (const v of p.variants) {
    const isSample = /sample/i.test(v.sku + v.title);
    const want = isSample ? '1' : '2';
    if (v.v1 !== want) needed.push({ ownerId: v.id, namespace: 'custom', key: 'v_prod_quantity_order_min', type:'single_line_text_field', value: want });
    if (v.v2 !== want) needed.push({ ownerId: v.id, namespace: 'custom', key: 'v_prods_quantity_order_units', type:'single_line_text_field', value: want });
  }
}
log(`metafields to write: ${needed.length}`);

(async () => {
  let ok=0, fail=0, batchIdx=0;
  for (let i=0; i<needed.length; i+=25) {
    const batch = needed.slice(i, i+25);
    batchIdx++;
    const r = await gql({
      query: `mutation($mfs: [MetafieldsSetInput!]!) { metafieldsSet(metafields:$mfs) { metafields { id } userErrors { field message } } }`,
      variables: { mfs: batch }
    });
    const errs = r?.data?.metafieldsSet?.userErrors || [];
    if (errs.length) { fail += errs.length; log(`batch ${batchIdx} errs: ${JSON.stringify(errs).slice(0,200)}`); }
    ok += (r?.data?.metafieldsSet?.metafields?.length || 0);
    if (batchIdx % 20 === 0) log(`batch ${batchIdx}  ok=${ok}  fail=${fail}`);
  }
  log(`DONE — ok=${ok} fail=${fail} batches=${batchIdx}`);
})().catch(e => { log('FATAL ' + e.message); process.exit(1); });