← 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
A tmp-kravet-finish-collection-clean.mjs
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 →