[object Object]

← back to Dw Photo Capture

Photo capture: post each metafield with its store-defined type (fix 422)

c03988dc8aafd1d75fa46574a128bde3192a9420 · 2026-09-18 13:52:33 -0700 · steve

Live test caught a Shopify 422: mf() forced single_line_text_field for every
metafield, but custom.material is defined multi_line_text_field on the store, so
Shopify rejected the whole product create. Now fetch PRODUCT metafield definitions
once (cached) -> (ns.key)->type map, post each metafield with its declared type;
keys with no definition default to single_line (free-form, accepted). Only
custom.material was multi_line among the fields createNewItem writes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCq3132eTmJESAKqJBM8FN

Files touched

Diff

commit c03988dc8aafd1d75fa46574a128bde3192a9420
Author: steve <steve@designerwallcoverings.com>
Date:   Fri Sep 18 13:52:33 2026 -0700

    Photo capture: post each metafield with its store-defined type (fix 422)
    
    Live test caught a Shopify 422: mf() forced single_line_text_field for every
    metafield, but custom.material is defined multi_line_text_field on the store, so
    Shopify rejected the whole product create. Now fetch PRODUCT metafield definitions
    once (cached) -> (ns.key)->type map, post each metafield with its declared type;
    keys with no definition default to single_line (free-form, accepted). Only
    custom.material was multi_line among the fields createNewItem writes.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01DCq3132eTmJESAKqJBM8FN
---
 server.js | 29 ++++++++++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 2c7af64..c74218d 100644
--- a/server.js
+++ b/server.js
@@ -2424,6 +2424,30 @@ function pgQuery(sql) {
   });
 }
 
+// ── Metafield TYPE map (store definitions) ───────────────────────────────────
+// A metafield value MUST be posted with the type its DEFINITION declares, or Shopify 422s the whole
+// product create (e.g. custom.material is defined multi_line_text_field on this store — forcing
+// single_line rejects the create). Fetch every PRODUCT definition once (cached 10m) → {"ns.key":type}.
+// (namespace,key) pairs with NO definition are free-form and safely accept single_line_text_field.
+let _mfTypeCache = { at: 0, map: null };
+async function getMetafieldTypeMap() {
+  if (_mfTypeCache.map && Date.now() - _mfTypeCache.at < 10 * 60 * 1000) return _mfTypeCache.map;
+  const map = {};
+  try {
+    let cursor = null, pages = 0;
+    do {
+      const r = await gql(`query($c:String){ metafieldDefinitions(first:250, ownerType:PRODUCT, after:$c){
+        pageInfo{ hasNextPage endCursor } edges{ node{ namespace key type{ name } } } } }`, { c: cursor });
+      const md = r.data && r.data.metafieldDefinitions;
+      if (!md) break;
+      for (const e of md.edges) map[`${e.node.namespace}.${e.node.key}`] = e.node.type.name;
+      cursor = md.pageInfo.hasNextPage ? md.pageInfo.endCursor : null;
+    } while (cursor && ++pages < 10);
+  } catch (e) { /* fall back to per-key default below */ }
+  _mfTypeCache = { at: Date.now(), map };
+  return map;
+}
+
 // ── VENDOR-FIRST dropdown source: the canonical vendor_registry ──────────────
 // Returns one row per real, active vendor with its canonical DW# minting inputs
 // (sku_prefix + sku_range_start) and — best-effort — the short FileMaker `vid`
@@ -2592,7 +2616,10 @@ async function createNewItem(p, b64, dryRun) {
   if (dryRun) return { ok: true, dryRun: true, preview };
   // ── COMMIT: PostgreSQL-staging + Shopify draft + FileMaker master (draft-only, never published) ──
   try {
-    const mf = (ns, key, val) => val ? { namespace: ns, key, value: String(val), type: 'single_line_text_field' } : null;
+    // Each metafield MUST carry the type its store DEFINITION declares (e.g. custom.material is
+    // multi_line_text_field) or Shopify 422s the whole create. Undefined keys default to single_line.
+    const mfTypes = await getMetafieldTypeMap();
+    const mf = (ns, key, val) => val ? { namespace: ns, key, value: String(val), type: (mfTypes[`${ns}.${key}`] || 'single_line_text_field') } : null;
     const variants = [{ option1: 'Roll', sku: dwsku, inventory_management: 'shopify', inventory_quantity: 0 },
       { option1: 'Sample', sku: dwsku + '-Sample', price: '4.25', inventory_management: 'shopify', inventory_quantity: 0 }];
     if (price) variants[0].price = price;

← 37a2682 auto-data-snapshot: 2026-09-18T13:45:34 (2 data files) — dat  ·  back to Dw Photo Capture  ·  Photo capture: two-photo capture + mfr# identity gate (front 4cbb39e →