[object Object]

← back to Contractwallpaper

auto-save: 2026-07-30T10:16:54 (1 files) — tmp-kravet-finish-collection-clean.mjs

43a400ec7f8ce728c20229e965120bbf4fc4cbe1 · 2026-07-30 10:17:00 -0700 · Steve Abrams

Files touched

Diff

commit 43a400ec7f8ce728c20229e965120bbf4fc4cbe1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 30 10:17:00 2026 -0700

    auto-save: 2026-07-30T10:16:54 (1 files) — tmp-kravet-finish-collection-clean.mjs
---
 tmp-kravet-finish-collection-clean.mjs | 87 ++++++++++++++++++++++++++++++++++
 1 file changed, 87 insertions(+)

diff --git a/tmp-kravet-finish-collection-clean.mjs b/tmp-kravet-finish-collection-clean.mjs
new file mode 100644
index 0000000..fa910d9
--- /dev/null
+++ b/tmp-kravet-finish-collection-clean.mjs
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+// Kravet metafield cleanup — normalize global.Finish (strip trailing ';' artifacts, split internal ';' -> ', ', Title Case)
+// and Title-Case global.Collection (preserving Roman numerals). Overwrite-authoritative, ledger-resumable.
+// Touches ONLY global.Finish + global.Collection on active Kravet-family products. Prices/other fields untouched.
+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 DRY = process.argv.includes('--dry');
+const LIMIT = (()=>{const i=process.argv.indexOf('--limit'); return i>=0?Number(process.argv[i+1]):null;})();
+
+// --- transforms ---
+const SMALL=new Set(["and","or","the","of","in","a","an","for","to","with","by"]);
+const ROMAN=/^(?:I{1,3}|IV|VI{0,3}|IX|XI{0,3})$/;      // I..XIII, the realistic collection numerals
+const capWord=p=>!p?p:(/^\d/.test(p)?p:p[0].toUpperCase()+p.slice(1).toLowerCase());
+function titleCase(s){
+  return String(s).split(/\s+/).map((w,i)=>{
+    if(!w) return w;
+    const low=w.toLowerCase();
+    if(i>0 && SMALL.has(low)) return low;
+    if(ROMAN.test(w)) return w.toUpperCase();          // standalone Roman numeral (Garden II)
+    if(w.includes('&')){                                // &-token: preserve short all-caps initialism parts
+      return w.split(/([&\-])/).map(part =>
+        (part==='&'||part==='-') ? part
+        : /^[A-Z0-9]{1,3}$/.test(part) ? part           // B, GP, J
+        : capWord(part)).join('');
+    }
+    return w.split('-').map(capWord).join('-');          // normal token, hyphen-aware
+  }).join(" ").replace(/\s+/g," ").trim();
+}
+const cleanFinish=v=>String(v).split(";").map(t=>t.trim()).filter(Boolean).map(titleCase).join(", ");
+
+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;
+}
+
+// same target set as the backfill
+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};});
+
+const MF_LEDGER='/Users/macstudio3/kravet-feed-pull/finish-collection-done.txt';
+const done=new Set(fs.existsSync(MF_LEDGER)?fs.readFileSync(MF_LEDGER,'utf8').trim().split('\n').filter(Boolean):[]);
+const rows2=rows.filter(r=>!done.has(r.sku));
+
+// bulk-load feed c14 (Finish) + c17 (Collection) keyed by mfr
+const wanted=[...new Set(rows2.map(r=>r.mfr))];
+const feed={};
+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), c14, c17 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 [k,c14,c17]=line.split('\t'); feed[k]=[c14,c17]; }
+}
+console.log(`targets:${rows2.length} (of ${rows.length}, ${done.size} done)  feed:${Object.keys(feed).length}  ${DRY?'[DRY RUN]':''}`);
+
+let ok=0,fail=0,fSet=0,cSet=0,shown=0;
+for(const {sku,id,mfr} of rows2){
+  try{
+    const vals=feed[mfr]; if(!vals){fail++; if(!DRY)fs.appendFileSync(MF_LEDGER,sku+'\n'); continue;}
+    const [rawFin,rawColl]=vals;
+    const mfs=[];
+    let fClean,cClean;
+    if(rawFin!=null && String(rawFin).trim() && String(rawFin).toUpperCase()!=='NULL'){ fClean=cleanFinish(rawFin);
+      if(fClean) mfs.push({ownerId:`gid://shopify/Product/${id}`,namespace:'global',key:'Finish',type:'single_line_text_field',value:fClean.slice(0,255)}); }
+    if(rawColl!=null && String(rawColl).trim() && String(rawColl).toUpperCase()!=='NULL'){ cClean=titleCase(rawColl);
+      if(cClean) mfs.push({ownerId:`gid://shopify/Product/${id}`,namespace:'global',key:'Collection',type:'single_line_text_field',value:cClean.slice(0,255)}); }
+    if(DRY){ if(shown<12 && (fClean||cClean)){ console.log(`  ${sku}: Finish "${rawFin}"->"${fClean}"  Collection "${rawColl}"->"${cClean}"`); shown++; } if(fClean)fSet++; if(cClean)cSet++; ok++; continue; }
+    if(mfs.length){ const d=await gql(`mutation($m:[MetafieldsSetInput!]!){metafieldsSet(metafields:$m){userErrors{message}}}`,{m:mfs});
+      const errs=d.metafieldsSet.userErrors; if(errs.length) throw new Error(JSON.stringify(errs).slice(0,160)); }
+    if(fClean)fSet++; if(cClean)cSet++; ok++; fs.appendFileSync(MF_LEDGER,sku+'\n');
+    if(ok%300===0) console.log(`  ...${ok}/${rows2.length}  (Finish:${fSet} Collection:${cSet} fail:${fail})`);
+    await sleep(90);
+  }catch(e){ fail++; if(fail<=20)console.error(`  FAIL ${sku}: ${e.message}`); }
+}
+console.log(`\n${DRY?'DRY':'FINISH/COLLECTION'} DONE. products:${ok}  Finish set:${fSet}  Collection set:${cSet}  failures:${fail}`);

← ffef1db search: scroll results into view on mobile (grid was below t  ·  back to Contractwallpaper  ·  Catalog-wide Finish/Collection cosmetic cleanup: Title-Case 3fae184 →