← back to Tk 11331 Exec

d2_metafields.mjs

57 lines

// DECISION 2 — set global.v_prods_quantity_order_min="5" and global.unit_of_measure="Sold Per Yard"
// on non-compliant products. PG mirror first (rule 5), then Shopify metafieldsSet, then verify re-read.
// Records per-product old values. --apply to write. --limit=N to cap the batch.
import fs from 'fs';
import {execSync} from 'child_process';
import {gql,sleep} from './lib.mjs';
const APPLY=process.argv.includes('--apply');
const LIMIT=parseInt((process.argv.find(a=>a.startsWith('--limit='))||'').split('=')[1]||'99999');
const OFFSET=parseInt((process.argv.find(a=>a.startsWith('--offset='))||'').split('=')[1]||'0');
const all=JSON.parse(fs.readFileSync('data/d2_set.json','utf8'));
const batch=all.slice(OFFSET,OFFSET+LIMIT);
console.log(`slice offset=${OFFSET} limit=${LIMIT} -> ${batch.length} rows`);
const RESTORE='data/d2-restore-map.jsonl';
const rstream=fs.createWriteStream(RESTORE,{flags:'a'});
const SET=`mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){
  metafields{namespace key value} userErrors{field message}}}`;
const VQ=`query($id:ID!){product(id:$id){status
  mfmin:metafield(namespace:"global",key:"v_prods_quantity_order_min"){value}
  mfuom:metafield(namespace:"global",key:"unit_of_measure"){value}}}`;
const psql=sql=>execSync(`psql -h /tmp -d dw_unified -Atc ${JSON.stringify(sql)}`,{encoding:'utf8'}).trim();

let ok=0,fail=0; const fails=[];
console.log(`${APPLY?'APPLY':'DRY-RUN'} D2 — ${batch.length} products (of ${all.length} total non-compliant)`);
for(const p of batch){
  const mf=[];
  if(p.need_min) mf.push({ownerId:p.gid,namespace:'global',key:'v_prods_quantity_order_min',type:'single_line_text_field',value:'5'});
  if(p.need_uom) mf.push({ownerId:p.gid,namespace:'global',key:'unit_of_measure',type:'single_line_text_field',value:'Sold Per Yard'});
  if(!mf.length) continue;
  if(!APPLY){ console.log(`  would set ${p.pid} min:${p.need_min?'5':'-'} uom:${p.need_uom?'SPY':'-'}`); ok++; continue; }
  rstream.write(JSON.stringify({ts:new Date().toISOString(),pid:p.pid,gid:p.gid,
    old:{min:p.min_val,uom:p.uom_val},wrote:{min:p.need_min?'5':null,uom:p.need_uom?'Sold Per Yard':null},
    undo:`metafieldsSet restore global.v_prods_quantity_order_min=${JSON.stringify(p.min_val)} global.unit_of_measure=${JSON.stringify(p.uom_val)} (delete uom if old was null)`})+'\n');
  // rule 5: PG mirror first
  const sets=[];
  if(p.need_min) sets.push(`'{global,v_prods_quantity_order_min}','{"type":"single_line_text_field","value":"5"}'::jsonb,true`);
  let expr='coalesce(metafields,\'{}\'::jsonb)';
  if(p.need_min) expr=`jsonb_set(${expr},'{global,v_prods_quantity_order_min}','{"type":"single_line_text_field","value":"5"}'::jsonb,true)`;
  if(p.need_uom) expr=`jsonb_set(${expr},'{global,unit_of_measure}','{"type":"single_line_text_field","value":"Sold Per Yard"}'::jsonb,true)`;
  try{ psql(`update shopify_products set metafields=${expr} where shopify_id='${p.gid}';`); }
  catch(e){ fail++; fails.push({pid:p.pid,stage:'pg',err:String(e).slice(0,200)}); console.log(`  PG-ERR ${p.pid}`); continue; }
  // Shopify authoritative write
  const res=await gql(SET,{mf});
  const ue=res.metafieldsSet.userErrors;
  if(ue.length){ fail++; fails.push({pid:p.pid,stage:'shopify',ue}); console.log(`  SHOP-ERR ${p.pid}`,JSON.stringify(ue)); continue; }
  await sleep(350);
  // verify via re-read
  const v=(await gql(VQ,{id:p.gid})).product;
  const okmin=!p.need_min||v.mfmin?.value==='5';
  const okuom=!p.need_uom||v.mfuom?.value==='Sold Per Yard';
  if(okmin&&okuom){ ok++; if(ok%50===0)console.log(`  ok ${ok}`);}
  else { fail++; fails.push({pid:p.pid,stage:'verify',got:{min:v.mfmin?.value,uom:v.mfuom?.value}}); console.log(`  VERIFY-FAIL ${p.pid}`);}
  await sleep(120);
}
rstream.end();
console.log(`D2 ${APPLY?'APPLIED':'DRY'} — ok=${ok} fail=${fail}`);
if(fails.length) fs.writeFileSync('data/d2-fails.json',JSON.stringify(fails,null,1));