[object Object]

← back to Designer Wallcoverings

tag-designer-colors.js — FALLBACK for Boost tiers without metafield-sourced filters: writes prefixed 'color:<DesignerName>' tag per active product from custom.color_1_name (+ --top2). Prefix avoids re-polluting the cleaned color smart-collections. Idempotent/resumable. Tested 3 live, 0 failed. Run: node tag-designer-colors.js --conc 8 (only if Boost can't do metafields).

1e5c69bab6b156c62911176dd2c2622df04d4843 · 2026-06-14 14:29:38 -0700 · Steve Abrams

Files touched

Diff

commit 1e5c69bab6b156c62911176dd2c2622df04d4843
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jun 14 14:29:38 2026 -0700

    tag-designer-colors.js — FALLBACK for Boost tiers without metafield-sourced filters: writes prefixed 'color:<DesignerName>' tag per active product from custom.color_1_name (+ --top2). Prefix avoids re-polluting the cleaned color smart-collections. Idempotent/resumable. Tested 3 live, 0 failed. Run: node tag-designer-colors.js --conc 8 (only if Boost can't do metafields).
---
 DW-Programming/tag-designer-colors.js | 80 +++++++++++++++++++++++++++++++++++
 1 file changed, 80 insertions(+)

diff --git a/DW-Programming/tag-designer-colors.js b/DW-Programming/tag-designer-colors.js
new file mode 100644
index 00000000..3317b6a1
--- /dev/null
+++ b/DW-Programming/tag-designer-colors.js
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+/**
+ * FALLBACK for Boost tiers that can't source a filter from a metafield: write the
+ * designer color name as a PREFIXED tag (default "color:Ecru") on each active
+ * product, read from the custom.color_1_name (+ optionally color_2_name) metafield.
+ * Boost is then configured with a tag-based Color filter on that prefix.
+ *
+ * PREFIX is deliberate: a bare "Navy"/"Sage" tag would re-add products to the color
+ * SMART COLLECTIONS we just cleaned — the "color:" prefix avoids that collision.
+ *
+ * Idempotent + resumable: only products MISSING the tag get a productUpdate (tags
+ * are merged, never replaced wholesale beyond adding the color tag).
+ * Usage: node tag-designer-colors.js [--limit N] [--conc 6] [--top2] [--prefix "color:"] [--dry-run]
+ */
+const https = require('https');
+const args = process.argv.slice(2);
+const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API_VERSION = '2024-01';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const DRY = args.includes('--dry-run');
+const TOP2 = args.includes('--top2');
+const PREFIX = args.find((_, i, a) => a[i-1] === '--prefix') || 'color:';
+const LIMIT = parseInt(args.find((_, i, a) => a[i-1] === '--limit') || '0', 10) || 0;
+const CONC = parseInt(args.find((_, i, a) => a[i-1] === '--conc') || '6', 10) || 6;
+
+function gql(query, variables){
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query, variables });
+    const req = https.request({ hostname: SHOPIFY_DOMAIN, path: `/admin/api/${API_VERSION}/graphql.json`, method: 'POST',
+      headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, 'Content-Length': Buffer.byteLength(body) }, timeout: 30000 }, res => {
+      let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } });
+    });
+    req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); req.write(body); req.end();
+  });
+}
+const tagFor = (name) => PREFIX + String(name).trim();
+async function addTags(id, mergedTags){
+  const r = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ userErrors{message} } }`,
+    { input: { id: `gid://shopify/Product/${id}`, tags: mergedTags } });
+  const ue = r?.data?.productUpdate?.userErrors; if (ue && ue.length) throw new Error(ue[0].message);
+  if (r?.errors) throw new Error(JSON.stringify(r.errors).slice(0,100));
+}
+(async () => {
+  if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+  console.log(`Tag designer colors — ${DRY?'DRY':'LIVE'} — prefix "${PREFIX}"${TOP2?' top2':' top1'} conc ${CONC}`);
+  let cursor=null,page=0,scanned=0,tagged=0,already=0,skip=0,failed=0; const todo=[];
+  while(true){
+    const after=cursor?`, after:"${cursor}"`:'';
+    const q=`{ products(first:100, query:"status:active"${after}){ pageInfo{hasNextPage endCursor} nodes{
+      id tags c1:metafield(namespace:"custom",key:"color_1_name"){value} c2:metafield(namespace:"custom",key:"color_2_name"){value} }}}`;
+    let r; try{r=await gql(q);}catch{ await new Promise(s=>setTimeout(s,3000)); continue; }
+    if(r.errors){ if(JSON.stringify(r.errors).includes('Throttled')){await new Promise(s=>setTimeout(s,4000));continue;} console.error(r.errors);break; }
+    const pg=r.data.products; page++;
+    for(const n of pg.nodes){ scanned++;
+      const names=[n.c1?.value].concat(TOP2?[n.c2?.value]:[]).filter(Boolean);
+      if(!names.length){ skip++; continue; }
+      const want=names.map(tagFor);
+      const have=new Set((n.tags||[]));
+      const missing=want.filter(t=>!have.has(t));
+      if(!missing.length){ already++; continue; }
+      todo.push({ id:n.id.replace('gid://shopify/Product/',''), tags:[...n.tags, ...missing] });
+    }
+    if(page%20===0) console.log(`  scanned ${scanned} | to-tag ${todo.length} | already ${already} | no-color ${skip}`);
+    if(!pg.pageInfo.hasNextPage) break; cursor=pg.pageInfo.endCursor;
+    if(LIMIT && todo.length>=LIMIT) break;
+    await new Promise(s=>setTimeout(s,200));
+  }
+  const work = LIMIT ? todo.slice(0,LIMIT) : todo;
+  console.log(`\nScan: ${scanned} active | ${work.length} to tag | ${already} already tagged | ${skip} no color metafield.`);
+  let idx=0; const queue=[...work];
+  async function worker(){
+    while(queue.length){
+      const p=queue.shift(); const n=++idx;
+      try{ if(!DRY) await addTags(p.id, p.tags); tagged++; if(n<=8||n%500===0) console.log(`  [${n}] tagged ${p.id}`); }
+      catch(e){ failed++; if(String(e.message).match(/Throttled|429/)) await new Promise(r=>setTimeout(r,3000)); if(n%200===0) console.log(`  [${n}] ERR ${p.id}: ${e.message}`); }
+    }
+  }
+  await Promise.all(Array.from({length:CONC},worker));
+  console.log(`\nDone: ${tagged} products tagged, ${failed} failed. Boost → add a tag-based Color filter on prefix "${PREFIX}".`);
+})().catch(e=>{console.error(e);process.exit(1);});

← a96bd4e7 sweep-to-100.js — drives search-by-color coverage to 100%: s  ·  back to Designer Wallcoverings  ·  brands-page: luxe wordmark SVGs for 6 new high-inventory bra 79ccd409 →