← 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
A scripts/publish-samples.mjs
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 →