[object Object]

← back to Greenland Onboard

auto-save: 2026-07-12T10:47:45 (1 files) — scripts/publish-samples.mjs

726327d5d4fc6dfa8d0a3de451298147073be1a8 · 2026-07-12 10:47:52 -0700 · Steve Abrams

Files touched

Diff

commit 726327d5d4fc6dfa8d0a3de451298147073be1a8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 12 10:47:52 2026 -0700

    auto-save: 2026-07-12T10:47:45 (1 files) — scripts/publish-samples.mjs
---
 scripts/publish-samples.mjs | 75 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 75 insertions(+)

diff --git a/scripts/publish-samples.mjs b/scripts/publish-samples.mjs
new file mode 100644
index 0000000..cc1385d
--- /dev/null
+++ b/scripts/publish-samples.mjs
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+// LIVE publish: each enriched SKU → ACTIVE Shopify product, single "Sample" variant @ $4.25,
+// quote-only ("Call or Email for Quote" via the `quotes` tag), local image base64-attached.
+// Per-SKU dup guard (GraphQL sku: query) so a re-run never double-creates. Updates
+// greenland_full_catalog with shopify_product_id + status. Throttled + 429-retry.
+//   node scripts/publish-samples.mjs <limit> [startIndex]   (limit=0 → all)
+import { readFileSync, appendFileSync, existsSync } 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 LIMIT = Number(process.argv[2] || 0);
+const START = Number(process.argv[3] || 0);
+const LEDGER = join(ROOT, 'data', 'published.ndjson');
+
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+async function sh(path, opts = {}, tries = 5) {
+  for (let i = 0; i < tries; i++) {
+    const r = await fetch(`${API}${path}`, { headers: H, ...opts });
+    if (r.status === 429) { await sleep(2000 * (i + 1)); continue; }
+    const t = await r.text();
+    if (!r.ok) throw new Error(`${r.status} ${path} :: ${t.slice(0, 300)}`);
+    return t ? JSON.parse(t) : {};
+  }
+  throw new Error(`429 exhausted ${path}`);
+}
+async function gql(query) {
+  const r = await fetch(`${API}/graphql.json`, { method: 'POST', headers: H, body: JSON.stringify({ query }) });
+  return (await r.json()).data;
+}
+async function skuExists(sku) {
+  const d = await gql(`{ productVariants(first:1, query:"sku:${sku}"){ edges{ node{ id product{ id legacyResourceId } } } } }`);
+  const e = d?.productVariants?.edges?.[0];
+  return e ? e.node.product.legacyResourceId : null;
+}
+const b64 = (fp) => existsSync(fp) ? execSync(`base64 -i "${fp}"`, { maxBuffer: 64e6 }).toString().replace(/\n/g, '') : null;
+
+const rows = JSON.parse(readFileSync(join(ROOT, 'data', 'enriched.json'), 'utf8'));
+const slice = rows.slice(START, LIMIT ? START + LIMIT : undefined);
+console.log(`store=${STORE}  publishing ${slice.length} of ${rows.length} (start=${START})`);
+
+let created = 0, skipped = 0, failed = 0;
+for (const r of slice) {
+  try {
+    const existing = await skuExists(r.dwSku);
+    if (existing) { skipped++; console.log(`  skip ${r.dwSku} (exists ${existing})`);
+      execSync(`psql "${DB}" -tAc "update greenland_full_catalog set shopify_product_id=${existing}, status='ACTIVE' where \\"dwSku\\"='${r.dwSku}'"`); continue; }
+    const img = b64(join(ROOT, 'public', 'img', `${r.dwSku}.jpg`));
+    const tags = [...new Set([...(r.tags || []), 'quotes'])].join(', ');
+    const payload = { product: {
+      title: r.title, body_html: r.body_html, vendor: 'Phillipe Romano', product_type: 'Wallcovering',
+      status: 'active', tags,
+      options: [{ name: 'Size' }],
+      variants: [{ option1: 'Sample', price: '4.25', sku: r.dwSku, taxable: true, requires_shipping: true, inventory_policy: 'continue', fulfillment_service: 'manual', inventory_management: null }],
+      metafields: [{ namespace: 'custom', key: 'mfr_number', type: 'single_line_text_field', value: String(r.mfrModel || '') }],
+      images: img ? [{ attachment: img, alt: r.alt }] : [],
+    } };
+    const res = await sh(`/products.json`, { method: 'POST', body: JSON.stringify(payload) });
+    const pid = res.product.id;
+    execSync(`psql "${DB}" -tAc "update greenland_full_catalog set shopify_product_id=${pid}, status='ACTIVE' where \\"dwSku\\"='${r.dwSku}'"`);
+    appendFileSync(LEDGER, JSON.stringify({ dwSku: r.dwSku, pid, title: r.title }) + '\n');
+    created++;
+    if (created % 20 === 0) console.log(`  ...created ${created}`);
+    await sleep(550);
+  } catch (e) { failed++; console.log(`  FAIL ${r.dwSku}: ${String(e.message).slice(0, 160)}`); await sleep(800); }
+}
+console.log(`\nDONE created=${created} skipped=${skipped} failed=${failed}`);

← f451923 scrub Greenland leak from microsite (title/h1/fallback → Phi  ·  back to Greenland Onboard  ·  live publisher: sample-$4.25 + quotes-tag, per-SKU dup-guard 6cd5fcc →