[object Object]

← back to Wallco Ai

scripts: stamp-shopify-bundle-metafields.js — repeatable stamper for wallco_ai.* product metafields

2973b9e3bc850e72b63f2de217796dbdce6106cc · 2026-05-28 17:21:57 -0700 · Steve Abrams

Tool that brings any pre-webhook bundle product up to the 4-metafield
shape that the orders/paid webhook handler needs to mint per-order
download tokens:

  - wallco_ai.bundle_slug          single_line_text_field
  - wallco_ai.bundle_design_ids    single_line_text_field
  - wallco_ai.bundle_count         number_integer
  - wallco_ai.source_date          date

Reuses the metafieldsSet pattern from attach-shopify-bundle-files.js
attachMetafield(). Post-write verify via REST against the live product
catches partial writes. Returns metafield IDs so a reversal via
metafieldDelete is a one-liner.

Usage:
  node scripts/stamp-shopify-bundle-metafields.js \
    --product=<id> --slug=<slug> --ids=<csv> --date=YYYY-MM-DD

Note: ran live against the two products called out in the task brief
(7843830923315 designer-zoo-calm + 7843831087155 drunk-monkeys-v2) and
Shopify returned 'Owner does not exist' on both — those product IDs are
not present on designer-laboratory-sandbox.myshopify.com (GraphQL
products query for handle:*designer-zoo* and handle:*drunk-monkey* both
return 0 edges, and the dry-run mass-push manifest from 2026-05-28
confirms only the cactus bundle was actually created). Stamper is ready
once the real product IDs are in hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 2973b9e3bc850e72b63f2de217796dbdce6106cc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 28 17:21:57 2026 -0700

    scripts: stamp-shopify-bundle-metafields.js — repeatable stamper for wallco_ai.* product metafields
    
    Tool that brings any pre-webhook bundle product up to the 4-metafield
    shape that the orders/paid webhook handler needs to mint per-order
    download tokens:
    
      - wallco_ai.bundle_slug          single_line_text_field
      - wallco_ai.bundle_design_ids    single_line_text_field
      - wallco_ai.bundle_count         number_integer
      - wallco_ai.source_date          date
    
    Reuses the metafieldsSet pattern from attach-shopify-bundle-files.js
    attachMetafield(). Post-write verify via REST against the live product
    catches partial writes. Returns metafield IDs so a reversal via
    metafieldDelete is a one-liner.
    
    Usage:
      node scripts/stamp-shopify-bundle-metafields.js \
        --product=<id> --slug=<slug> --ids=<csv> --date=YYYY-MM-DD
    
    Note: ran live against the two products called out in the task brief
    (7843830923315 designer-zoo-calm + 7843831087155 drunk-monkeys-v2) and
    Shopify returned 'Owner does not exist' on both — those product IDs are
    not present on designer-laboratory-sandbox.myshopify.com (GraphQL
    products query for handle:*designer-zoo* and handle:*drunk-monkey* both
    return 0 edges, and the dry-run mass-push manifest from 2026-05-28
    confirms only the cactus bundle was actually created). Stamper is ready
    once the real product IDs are in hand.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 scripts/stamp-shopify-bundle-metafields.js | 165 +++++++++++++++++++++++++++++
 1 file changed, 165 insertions(+)

