← back to Designer Wallcoverings
Remap reseller vendor 'Newwall' -> real brand per-SKU (Coordonné 46 / Tres Tintas 28), scrub leak from vendor+title+description on 74 products
9a06a6a093d252d5d7bf3159fc1996b695b3da8a · 2026-06-26 09:25:22 -0700 · Steve
Files touched
A shopify/scripts/remap-newwall-to-coordonne.js
Diff
commit 9a06a6a093d252d5d7bf3159fc1996b695b3da8a
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jun 26 09:25:22 2026 -0700
Remap reseller vendor 'Newwall' -> real brand per-SKU (Coordonné 46 / Tres Tintas 28), scrub leak from vendor+title+description on 74 products
---
shopify/scripts/remap-newwall-to-coordonne.js | 106 ++++++++++++++++++++++++++
1 file changed, 106 insertions(+)
diff --git a/shopify/scripts/remap-newwall-to-coordonne.js b/shopify/scripts/remap-newwall-to-coordonne.js
new file mode 100644
index 00000000..3bbb6cfe
--- /dev/null
+++ b/shopify/scripts/remap-newwall-to-coordonne.js
@@ -0,0 +1,106 @@
+#!/usr/bin/env node
+/**
+ * Remap vendor "Newwall" (a reseller) -> real brand "Coordonné" on the 23 leaked
+ * DWXW products. Scrubs the leak from 3 fields: vendor, title suffix, descriptionHtml.
+ * Source of truth for the real brand: dw_unified.newwall_catalog.vendor_name
+ * (verified 2026-06-26: all 23 -> Coordonné).
+ *
+ * DRY RUN by default. Set APPLY=1 to write to Shopify + sync local mirror.
+ */
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+const ROOT = path.resolve(__dirname, '..', '..');
+const env = Object.fromEntries(
+ fs.readFileSync(path.join(ROOT, '.env'), 'utf8')
+ .split('\n').filter(l => l.includes('=') && !l.trim().startsWith('#'))
+ .map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()]; })
+);
+
+const STORE = (env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com').replace(/^https?:\/\//, '');
+const TOKEN = env.SHOPIFY_ADMIN_TOKEN || env.SHOPIFY_ADMIN_ACCESS_TOKEN;
+const APPLY = process.env.APPLY === '1';
+
+if (!TOKEN) { console.error('No SHOPIFY_ADMIN_TOKEN in .env'); process.exit(1); }
+
+// Per-SKU real-brand map (DWXW-key -> real brand) from dw_unified.newwall_catalog.vendor_name.
+// newwall.com is a reseller; the 74 live products split Coordonné(46)/Tres Tintas(28).
+const BRAND_MAP = Object.fromEntries(
+ fs.readFileSync('/tmp/newwall_brand_map.tsv', 'utf8').trim().split('\n')
+ .map(l => l.split('\t')).map(([k, v]) => [k.toUpperCase(), v])
+);
+const keyOf = handle => { const m = (handle || '').match(/dwxw-\d+/i); return m ? m[0].toUpperCase() : null; };
+
+async function gql(query, variables) {
+ const r = await fetch(`https://${STORE}/admin/api/2024-01/graphql.json`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
+ body: JSON.stringify({ query, variables }),
+ });
+ return r.json();
+}
+
+// --- field transforms (brand passed per-product) ---
+const fixTitle = (t, brand) => t.replace(/\s*\|\s*Newwall\s*$/i, ` | ${brand}`).replace(/Newwall/gi, brand);
+const fixDesc = (d, brand) => (d || '')
+ .replace(/from the\s+[A-Za-z0-9]+\s+collection by\s+Newwall/gi, `by ${brand}`) // kills bogus "C collection"
+ .replace(/Newwall/gi, brand);
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function fetchAll() {
+ const out = [];
+ let cursor = null, more = true;
+ while (more) {
+ const res = await gql(`
+ query($c:String){ products(first:100, query:"vendor:'Newwall'", after:$c){
+ pageInfo{hasNextPage endCursor}
+ edges{ node{ id handle title vendor descriptionHtml status } } } }`, { c: cursor });
+ const edges = res.data?.products?.edges || [];
+ edges.forEach(e => out.push(e.node));
+ const pi = res.data?.products?.pageInfo; more = pi?.hasNextPage; cursor = pi?.endCursor;
+ }
+ return out;
+}
+
+(async () => {
+ const products = await fetchAll();
+ console.log(`Found ${products.length} live products with vendor "Newwall"\n${'='.repeat(70)}`);
+ let changed = 0, skipped = 0;
+ const tally = {};
+ for (const p of products) {
+ const brand = BRAND_MAP[keyOf(p.handle)];
+ if (!brand) { skipped++; console.log(`\n[SKIP no brand match] ${p.handle}`); continue; }
+ const newTitle = fixTitle(p.title, brand);
+ const newDesc = fixDesc(p.descriptionHtml, brand);
+ const willChange = p.vendor !== brand || newTitle !== p.title || newDesc !== p.descriptionHtml;
+ if (!willChange) continue;
+ changed++; tally[brand] = (tally[brand] || 0) + 1;
+ console.log(`\n[${p.status}] ${p.handle}`);
+ console.log(` vendor: ${p.vendor} -> ${brand}`);
+ if (newTitle !== p.title) console.log(` title : ${p.title}\n -> ${newTitle}`);
+ if (newDesc !== p.descriptionHtml) console.log(` desc : …Newwall… -> …${brand}… (collection-letter artifact removed)`);
+
+ if (APPLY) {
+ const r = await gql(`
+ mutation($in:ProductInput!){ productUpdate(input:$in){ product{id} userErrors{field message} } }`,
+ { in: { id: p.id, title: newTitle, vendor: brand, descriptionHtml: newDesc } });
+ const errs = r.data?.productUpdate?.userErrors || [];
+ if (errs.length) { console.log(` !! ERROR: ${JSON.stringify(errs)}`); }
+ else {
+ console.log(` ✓ written`);
+ // keep local mirror in sync (PG is source of truth per CLAUDE.md; this re-aligns the cache)
+ try {
+ execFileSync('psql', ['-d', 'dw_unified', '-c',
+ `update shopify_products set vendor=$$${brand}$$, title=$$${newTitle.replace(/\$/g,'')}$$ where handle=$$${p.handle}$$`],
+ { stdio: 'ignore' });
+ } catch (_) {}
+ }
+ await sleep(350); // rate-limit courtesy
+ }
+ }
+ console.log(`\n${'='.repeat(70)}\n${APPLY ? 'APPLIED' : 'DRY RUN'} — ${changed}/${products.length} changed, ${skipped} skipped.`);
+ console.log('Per-brand:', JSON.stringify(tally));
+ if (!APPLY) console.log('Re-run with APPLY=1 to write live.');
+})();
← 9e5acb6f auto-save: 2026-06-26T08:43:59 (38 files) — pending-approval
·
back to Designer Wallcoverings
·
auto-save: 2026-06-26T09:44:31 (5 files) — pending-approval/ 14755f27 →