← back to Greenland Onboard

scripts/build-collections.mjs

68 lines

#!/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}`);