diff --git a/scripts/stamp-shopify-bundle-metafields.js b/scripts/stamp-shopify-bundle-metafields.js
new file mode 100755
index 0000000..f6a048a
--- /dev/null
+++ b/scripts/stamp-shopify-bundle-metafields.js
@@ -0,0 +1,165 @@
+#!/usr/bin/env node
+/**
+ * stamp-shopify-bundle-metafields.js
+ *
+ * Repeatable stamper for the 4 wallco_ai.* product metafields that the
+ * `POST /api/shopify/webhooks/orders-paid` handler needs to mint per-order
+ * download tokens for bundle products.
+ *
+ * Used to bring "pre-webhook" bundle products up to the shape that
+ * scripts/push-shopify-digital-bundle.js produces for newly-created ones.
+ *
+ * Metafields stamped (wallco_ai namespace):
+ *   - bundle_slug          single_line_text_field
+ *   - bundle_design_ids    single_line_text_field   (comma-separated PG ids)
+ *   - bundle_count         number_integer
+ *   - source_date          date                     (YYYY-MM-DD)
+ *
+ * Usage:
+ *   node scripts/stamp-shopify-bundle-metafields.js \
+ *     --product=7843830923315 \
+ *     --slug=designer-zoo-calm \
+ *     --ids=54111,54101 \
+ *     --date=2026-05-28
+ *
+ * Optional:
+ *   --dry           print payload, do not write
+ *   --no-verify     skip the post-write GET re-check
+ *
+ * Exits non-zero on userErrors or post-verify mismatch. Reversibility:
+ * each metafieldsSet returns the metafield id; capture from stdout and
+ * `metafieldDelete` if ever needed.
+ */
+'use strict';
+
+const path = require('path');
+const fs = require('fs');
+
+// ─── env (read .env directly; avoids shell `set -a` parsing of multiline keys) ───
+const envPath = path.join(__dirname, '..', '.env');
+if (fs.existsSync(envPath)) {
+  for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
+    const m = /^([A-Z0-9_]+)=(.*)$/.exec(line);
+    if (m && !(m[1] in process.env)) process.env[m[1]] = m[2];
+  }
+}
+const store = process.env.SHOPIFY_STORE;
+const token = process.env.SHOPIFY_ADMIN_TOKEN;
+if (!store || !token) {
+  console.error('missing SHOPIFY_STORE or SHOPIFY_ADMIN_TOKEN in .env');
+  process.exit(2);
+}
+
+// ─── args ─────────────────────────────────────────────────────────────────────
+function arg(name, fallback) {
+  const hit = process.argv.find(a => a.startsWith(`--${name}=`));
+  return hit ? hit.slice(name.length + 3) : fallback;
+}
+const flag = (name) => process.argv.includes(`--${name}`);
+const productId = arg('product');
+const slug = arg('slug');
+const ids = arg('ids');           // "54111,54101"
+const date = arg('date');         // "2026-05-28"
+const countOverride = arg('count');
+const dry = flag('dry');
+const noVerify = flag('no-verify');
+
+if (!productId || !slug || !ids || !date) {
+  console.error('usage: --product=<id> --slug=<slug> --ids=<csv> --date=YYYY-MM-DD [--count=N] [--dry] [--no-verify]');
+  process.exit(2);
+}
+if (!/^\d+$/.test(productId)) { console.error('product must be numeric'); process.exit(2); }
+if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { console.error('date must be YYYY-MM-DD'); process.exit(2); }
+const idArr = ids.split(',').map(s => s.trim()).filter(Boolean);
+if (idArr.length === 0 || !idArr.every(s => /^\d+$/.test(s))) {
+  console.error('ids must be comma-separated integers');
+  process.exit(2);
+}
+const count = countOverride ? Number(countOverride) : idArr.length;
+if (!Number.isInteger(count) || count < 1) { console.error('count must be a positive integer'); process.exit(2); }
+
+const ownerId = `gid://shopify/Product/${productId}`;
+const url = `https://${store}/admin/api/2026-01/graphql.json`;
+
+// ─── build metafieldsSet payload ──────────────────────────────────────────────
+const metafields = [
+  { ownerId, namespace: 'wallco_ai', key: 'bundle_slug',       type: 'single_line_text_field', value: slug },
+  { ownerId, namespace: 'wallco_ai', key: 'bundle_design_ids', type: 'single_line_text_field', value: idArr.join(',') },
+  { ownerId, namespace: 'wallco_ai', key: 'bundle_count',      type: 'number_integer',         value: String(count) },
+  { ownerId, namespace: 'wallco_ai', key: 'source_date',       type: 'date',                   value: date },
+];
+
+console.log(`product ${productId} (${ownerId})`);
+console.log(`  slug=${slug}  ids=${idArr.join(',')}  count=${count}  date=${date}`);
+
+if (dry) {
+  console.log('--- DRY: would POST metafieldsSet ---');
+  console.log(JSON.stringify(metafields, null, 2));
+  process.exit(0);
+}
+
+// ─── write ───────────────────────────────────────────────────────────────────
+const mutation = `mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
+  metafieldsSet(metafields: $metafields) {
+    metafields { id namespace key value type }
+    userErrors { field message code }
+  }
+}`;
+
+(async () => {
+  const r = await fetch(url, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': token },
+    body: JSON.stringify({ query: mutation, variables: { metafields } }),
+  });
+  const j = await r.json();
+  if (j.errors) {
+    console.error('graphql errors:', JSON.stringify(j.errors).slice(0, 800));
+    process.exit(3);
+  }
+  const ue = (j.data && j.data.metafieldsSet && j.data.metafieldsSet.userErrors) || [];
+  if (ue.length) {
+    console.error('userErrors:', JSON.stringify(ue, null, 2));
+    process.exit(4);
+  }
+  const written = j.data.metafieldsSet.metafields;
+  console.log(`OK — stamped ${written.length} metafields:`);
+  for (const m of written) {
+    console.log(`  ${m.namespace}.${m.key} (${m.type}) = ${JSON.stringify(m.value)}  id=${m.id}`);
+  }
+
+  if (noVerify) return;
+
+  // ─── post-write verify via REST (don't trust the mutation response alone) ──
+  const verifyUrl = `https://${store}/admin/api/2026-01/products/${productId}/metafields.json`;
+  const rv = await fetch(verifyUrl, { headers: { 'X-Shopify-Access-Token': token } });
+  const jv = await rv.json();
+  const live = (jv.metafields || []).filter(m => m.namespace === 'wallco_ai');
+  const liveByKey = Object.fromEntries(live.map(m => [m.key, m]));
+  const required = ['bundle_slug', 'bundle_design_ids', 'bundle_count', 'source_date'];
+  const missing = required.filter(k => !liveByKey[k]);
+  if (missing.length) {
+    console.error('VERIFY FAILED — missing keys:', missing.join(', '));
+    process.exit(5);
+  }
+  // value sanity check
+  const want = {
+    bundle_slug: slug,
+    bundle_design_ids: idArr.join(','),
+    bundle_count: String(count),
+    source_date: date,
+  };
+  const mismatches = [];
+  for (const k of required) {
+    const got = String(liveByKey[k].value);
+    if (got !== want[k]) mismatches.push(`${k}: want=${JSON.stringify(want[k])} got=${JSON.stringify(got)}`);
+  }
+  if (mismatches.length) {
+    console.error('VERIFY FAILED — value mismatch:\n  ' + mismatches.join('\n  '));
+    process.exit(6);
+  }
+  console.log('VERIFY OK — all 4 wallco_ai.* metafields present and matching on live product');
+})().catch(e => {
+  console.error('fatal:', e && e.message ? e.message : e);
+  process.exit(1);
+});

← c000130 feat(pdp): V2 Bento Grid theme — /design/:id/bento + /api/de  ·  back to Wallco Ai  ·  feat(pdp): theme submenu inside UR hamburger — Classic + V2 7f1d2f6 →