← back to Contractwallpaper
auto-save: 2026-07-28T11:00:01 (2 files) — tmp-kravet-alldata.mjs tmp-kravet-spec-pilot.mjs
59e9f3f654ba161428a11c97e4341f0504bf6fdb · 2026-07-28 11:00:07 -0700 · Steve Abrams
Files touched
A tmp-kravet-alldata.mjsA tmp-kravet-spec-pilot.mjs
Diff
commit 59e9f3f654ba161428a11c97e4341f0504bf6fdb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 11:00:07 2026 -0700
auto-save: 2026-07-28T11:00:01 (2 files) — tmp-kravet-alldata.mjs tmp-kravet-spec-pilot.mjs
---
tmp-kravet-alldata.mjs | 73 ++++++++++++++++++++++++++++++++++++++++++
tmp-kravet-spec-pilot.mjs | 81 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 154 insertions(+)
diff --git a/tmp-kravet-alldata.mjs b/tmp-kravet-alldata.mjs
new file mode 100644
index 0000000..d74fc1e
--- /dev/null
+++ b/tmp-kravet-alldata.mjs
@@ -0,0 +1,73 @@
+#!/usr/bin/env node
+// Kravet ALL-DATA metafield backfill — every feed column -> global.<header>, overwrite-authoritative.
+// Excludes nothing except pure image-src plumbing (images handled separately). Sellable variant PRICES are NOT touched.
+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 LIMIT = process.argv[2]?Number(process.argv[2]):null;
+
+// header -> c-index
+const hmap={}, headers=[];
+for(const ln of fs.readFileSync('/tmp/kravet_feed_header.txt','utf8').trim().split('\n')){
+ const [c,h]=ln.split('\t'); const hh=h.trim(); hmap[c]=hh; headers.push([c,hh]);
+}
+// ALL columns as data EXCEPT identity/plumbing dupes that add no value as metafields
+const SKIP=new Set(['Image Src','Image Position']); // (feed has no these; guard anyway)
+const cols = headers.map(([c])=>c); // c1..c55
+
+async function gql(query,variables,tries=0){
+ const r=await fetch(GQL,{method:'POST',headers:H,body:JSON.stringify({query,variables})});
+ if(r.status===429||r.status===502||r.status===503){ if(tries<6){await sleep(1500*(tries+1)); return gql(query,variables,tries+1);} }
+ const j=await r.json();
+ if(j.errors){ if(tries<6 && JSON.stringify(j.errors).includes('Throttled')){await sleep(2000*(tries+1)); return gql(query,variables,tries+1);} throw new Error(JSON.stringify(j.errors).slice(0,180)); }
+ return j.data;
+}
+
+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 EXISTS(SELECT 1 FROM kravet_feed_stage f WHERE upper(f.c1)=upper(s.mfr_sku))
+ AND s.vendor ILIKE ANY(ARRAY['%brunschwig%','%lee jofa%','%kravet%','%cole%','%baker%','%mulberry%','%clarke%','%groundworks%','%colefax%','%threads%','%andrew martin%','%aerin%','%barclay%','%thom filicia%'])
+ ORDER BY s.sku ${LIMIT?`LIMIT ${LIMIT}`:''};`]).toString().trim().split('\n').map(l=>{const[sku,id,mfr]=l.split('|');return{sku,id,mfr};});
+
+// bulk-load the feed rows we need into a JS map keyed by mfr, using a single psql dump
+const wanted=[...new Set(rows.map(r=>r.mfr))];
+const sel=cols.join(',');
+const feed={};
+// chunk the IN() to keep query size sane
+for(let i=0;i<wanted.length;i+=2000){
+ const chunk=wanted.slice(i,i+2000).map(m=>`'${m.replace(/'/g,"''")}'`).join(',');
+ const out=execFileSync('psql',['-h','/tmp','-d','dw_unified','-tA','-F','\t','-c',
+ `SELECT upper(c1), ${sel} FROM kravet_feed_stage WHERE upper(c1) IN (${chunk});`],{maxBuffer:1<<28}).toString().trim();
+ for(const line of out.split('\n')){ if(!line)continue; const parts=line.split('\t'); const key=parts[0]; feed[key]=parts.slice(1); }
+}
+console.log(`targets:${rows.length} feed rows loaded:${Object.keys(feed).length}`);
+
+let ok=0,fail=0,totalMF=0;
+for(const {sku,id,mfr} of rows){
+ try{
+ const vals=feed[mfr]; if(!vals){fail++;continue;}
+ const mfs=[];
+ cols.forEach((c,idx)=>{
+ const hdr=hmap[c]; if(SKIP.has(hdr)) return;
+ let v=vals[idx]; if(v==null) return; v=String(v).trim();
+ if(v===''||v.toUpperCase()==='NULL'||v==='0'||v==='0.0') return;
+ mfs.push({ownerId:`gid://shopify/Product/${id}`,namespace:'global',key:hdr,type:'single_line_text_field',value:v.slice(0,255)});
+ });
+ if(!mfs.length) continue;
+ for(let i=0;i<mfs.length;i+=25){
+ const d=await gql(`mutation($m:[MetafieldsSetInput!]!){metafieldsSet(metafields:$m){userErrors{message}}}`,{m:mfs.slice(i,i+25)});
+ const errs=d.metafieldsSet.userErrors; if(errs.length) throw new Error(JSON.stringify(errs).slice(0,160));
+ await sleep(120);
+ }
+ ok++; totalMF+=mfs.length;
+ if(ok%200===0) console.log(` ...${ok}/${rows.length} (${totalMF} metafields, ${fail} fail)`);
+ }catch(e){ fail++; if(fail<=20)console.error(` FAIL ${sku}: ${e.message}`); }
+ await sleep(90);
+}
+console.log(`\nALL-DATA DONE. products:${ok}/${rows.length} metafields:${totalMF} avg:${ok?(totalMF/ok).toFixed(1):0} failures:${fail}`);
diff --git a/tmp-kravet-spec-pilot.mjs b/tmp-kravet-spec-pilot.mjs
new file mode 100644
index 0000000..389f913
--- /dev/null
+++ b/tmp-kravet-spec-pilot.mjs
@@ -0,0 +1,81 @@
+#!/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(' | '));
+}
← 91d4777 auto-save: 2026-07-28T10:29:49 (1 files) — tmp-kravet-cat360
·
back to Contractwallpaper
·
auto-save: 2026-07-28T12:30:33 (1 files) — tmp-kravet-alldat 1124c80 →