[object Object]

← back to Dw Yolo Loop

Artmura push: fix 422 (drop metafields from create — store defs collide), add req timeout + metafield backfill

55fc39776f7734f0341a7112087fca085942ab5b · 2026-06-12 09:44:34 -0700 · Steve Abrams

- create payload no longer sends metafields: custom.material=multi_line_text_field, custom.collection/lead_time=product_reference → REST create 422'd on type mismatch (atomic, 0 created)
- specs remain in body_html; backfill_metafields.js writes them post-create with correct per-key types (skips the 2 product_reference keys)
- 20s request timeout to prevent hangs
- Steve-authorized live draft push in progress

Files touched

Diff

commit 55fc39776f7734f0341a7112087fca085942ab5b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jun 12 09:44:34 2026 -0700

    Artmura push: fix 422 (drop metafields from create — store defs collide), add req timeout + metafield backfill
    
    - create payload no longer sends metafields: custom.material=multi_line_text_field, custom.collection/lead_time=product_reference → REST create 422'd on type mismatch (atomic, 0 created)
    - specs remain in body_html; backfill_metafields.js writes them post-create with correct per-key types (skips the 2 product_reference keys)
    - 20s request timeout to prevent hangs
    - Steve-authorized live draft push in progress
---
 scripts/artmura-onboard/backfill_metafields.js | 80 ++++++++++++++++++++++++++
 scripts/artmura-onboard/push-artmura-live.js   | 17 +++---
 2 files changed, 87 insertions(+), 10 deletions(-)

