[object Object]

← back to Designer Wallcoverings

Fix double-encoded metafield leak: unwrap {type,value} blobs before writing custom.material / dwc.contents

b25933f1be9ea12a28f2794577b3c09a9c474234 · 2026-06-24 15:49:50 -0700 · Steve Abrams

Root cause of raw JSON ('{"type":"single_line_text_field","value":"Nonwoven"}')
appearing in the Material/Contents spec on ~4,300 DWQW/Malibu PDPs. Adds mfVal()
guard in both metafield writers so dirty source columns can no longer leak.

Files touched

Diff

commit b25933f1be9ea12a28f2794577b3c09a9c474234
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jun 24 15:49:50 2026 -0700

    Fix double-encoded metafield leak: unwrap {type,value} blobs before writing custom.material / dwc.contents
    
    Root cause of raw JSON ('{"type":"single_line_text_field","value":"Nonwoven"}')
    appearing in the Material/Contents spec on ~4,300 DWQW/Malibu PDPs. Adds mfVal()
    guard in both metafield writers so dirty source columns can no longer leak.
---
 DW-Agents/vendor-command-center/server.js          | 26 ++++++++++++++++++----
 .../nextjs-liquid-app/backfill-metafields.js       | 19 +++++++++++++++-
 2 files changed, 40 insertions(+), 5 deletions(-)

diff --git a/DW-Agents/vendor-command-center/server.js b/DW-Agents/vendor-command-center/server.js
index eef3a09c..fd039183 100644
--- a/DW-Agents/vendor-command-center/server.js
+++ b/DW-Agents/vendor-command-center/server.js
@@ -27,6 +27,24 @@ const stdWorker = require('./standardization-worker');
 const { processImage, uploadCroppedToShopify } = require('./logo-cropper');
 const { validateVendor, getDiscontinuationSummary } = require('./discontinuation-validator');
 
+// Defensive unwrap for double-encoded metafield values. Some upstream scrapers
+// stored a whole Shopify metafield object — {"type":"single_line_text_field","value":"X"}
+// — as the *string* value of a spec column (material/composition/etc). Writing that
+// straight to a metafield leaks raw JSON onto the PDP. mfVal() returns the inner
+// .value when it detects that shape, otherwise the value unchanged. (Bug: DWQW/Malibu
+// cohort, custom.material + dwc.contents — 2026-06-24.)
+function mfVal(v) {
+  if (typeof v !== 'string') return v;
+  const s = v.trim();
+  if (s.startsWith('{') && s.includes('"value"')) {
+    try {
+      const o = JSON.parse(s);
+      if (o && typeof o.value === 'string') return o.value;
+    } catch (_) { /* not JSON — fall through */ }
+  }
+  return v;
+}
+
 const app = express();
 const PORT = 9660;
 
@@ -1103,7 +1121,7 @@ app.post('/api/launch-test', async (req, res) => {
       // Private label: hide real vendor name, collection, and MFR SKU from public fields
       const isPL = !!(v.is_private_label && v.private_label_name);
       const tags = [displayVendor, p.collection, p.pattern_name, 'Luxury', 'Wallcovering'].filter(Boolean);
-      if (p.material) tags.push(p.material);
+      if (p.material) tags.push(mfVal(p.material));
       if (p.fire_rating) tags.push('Fire Rated');
       if (p.design_style) tags.push(p.design_style);
       // For private label, also add MFR SKU as tag (searchable but not customer-visible in title)
@@ -1188,7 +1206,7 @@ app.post('/api/launch-test', async (req, res) => {
         // Private label: store real vendor info in hidden namespace
         isPL ? { namespace: 'private_label', key: 'real_vendor_name', value: v.vendor_name } : null,
         isPL ? { namespace: 'private_label', key: 'real_mfr_sku', value: p.mfr_sku } : null,
-        { namespace: 'global', key: 'Contents', value: p.material },
+        { namespace: 'global', key: 'Contents', value: mfVal(p.material) },
         { namespace: 'global', key: 'application', value: p.application || 'Paste the wall' },
         // Repeat (patterns only, not textures)
         !isTexture ? { namespace: 'global', key: 'repeat', value: p.pattern_repeat } : null,
@@ -1266,7 +1284,7 @@ app.post('/api/launch-test', async (req, res) => {
             if (p.pattern_name) tagSet.add(p.pattern_name);
             if (p.color_name) tagSet.add(p.color_name);
             if (p.collection) tagSet.add(p.collection);
-            if (p.material) tagSet.add(p.material);
+            if (p.material) tagSet.add(mfVal(p.material));
 
             // ALL colors (no "Background Color" prefix)
             (aiAnalysis.allColors || []).forEach(c => { if (c) tagSet.add(c.trim()); });
@@ -1308,7 +1326,7 @@ app.post('/api/launch-test', async (req, res) => {
             idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'brand', value: v.vendor_name, type: 'single_line_text_field' });
             idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'manufacturer_sku', value: p.mfr_sku, type: 'single_line_text_field' });
             idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'collection_name', value: p.collection || v.vendor_name, type: 'single_line_text_field' });
-            idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'material', value: p.material || 'Non-woven', type: 'multi_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'material', value: mfVal(p.material) || 'Non-woven', type: 'multi_line_text_field' });
             // dwc.* fields
             idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'pattern_name', value: p.pattern_name || '', type: 'single_line_text_field' });
             idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'color', value: p.color_name || aiAnalysis.dominantColor, type: 'single_line_text_field' });
diff --git a/DW-Programming/nextjs-liquid-app/backfill-metafields.js b/DW-Programming/nextjs-liquid-app/backfill-metafields.js
index bba22616..1818e2cd 100644
--- a/DW-Programming/nextjs-liquid-app/backfill-metafields.js
+++ b/DW-Programming/nextjs-liquid-app/backfill-metafields.js
@@ -11,6 +11,23 @@ const STORE = 'designer-laboratory-sandbox.myshopify.com';
 const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const pool = new Pool({ connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified') });
 
+// Defensive unwrap for double-encoded metafield values. Some upstream scrapers
+// stored a whole Shopify metafield object — {"type":"single_line_text_field","value":"X"}
+// — as the *string* value of a spec column (composition/material/etc). Writing that
+// straight to dwc.contents leaks raw JSON onto the PDP. Returns the inner .value when
+// it detects that shape, otherwise the value unchanged. (DWQW/Malibu cohort, 2026-06-24.)
+function mfVal(v) {
+  if (typeof v !== 'string') return v;
+  const s = v.trim();
+  if (s.startsWith('{') && s.includes('"value"')) {
+    try {
+      const o = JSON.parse(s);
+      if (o && typeof o.value === 'string') return o.value;
+    } catch (_) { /* not JSON — fall through */ }
+  }
+  return v;
+}
+
 // Rate limit: 2 req/sec with burst of 40
 const DELAY_MS = 550;
 let requestCount = 0;
@@ -95,7 +112,7 @@ function buildMissingMetafields(product, existingKeys, vendorData) {
   const vendorName = product.vendor_name || '';
   const width = product.width || '';
   const repeat = product.pattern_repeat || product.repeat || '';
-  const composition = product.composition || '';
+  const composition = mfVal(product.composition) || '';
   const maintenance = product.maintenance || product.care_instructions || '';
   const country = product.country_of_origin || product.country || 'USA';
   const matchType = product.match_type || '';

← 005ffea3 patcher: make .value regex non-backtracking ((?!\\w)) so re-  ·  back to Designer Wallcoverings  ·  Add fix-double-encoded-metafields.js: dry-run-safe repair fo bcebc432 →