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