← back to Dw Yolo Loop
Thibaut: strip 160 junk parenthetical tags from live store (GET-strip-PUT, preserves trailing color, 0 failed)
c695c88d59978a7b9672605163eb121271c89465 · 2026-06-12 09:41:38 -0700 · Steve Abrams
Files touched
M thibaut-mfr-fix/BACKLOG.mdA thibaut-mfr-fix/strip-junk-tags.js
Diff
commit c695c88d59978a7b9672605163eb121271c89465
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 09:41:38 2026 -0700
Thibaut: strip 160 junk parenthetical tags from live store (GET-strip-PUT, preserves trailing color, 0 failed)
---
thibaut-mfr-fix/BACKLOG.md | 3 +-
thibaut-mfr-fix/strip-junk-tags.js | 87 ++++++++++++++++++++++++++++++++++++++
2 files changed, 89 insertions(+), 1 deletion(-)
diff --git a/thibaut-mfr-fix/BACKLOG.md b/thibaut-mfr-fix/BACKLOG.md
index 3c31b1a..22ca926 100644
--- a/thibaut-mfr-fix/BACKLOG.md
+++ b/thibaut-mfr-fix/BACKLOG.md
@@ -17,7 +17,8 @@ Need the real pattern name sourced from Thibaut's catalog, then rebuild + de-SKU
| 82254 | T8610 | T8610 Cream \| Thibaut |
| 1244066 | TAMARAC | Tamarac \| Thibaut |
-## B) Junk parenthetical tags — ~160 active Thibaut products
+## B) ✅ DONE 2026-06-12 — Junk parenthetical tags stripped (160/160, 0 failed)
+_Original note:_ ~160 active Thibaut products
Tags contain a garbage entry like `Thibaut Taluk Sisal(T41170)` (SKU embedded in a parenthetical name).
The clean mfr token has now been added alongside; these junk tags should be stripped in a follow-up pass.
diff --git a/thibaut-mfr-fix/strip-junk-tags.js b/thibaut-mfr-fix/strip-junk-tags.js
new file mode 100644
index 0000000..518dcf5
--- /dev/null
+++ b/thibaut-mfr-fix/strip-junk-tags.js
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+/**
+ * Strip junk parenthetical tags ( "Thibaut Taluk Sisal(T41170)" ) from live Thibaut products.
+ * Operates on CURRENT LIVE tags (GET → strip → PUT) so it never clobbers the mfr-SKU tag
+ * added by the earlier backfill. Preserves any trailing color after the "(SKU)" (e.g. "...(T4061) - Taupe" -> "Taupe").
+ *
+ * SHOPIFY_ADMIN_TOKEN=… node strip-junk-tags.js # dry-run: show first 5 before/after
+ * SHOPIFY_ADMIN_TOKEN=… node strip-junk-tags.js --apply # PUT all, 2/sec
+ */
+const https = require('https');
+const { Pool } = require('pg');
+const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_PRODUCT_TOKEN;
+const API = '2024-10';
+const DB = process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp';
+const APPLY = process.argv.includes('--apply');
+if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const pool = new Pool({ connectionString: DB });
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function shopify(method, path, body) {
+ return new Promise((resolve, reject) => {
+ const data = body ? JSON.stringify(body) : null;
+ const req = https.request({ hostname: STORE, path: `/admin/api/${API}${path}`, method,
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json',
+ ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) } },
+ res => { let b=''; res.on('data',c=>b+=c); res.on('end',()=>{ let j; try{j=JSON.parse(b);}catch{j=b;}
+ resolve({ status: res.statusCode, body: j, callLimit: res.headers['x-shopify-shop-api-call-limit'] }); }); });
+ req.on('error', reject); if (data) req.write(data); req.end();
+ });
+}
+
+// remove the "(SKU)" junk from one tag segment; return cleaned text after the paren (or '' to drop)
+function cleanSegment(seg, mfr) {
+ const re = new RegExp('\\(' + mfr.replace(/[.*+?^${}()|[\]\\]/g,'\\$&') + '\\)', 'i');
+ if (!re.test(seg)) return seg; // not a junk segment — keep as-is
+ const after = seg.slice(seg.search(re) + seg.match(re)[0].length); // text after "(SKU)"
+ return after.replace(/^[\s\-–|]+/, '').trim(); // strip leading " - " etc; '' means drop
+}
+
+function rebuildTags(tags, mfr) {
+ const out = []; const seen = new Set();
+ for (const raw of tags.split(',')) {
+ const seg = raw.trim(); if (!seg) continue;
+ const cleaned = cleanSegment(seg, mfr);
+ if (!cleaned) continue; // dropped junk
+ const k = cleaned.toLowerCase();
+ if (seen.has(k)) continue; // dedup
+ seen.add(k); out.push(cleaned);
+ }
+ return out.join(', ');
+}
+
+(async () => {
+ const { rows } = await pool.query(`
+ SELECT split_part(shopify_id,'/',5) AS pid, mfr_sku
+ FROM shopify_products
+ WHERE (vendor_prefix='DWTT' OR vendor ILIKE 'thibaut') AND mfr_sku IS NOT NULL AND mfr_sku<>''
+ AND status='ACTIVE' AND shopify_id ~ '^gid://shopify/Product/[0-9]+$'
+ AND tags ~ ('\\(' || mfr_sku || '\\)')
+ ORDER BY pid`);
+ console.log(`Store: ${STORE} · junk-tag products: ${rows.length} · mode: ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
+ let ok=0, fail=0, nochange=0, shown=0;
+ for (const r of rows) {
+ let live;
+ try { live = await shopify('GET', `/products/${r.pid}.json?fields=id,tags`); }
+ catch (e) { fail++; console.error(` GET ERR ${r.pid}: ${e.message}`); await sleep(520); continue; }
+ if (live.status !== 200) { fail++; console.error(` GET ${r.pid}: HTTP ${live.status}`); await sleep(520); continue; }
+ const before = live.body.product.tags || '';
+ const after = rebuildTags(before, r.mfr_sku);
+ if (after === before) { nochange++; continue; }
+ if (!APPLY) {
+ if (shown++ < 5) {
+ const removed = before.split(',').map(s=>s.trim()).filter(s => !after.split(', ').includes(s));
+ console.log(`\n ${r.pid} (mfr ${r.mfr_sku}) removed/changed: ${JSON.stringify(removed)}`);
+ }
+ await sleep(120); continue;
+ }
+ const res = await shopify('PUT', `/products/${r.pid}.json`, { product: { id: Number(r.pid), tags: after } });
+ if (res.status === 200) ok++; else { fail++; console.error(` PUT FAIL ${r.pid}: HTTP ${res.status} ${JSON.stringify(res.body).slice(0,120)}`); }
+ if ((ok+fail) % 50 === 0) console.log(` ...${ok+fail}/${rows.length} (ok=${ok} fail=${fail})`);
+ const [used] = (res.callLimit || '0/40').split('/').map(Number);
+ await sleep(used > 30 ? 1500 : 520);
+ }
+ console.log(`\n${APPLY ? 'DONE' : 'DRY-RUN'}. changed=${ok} failed=${fail} no-change=${nochange} of ${rows.length}`);
+ await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });
← 7a52550 Artmura site: README documenting features, data, endpoints,
·
back to Dw Yolo Loop
·
Artmura push: fix 422 (drop metafields from create — store d 55fc397 →