← back to Greenland Onboard
build 38 Phillipe Romano smart collections (material/style/hue, vendor-scoped)
359823686aaf554207f648d7607d288c70325b64 · 2026-07-12 22:49:03 -0700 · Steve Abrams
Files touched
A scripts/build-collections.mjs
Diff
commit 359823686aaf554207f648d7607d288c70325b64
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 12 22:49:03 2026 -0700
build 38 Phillipe Romano smart collections (material/style/hue, vendor-scoped)
---
scripts/build-collections.mjs | 67 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
diff --git a/scripts/build-collections.mjs b/scripts/build-collections.mjs
new file mode 100644
index 0000000..a6dfcca
--- /dev/null
+++ b/scripts/build-collections.mjs
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+// Build SMART collections for the Phillipe Romano line, scoped to vendor="Phillipe Romano"
+// + one of our tags (material / Style: / Hue:). Smart = auto-populates from tags, stays current.
+// Idempotent: skips a collection whose handle already exists.
+import { readFileSync } from 'node:fs';
+import { execFileSync } from 'node:child_process';
+const env = readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`, 'utf8');
+const pick = (k) => (env.match(new RegExp(`^${k}=(.*)$`, 'm'))?.[1] || '').replace(/^['"]|['"]$/g, '').trim();
+const STORE = pick('SHOPIFY_STORE') || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = pick('SHOPIFY_ADMIN_TOKEN');
+const API = `https://${STORE}/admin/api/2024-10`;
+const DB = 'postgresql:///dw_unified?host=/tmp&user=stevestudio2';
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const PSQL = '/opt/homebrew/opt/postgresql@14/bin/psql';
+const q = sql => JSON.parse(execFileSync(PSQL, [DB, '-tAc', sql], { encoding: 'utf8', maxBuffer: 64e6 }).trim() || '[]');
+
+// distinct attribute values (materials always; styles; hues with >=5)
+const materials = q(`select coalesce(json_agg(distinct material),'[]') from greenland_full_catalog`);
+const styles = q(`select coalesce(json_agg(distinct style),'[]') from greenland_full_catalog where style is not null`);
+const hues = q(`select coalesce(json_agg(hue),'[]') from (select hue from greenland_full_catalog group by hue having count(*)>=5) t`);
+
+const MATWORD = { Silk:'Silk', Grass:'Grasscloth', Wood:'Wood Veneer', Suede:'Suede', Linen:'Linen', Raffia:'Raffia',
+ 'Paper Weave':'Paper Weave', Abaca:'Abaca', Hemp:'Hemp', Sisal:'Sisal', Jute:'Jute', Velvet:'Velvet', Wool:'Wool',
+ Arrowroot:'Arrowroot', 'Cotton Yarn':'Cotton', 'Water hyacinth':'Water Hyacinth', 'Metal Leaf':'Metal Leaf',
+ 'PE Weaving / PE Woven Material':'Woven', Specialty:'Signature', 'Multi-material':'Mixed Media' };
+const slug = s => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
+
+// build definitions: {title, handle, tag}
+const defs = [];
+for (const m of materials) defs.push({ title: `Phillipe Romano ${MATWORD[m] || m}`, handle: `pr-${slug(MATWORD[m] || m)}`, tag: m });
+for (const s of styles) defs.push({ title: `Phillipe Romano ${s}`, handle: `pr-style-${slug(s)}`, tag: `Style: ${s}` });
+for (const h of hues) defs.push({ title: `Phillipe Romano ${h}`, handle: `pr-hue-${slug(h)}`, tag: `Hue: ${h}` });
+
+async function existingHandles() {
+ const out = new Set();
+ for (const kind of ['smart_collections', 'custom_collections']) {
+ let url = `${API}/${kind}.json?limit=250&fields=handle`;
+ for (let i = 0; i < 30 && url; i++) {
+ const r = await fetch(url, { headers: H });
+ const j = await r.json();
+ (j[kind] || []).forEach(c => out.add(c.handle));
+ const link = r.headers.get('link') || '';
+ const m = link.match(/<([^>]+)>;\s*rel="next"/);
+ url = m ? m[1] : null;
+ await sleep(300);
+ }
+ }
+ return out;
+}
+
+const have = await existingHandles();
+console.log(`existing collection handles: ${have.size}; building ${defs.length} PR collections (${materials.length} material · ${styles.length} style · ${hues.length} hue)`);
+let created = 0, skipped = 0, failed = 0;
+for (const d of defs) {
+ if (have.has(d.handle)) { skipped++; continue; }
+ const body = { smart_collection: { title: d.title, handle: d.handle, published: true, disjunctive: false,
+ rules: [ { column: 'vendor', relation: 'equals', condition: 'Phillipe Romano' },
+ { column: 'tag', relation: 'equals', condition: d.tag } ] } };
+ try {
+ const r = await fetch(`${API}/smart_collections.json`, { method: 'POST', headers: H, body: JSON.stringify(body) });
+ if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0,150)}`);
+ created++; console.log(` + ${d.handle} (tag="${d.tag}")`);
+ await sleep(600);
+ } catch (e) { failed++; console.log(` FAIL ${d.handle}: ${String(e.message).slice(0,120)}`); await sleep(800); }
+}
+console.log(`\nDONE created=${created} skipped(existing)=${skipped} failed=${failed}`);
← fb2d951 Finish cork SKU standardization: convert 3 stray DWGL- stray
·
back to Greenland Onboard
·
auto-save: 2026-07-12T23:21:12 (1 files) — scripts/update-ta 6aa4faf →