diff --git a/scripts/artmura-onboard/backfill_metafields.js b/scripts/artmura-onboard/backfill_metafields.js
new file mode 100644
index 0000000..a37a01d
--- /dev/null
+++ b/scripts/artmura-onboard/backfill_metafields.js
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+/**
+ * Backfill Artmura product metafields with the CORRECT per-key types (the create 422'd
+ * because custom.material=multi_line_text_field and custom.collection/lead_time=product_reference).
+ * Runs AFTER push-artmura-live.js (reads shopify_product_id from newwall_catalog).
+ *
+ *   node backfill_metafields.js            # dry-run (first 3)
+ *   node backfill_metafields.js --apply
+ */
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+
+const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_PRODUCT_TOKEN;
+const API = process.env.SHOPIFY_API_VERSION || '2024-10';
+const DB = process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp';
+const APPLY = process.argv.includes('--apply');
+const PKG = path.join(__dirname, 'data', 'artmura.json');
+if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const pool = new Pool({ connectionString: DB });
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function shopify(method, p, body) {
+  return new Promise((resolve, reject) => {
+    const data = body ? JSON.stringify(body) : null;
+    const req = https.request({ hostname: STORE, path: `/admin/api/${API}${p}`, method,
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json',
+                 ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) } },
+      res => { let b = ''; res.on('data', c => b += c); res.on('end', () => { let j; try { j = JSON.parse(b); } catch { j = b; }
+        resolve({ status: res.statusCode, body: j, callLimit: res.headers['x-shopify-shop-api-call-limit'] }); }); });
+    req.on('error', reject); req.setTimeout(20000, () => req.destroy(new Error('timeout')));
+    if (data) req.write(data); req.end();
+  });
+}
+
+// key -> type, matching the store's existing definitions. SKIP collection + lead_time
+// (defined as product_reference — cannot hold our text).
+const FIELDS = [
+  ['design_name', 'single_line_text_field', r => r.pattern_series],
+  ['colorway_name', 'single_line_text_field', r => r.color],
+  ['material', 'multi_line_text_field', r => r.substrate || 'Non-Woven'],
+  ['width', 'single_line_text_field', r => r.dimensions],
+  ['wall_coverage', 'single_line_text_field', r => r.wall_coverage],
+  ['origin', 'single_line_text_field', r => r.origin],
+  ['grade', 'single_line_text_field', r => r.grade],
+  ['mfr_sku', 'single_line_text_field', r => r.mfr_sku],
+  ['source_url', 'single_line_text_field', r => r.product_url],
+];
+
+(async () => {
+  const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8')).products;
+  const { rows } = await pool.query(
+    `SELECT handle, shopify_product_id FROM newwall_catalog WHERE vendor_name='Artmura' AND shopify_product_id IS NOT NULL AND shopify_product_id<>''`);
+  const pid = Object.fromEntries(rows.map(r => [r.handle, r.shopify_product_id]));
+  const todo = pkg.filter(p => pid[p.handle]);
+  console.log(`metafield backfill: ${todo.length} products · mode: ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
+
+  if (!APPLY) {
+    const r = todo[0]; const mfs = FIELDS.map(([k, t, f]) => ({ key: k, type: t, value: f(r) })).filter(m => m.value);
+    console.log(`example ${pid[r.handle]}:`); mfs.forEach(m => console.log(`  custom.${m.key} (${m.type}) = ${m.value}`));
+    console.log(`\nDRY-RUN. --apply to write.`); await pool.end(); return;
+  }
+
+  let ok = 0, fail = 0;
+  for (const r of todo) {
+    const id = pid[r.handle];
+    const metafields = FIELDS.map(([k, t, f]) => { const v = f(r); return v ? { namespace: 'custom', key: k, type: t, value: String(v) } : null; }).filter(Boolean);
+    try {
+      const res = await shopify('PUT', `/products/${id}.json`, { product: { id: Number(id), metafields } });
+      if (res.status === 200) { ok++; if (ok % 40 === 0) console.log(`  ...${ok}`); }
+      else { fail++; console.error(`  FAIL ${id}: HTTP ${res.status} ${JSON.stringify(res.body).slice(0, 160)}`); }
+      const [used] = (res.callLimit || '0/40').split('/').map(Number);
+      await sleep(used > 30 ? 1500 : 540);
+    } catch (e) { fail++; console.error(`  ERR ${id}: ${e.message}`); await sleep(540); }
+  }
+  console.log(`\nDONE. metafields written=${ok} failed=${fail}`);
+  await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/artmura-onboard/push-artmura-live.js b/scripts/artmura-onboard/push-artmura-live.js
index 4c4e407..4e83841 100644
--- a/scripts/artmura-onboard/push-artmura-live.js
+++ b/scripts/artmura-onboard/push-artmura-live.js
@@ -41,7 +41,9 @@ function shopify(method, p, body) {
     }, res => { let b = ''; res.on('data', c => b += c);
       res.on('end', () => { let j; try { j = JSON.parse(b); } catch { j = b; }
         resolve({ status: res.statusCode, body: j, callLimit: res.headers['x-shopify-shop-api-call-limit'] }); }); });
-    req.on('error', reject); if (data) req.write(data); req.end();
+    req.on('error', reject);
+    req.setTimeout(20000, () => req.destroy(new Error('request timeout')));
+    if (data) req.write(data); req.end();
   });
 }
 
@@ -66,8 +68,10 @@ function buildPayload(r, dwSku) {
   const tags = [r.pattern_series, r.collection_book, 'Wallcovering', 'Non-Woven', 'Made in Italy', 'Artmura', dwSku]
     .filter(Boolean).join(', ');
   const images = (r.images || []).map(src => ({ alt: esc(r.title), src }));
-  const mf = (key, value, type = 'single_line_text_field') =>
-    value ? { key, type, value: String(value), namespace: 'custom' } : null;
+  // NOTE: no metafields on create — the store has existing custom.* definitions with
+  // fixed types (material=multi_line_text_field, some keys=product_reference) and REST
+  // create rejects (422) on any type mismatch. Specs live in body_html; metafields are
+  // backfilled separately (backfill_metafields.js) with per-key type detection.
   return { product: {
     title, vendor: 'Artmura', product_type: r.product_type || 'Wallcoverings',
     status: STATUS, tags, images, body_html: specTable(r),
@@ -76,13 +80,6 @@ function buildPayload(r, dwSku) {
       { sku: dwSku, price: String(r.price_newwall_retail), option1: 'Yard', taxable: true, requires_shipping: true },
       { sku: `${dwSku}-Sample`, price: String(r.sample_price || 5), option1: 'Sample', taxable: true, requires_shipping: true },
     ],
-    metafields: [
-      mf('design_name', r.pattern_series), mf('colorway_name', r.color),
-      mf('collection', r.collection_book), mf('material', r.substrate || 'Non-Woven'),
-      mf('width', r.dimensions), mf('wall_coverage', r.wall_coverage),
-      mf('origin', r.origin), mf('grade', r.grade), mf('lead_time', r.lead_time),
-      mf('mfr_sku', r.mfr_sku), mf('source_url', r.product_url),
-    ].filter(Boolean),
   } };
 }
 

← c695c88 Thibaut: strip 160 junk parenthetical tags from live store (  ·  back to Dw Yolo Loop  ·  Artmura site: per-series 'Pattern' filter in catalog (21 col 9172451 →