← back to Designer Wallcoverings
Carl Robinson: Shopify push (draft) + activate/publish-all-channels scripts (env-only creds)
0eddd307db8dc87931f2fd29187f0f844fc996f8 · 2026-07-06 11:11:48 -0700 · steve@designerwallcoverings.com
Files touched
M scripts/wallquest-refresh/load-db-carl-robinson.cjsA scripts/wallquest-refresh/publish-channels-carl-robinson.cjsA scripts/wallquest-refresh/push-shopify-carl-robinson.cjs
Diff
commit 0eddd307db8dc87931f2fd29187f0f844fc996f8
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Mon Jul 6 11:11:48 2026 -0700
Carl Robinson: Shopify push (draft) + activate/publish-all-channels scripts (env-only creds)
---
.../wallquest-refresh/load-db-carl-robinson.cjs | 2 +-
.../publish-channels-carl-robinson.cjs | 58 ++++++++++++++++++
.../push-shopify-carl-robinson.cjs | 70 ++++++++++++++++++++++
3 files changed, 129 insertions(+), 1 deletion(-)
diff --git a/scripts/wallquest-refresh/load-db-carl-robinson.cjs b/scripts/wallquest-refresh/load-db-carl-robinson.cjs
index 92bb26da..2727be11 100644
--- a/scripts/wallquest-refresh/load-db-carl-robinson.cjs
+++ b/scripts/wallquest-refresh/load-db-carl-robinson.cjs
@@ -5,7 +5,7 @@
const fs = require('fs');
const { Client } = require('pg');
-const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
const ENR = '/tmp/wq-carl-robinson-enriched.json';
const NORM = '/tmp/wq-carl-robinson-normalized.json';
const recs = JSON.parse(fs.readFileSync(fs.existsSync(ENR) ? ENR : NORM, 'utf8'));
diff --git a/scripts/wallquest-refresh/publish-channels-carl-robinson.cjs b/scripts/wallquest-refresh/publish-channels-carl-robinson.cjs
new file mode 100644
index 00000000..31d5441a
--- /dev/null
+++ b/scripts/wallquest-refresh/publish-channels-carl-robinson.cjs
@@ -0,0 +1,58 @@
+// Activate the 67 Carl Robinson → Phillipe Romano products and publish to ALL sales channels.
+// Reads SHOPIFY_ADMIN_TOKEN from env (no probing). Idempotent. Rate-limited.
+// Pre-req already verified: all 67 pass the DW activation gate (width + description + images).
+const https = require('https');
+const { Client } = require('pg');
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function gql(query, variables) {
+ return new Promise((resolve, reject) => {
+ const body = JSON.stringify({ query, variables });
+ const req = https.request({ hostname: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 40000 },
+ res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error('bad json: ' + d.slice(0, 120))); } }); });
+ req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+ req.write(body); req.end();
+ });
+}
+
+(async () => {
+ // 1) enumerate all sales channels (publications)
+ const pubRes = await gql('{ publications(first:25){edges{node{id name}}} }');
+ if (pubRes.errors) { console.error('publications error:', JSON.stringify(pubRes.errors).slice(0, 200)); process.exit(1); }
+ const pubs = pubRes.data.publications.edges.map(e => e.node);
+ console.log('sales channels:', pubs.map(p => p.name).join(', '));
+ const pubInput = pubs.map(p => ({ publicationId: p.id }));
+
+ // 2) load the 67 product ids
+ const db = new Client({ connectionString: CONN }); await db.connect();
+ const { rows } = await db.query("SELECT dw_sku, shopify_product_id FROM dw_sku_registry WHERE dw_sku LIKE 'DWCR-%' AND shopify_product_id IS NOT NULL ORDER BY dw_sku");
+ console.log(`activating + publishing ${rows.length} products to ${pubs.length} channels…`);
+
+ let act = 0, pub = 0, err = 0;
+ for (const r of rows) {
+ const gid = `gid://shopify/Product/${r.shopify_product_id}`;
+ try {
+ // activate
+ const a = await gql('mutation($id:ID!){ productUpdate(input:{id:$id, status:ACTIVE}){ product{id status} userErrors{message} } }', { id: gid });
+ const aErr = a.errors || a.data?.productUpdate?.userErrors?.length;
+ if (!aErr) act++;
+ // publish to all channels
+ const p = await gql('mutation($id:ID!,$in:[PublicationInput!]!){ publishablePublish(id:$id, input:$in){ userErrors{message} } }', { id: gid, in: pubInput });
+ const pErr = p.errors || p.data?.publishablePublish?.userErrors?.length;
+ if (!pErr) { pub++; await db.query("UPDATE dw_sku_registry SET status='active', updated_at=now() WHERE dw_sku=$1", [r.dw_sku]); }
+ else { err++; console.log(`✗ ${r.dw_sku} publish: ${JSON.stringify(p.errors || p.data?.publishablePublish?.userErrors).slice(0,140)}`); }
+ if (!aErr && !pErr) console.log(`✓ ${r.dw_sku} active + on ${pubs.length} channels`);
+ } catch (e) { err++; console.log(`✗ ${r.dw_sku} ${String(e).slice(0, 100)}`); }
+ await sleep(600);
+ }
+ await db.query("UPDATE carl_robinson_catalog SET on_shopify=true WHERE shopify_product_id IS NOT NULL");
+ console.log(`\nDONE: activated=${act} published=${pub} errors=${err} across channels: ${pubs.map(p=>p.name).join(', ')}`);
+ await db.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
diff --git a/scripts/wallquest-refresh/push-shopify-carl-robinson.cjs b/scripts/wallquest-refresh/push-shopify-carl-robinson.cjs
new file mode 100644
index 00000000..f4a8239d
--- /dev/null
+++ b/scripts/wallquest-refresh/push-shopify-carl-robinson.cjs
@@ -0,0 +1,70 @@
+// Phase 8 — push Carl Robinson → Phillipe Romano drafts to Shopify as status:DRAFT.
+// DRAFT = invisible on storefront, reversible. Registers each SKU in dw_sku_registry,
+// records shopify_product_id back into carl_robinson_catalog. Rate-limited.
+// Env: LIMIT=1 to validate one first; DRAFTS=/path override.
+const fs = require('fs');
+const https = require('https');
+const { Client } = require('pg');
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const drafts = JSON.parse(fs.readFileSync(process.env.DRAFTS || '/tmp/carl-robinson-PR-drafts.json', 'utf8'));
+const LIMIT = process.env.LIMIT ? parseInt(process.env.LIMIT, 10) : drafts.length;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function shopify(method, path, body) {
+ return new Promise((resolve, reject) => {
+ const data = body ? JSON.stringify(body) : null;
+ const req = https.request({ hostname: STORE, path: `/admin/api/${API}${path}`, method,
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) }, timeout: 40000 },
+ res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { const j = d ? JSON.parse(d) : {}; resolve({ status: res.statusCode, j }); } catch (e) { resolve({ status: res.statusCode, raw: d }); } }); });
+ req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+ if (data) req.write(data); req.end();
+ });
+}
+
+function buildPayload(d) {
+ return { product: {
+ title: d.title, body_html: d.body_html, vendor: d.vendor, product_type: d.product_type,
+ status: 'draft', // <-- reversible, not customer-visible
+ tags: d.tags.join(', '),
+ options: [{ name: 'Size' }],
+ variants: [
+ { option1: 'Sample', sku: `${d.dw_sku}-Sample`, price: '4.25', inventory_management: null, inventory_policy: 'deny', taxable: true },
+ { option1: 'Standard', sku: d.dw_sku, price: d.variants[1].price || '0.00', inventory_management: 'shopify', inventory_policy: 'continue', inventory_quantity: 2026, taxable: true },
+ ],
+ images: (d.images || []).slice(0, 20).map(src => ({ src })),
+ metafields: d.metafields.filter(m => m.value !== '' && m.value != null),
+ }};
+}
+
+(async () => {
+ const db = new Client({ connectionString: CONN }); await db.connect();
+ let ok = 0, fail = 0, skip = 0;
+ const work = drafts.slice(0, LIMIT);
+ for (const d of work) {
+ // dedup: skip if already registered/pushed
+ const reg = await db.query('SELECT shopify_product_id FROM dw_sku_registry WHERE dw_sku=$1', [d.dw_sku]);
+ if (reg.rows[0]?.shopify_product_id) { console.log(`skip ${d.dw_sku} (already on Shopify ${reg.rows[0].shopify_product_id})`); skip++; continue; }
+ try {
+ const { status, j, raw } = await shopify('POST', '/products.json', buildPayload(d));
+ if (status >= 200 && status < 300 && j.product) {
+ const pid = j.product.id, handle = j.product.handle;
+ await db.query(`INSERT INTO dw_sku_registry (dw_sku,vendor_prefix,vendor_name,mfr_sku,shopify_product_id,shopify_handle,status)
+ VALUES ($1,'DWCR-','Phillipe Romano',$2,$3,$4,'draft')
+ ON CONFLICT (dw_sku) DO UPDATE SET shopify_product_id=EXCLUDED.shopify_product_id, shopify_handle=EXCLUDED.shopify_handle, updated_at=now()`,
+ [d.dw_sku, d._mfr, String(pid), handle]);
+ await db.query('UPDATE carl_robinson_catalog SET shopify_product_id=$1, on_shopify=true, updated_at=now() WHERE mfr_sku=$2', [String(pid), d._mfr]);
+ ok++; console.log(`✓ ${d.dw_sku} → ${pid} (draft) "${d.title}" [${ok}/${work.length}]`);
+ } else {
+ fail++; console.log(`✗ ${d.dw_sku} HTTP ${status}: ${JSON.stringify(j?.errors || raw).slice(0, 160)}`);
+ }
+ } catch (e) { fail++; console.log(`✗ ${d.dw_sku} ${String(e).slice(0, 100)}`); }
+ await sleep(650); // standard plan 2 req/s
+ }
+ console.log(`\nPUSH DONE: created=${ok} failed=${fail} skipped=${skip} (status=draft on ${STORE})`);
+ await db.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
← 60fb5850 Carl Robinson E20 → Phillipe Romano onboard pipeline (scrape
·
back to Designer Wallcoverings
·
Onboard Daisy Bennett Natural Wallcoverings (WallQuest→Phill 532c47be →