← back to Greenland Onboard
scripts/update-tags.mjs
52 lines
#!/usr/bin/env node
// Stamp each LIVE product with the full attribute set Steve asked for:
// tags += Style:<style>, Color:<color>, Hue:<hue>, Hex:<#hex> (filterable)
// metafields custom.{hex,hue,style,color} (structured, theme-readable)
// Idempotent: rebuilds the tag list from scratch each run. Reads shopify_product_id
// from greenland_full_catalog, so run AFTER the publish completes.
import { readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
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));
async function put(path, body, tries = 5) {
for (let i = 0; i < tries; i++) {
const r = await fetch(`${API}${path}`, { method: 'PUT', headers: H, body: JSON.stringify(body) });
if (r.status === 429) { await sleep(2000 * (i + 1)); continue; }
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`.slice(0, 200));
return r.json();
}
throw new Error('429 exhausted');
}
// source EVERYTHING from the DB (authoritative — enriched.json can drift)
const rows = JSON.parse(execSync(
`psql "${DB}" -tAc "select coalesce(json_agg(json_build_object('pid',shopify_product_id,'sku',dw_sku,'material',material,'city',city,'color',clean_color,'hex',hex,'hue',hue,'style',style))::text,'[]') from greenland_full_catalog where shopify_product_id is not null"`,
{ encoding: 'utf8', maxBuffer: 128e6 }).trim());
console.log(`updating ${rows.length} live products with Style/Color/Hue/Hex`);
let ok = 0, fail = 0;
for (const r of rows) {
const pid = r.pid;
const tags = [
r.material, r.city, 'Phillipe Romano', 'quotes', 'Coastal Naturals',
`Style: ${r.style}`, `Color: ${r.color}`, `Hue: ${r.hue}`, `Hex: ${r.hex}`,
].filter(Boolean).join(', ');
try {
// tags only — Hex/Style/Color/Hue live in the tag set; skip custom.* metafields
// (a pre-existing store def types one of them product_reference → 422s the whole PUT)
await put(`/products/${pid}.json`, { product: { id: pid, tags } });
ok++; if (ok % 50 === 0) console.log(` ...updated ${ok}`);
await sleep(520);
} catch (e) { fail++; console.log(` FAIL ${r.dwSku}: ${String(e.message).slice(0, 120)}`); await sleep(700); }
}
console.log(`\nDONE updated=${ok} failed=${fail}`);