← back to Contractwallpaper

tmp-kravet-spec-pilot.mjs

82 lines

#!/usr/bin/env node
// Kravet spec pilot — overwrite-authoritative. Writes global.<verbatim feed header> metafields
// from the fresh deswallco feed (kravet_feed_stage) for N SKUs. Verifies readback.
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';
const env = Object.fromEntries(fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8')
  .split('\n').filter(l=>l.includes('=')).map(l=>{const i=l.indexOf('='); return [l.slice(0,i).trim(), l.slice(i+1).trim()];}));
const DOMAIN=(env.SHOPIFY_STORE_DOMAIN||env.SHOPIFY_STORE).replace(/^https?:\/\//,'').replace(/\/$/,'');
const GQL=`https://${DOMAIN}/admin/api/2024-10/graphql.json`;
const H={'X-Shopify-Access-Token':env.SHOPIFY_ADMIN_TOKEN,'Content-Type':'application/json'};
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const N = Number(process.argv[2]||25);

// header -> c-index map (from staging load)
const hmap={}; // 'Header' -> 'c12'
for(const ln of fs.readFileSync('/tmp/kravet_feed_header.txt','utf8').trim().split('\n')){
  const [c,h]=ln.split('\t'); hmap[h.trim()]=c;
}
// spec/detail columns to push as global.<header> (EXCLUDES pricing + identity + volatile inventory)
const SPEC_HEADERS = ['Vert. Repeat','Horz. Repeat','Repeat UOM','Width','Width UOM','Country of Origin',
 'Content','Finish','Clean Code','Durability','Collection','Use','Type1','Type2','Style1','Style2',
 'Direction','Wallcover Length(YD)','Weight','Weight UOM','Prop 65','CA TB117','UFAC',
 'Minimum order qty','Order increment qty','Horizontal half drop repeat','Memo Sample Available',
 'Lead Time in Days','Ship From','Wallcovering Area','Color 1','Color 2','Color 3'];

async function gql(query,variables){
  const r=await fetch(GQL,{method:'POST',headers:H,body:JSON.stringify({query,variables})});
  const j=await r.json(); if(j.errors) throw new Error(JSON.stringify(j.errors).slice(0,200)); return j.data;
}

// Pick N active Kravet-fam SKUs present in the feed (mix of has/missing specs)
const rows=execFileSync('psql',['-h','/tmp','-d','dw_unified','-tA','-F','|','-c',`
  SELECT s.sku, replace(s.shopify_id,'gid://shopify/Product/',''), upper(s.mfr_sku)
  FROM shopify_products s
  WHERE s.status='ACTIVE' AND s.vendor ILIKE ANY(ARRAY['%brunschwig%','%lee jofa%','%kravet%','%clarke%','%baker%','%cole%'])
    AND EXISTS(SELECT 1 FROM kravet_feed_stage f WHERE upper(f.c1)=upper(s.mfr_sku))
  ORDER BY (s.metafields::text ILIKE '%Clean Code%'), random()
  LIMIT ${N};`]).toString().trim().split('\n').map(l=>{const [sku,id,mfr]=l.split('|');return{sku,id,mfr};});

function feedRow(mfr){
  const cols=SPEC_HEADERS.map(h=>hmap[h]).filter(Boolean);
  const sel=cols.join(',');
  const out=execFileSync('psql',['-h','/tmp','-d','dw_unified','-tA','-F','','-c',
    `SELECT ${sel} FROM kravet_feed_stage WHERE upper(c1)=upper('${mfr.replace(/'/g,"''")}') LIMIT 1;`]).toString().trim();
  if(!out) return null;
  const vals=out.split(''); const rec={};
  SPEC_HEADERS.filter(h=>hmap[h]).forEach((h,i)=>{ rec[h]=vals[i]; });
  return rec;
}

let ok=0,fail=0,totalMF=0;
for(const {sku,id,mfr} of rows){
  try{
    const rec=feedRow(mfr); if(!rec){console.log(`  skip ${sku} (no feed row)`);continue;}
    const mfs=[];
    for(const h of SPEC_HEADERS){
      let v=rec[h]; if(v==null) continue; v=String(v).trim();
      if(v===''||v.toUpperCase()==='NULL'||v==='0') continue;   // skip empty/zero
      mfs.push({ownerId:`gid://shopify/Product/${id}`,namespace:'global',key:h.trim(),type:'single_line_text_field',value:v});
    }
    if(!mfs.length){console.log(`  ${sku}: no spec values in feed`);continue;}
    // metafieldsSet max 25 per call
    for(let i=0;i<mfs.length;i+=25){
      const chunk=mfs.slice(i,i+25);
      const d=await gql(`mutation($m:[MetafieldsSetInput!]!){metafieldsSet(metafields:$m){userErrors{field message}}}`,{m:chunk});
      const errs=d.metafieldsSet.userErrors; if(errs.length) throw new Error(JSON.stringify(errs).slice(0,200));
      await sleep(300);
    }
    ok++; totalMF+=mfs.length;
    console.log(`  ${sku} [${mfr}] ${mfs.length} specs set  (e.g. ${mfs.slice(0,3).map(m=>m.key+'='+m.value).join(', ')})`);
  }catch(e){ fail++; console.error(`  FAIL ${sku}: ${e.message}`); }
  await sleep(250);
}
console.log(`\nPILOT DONE. products:${ok}/${rows.length}  total metafields written:${totalMF}  avg:${ok?(totalMF/ok).toFixed(1):0}/product  failures:${fail}`);
console.log(`Verify one: https://admin note — readback below`);
// Readback verify on first success
if(rows[0]){
  const d=await gql(`{product(id:"gid://shopify/Product/${rows[0].id}"){metafields(first:60,namespace:"global"){edges{node{key value}}}}}`);
  const got=d.product.metafields.edges.map(e=>e.node).filter(n=>SPEC_HEADERS.includes(n.key));
  console.log(`READBACK ${rows[0].sku}: ${got.length} global spec metafields ->`, got.slice(0,8).map(n=>`${n.key}=${n.value}`).join(' | '));
}