← 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
A scripts/stamp-shopify-bundle-metafields.js
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 →