[object Object]

← 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

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 →