← back to Dw Yolo Loop
Thibaut mfr-fix: direct Shopify Admin REST pusher (DTD verdict A) — bypasses sandbox queue, writes live store from staging, dry-run + per-row logging
cd8b70e117ef273305f2cb4980f0862d6e7329a8 · 2026-06-11 14:02:27 -0700 · Steve Abrams
Files touched
A thibaut-mfr-fix/push-thibaut-live.js
Diff
commit cd8b70e117ef273305f2cb4980f0862d6e7329a8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 11 14:02:27 2026 -0700
Thibaut mfr-fix: direct Shopify Admin REST pusher (DTD verdict A) — bypasses sandbox queue, writes live store from staging, dry-run + per-row logging
---
thibaut-mfr-fix/push-thibaut-live.js | 103 +++++++++++++++++++++++++++++++++++
1 file changed, 103 insertions(+)
diff --git a/thibaut-mfr-fix/push-thibaut-live.js b/thibaut-mfr-fix/push-thibaut-live.js
new file mode 100644
index 0000000..2e13246
--- /dev/null
+++ b/thibaut-mfr-fix/push-thibaut-live.js
@@ -0,0 +1,103 @@
+#!/usr/bin/env node
+/**
+ * Direct Shopify Admin REST pusher for the Thibaut mfr-SKU fix (DTD verdict A, 2026-06-11).
+ * Bypasses the sandbox-targeted shopify_api_queue worker; writes straight to the LIVE store.
+ *
+ * Source of truth: PG table thibaut_mfr_fix_staging (fix_type 'tag'|'title', old_value, new_value, shopify_id).
+ * Each PUT merges ONLY the title or tags field — never variants/images/price.
+ *
+ * Usage:
+ * SHOPIFY_ADMIN_TOKEN=shpat_xxx node push-thibaut-live.js # dry-run: GET first 3, print planned change
+ * SHOPIFY_ADMIN_TOKEN=shpat_xxx node push-thibaut-live.js --apply # fire all, 2 req/sec, log per-row to staging
+ * ...optional: SHOPIFY_STORE=designerwallcoverings.myshopify.com (default) · --only=tag|title · --limit=N
+ */
+const https = require('https');
+const { Pool } = require('pg');
+
+const STORE = process.env.SHOPIFY_STORE || 'designerwallcoverings.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_PRODUCT_TOKEN;
+const API = process.env.SHOPIFY_API_VERSION || '2024-10';
+const DB = process.env.DATABASE_URL || 'postgresql://localhost/dw_unified';
+const APPLY = process.argv.includes('--apply');
+const ONLY = (process.argv.find(a => a.startsWith('--only=')) || '').split('=')[1] || null;
+const LIMIT = parseInt((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || '0', 10);
+
+if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN env var (never hard-code it).'); 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 buf = ''; res.on('data', c => buf += c);
+ res.on('end', () => { let j; try { j = JSON.parse(buf); } catch { j = buf; }
+ 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();
+ });
+}
+
+(async () => {
+ const where = ONLY ? `AND fix_type = '${ONLY === 'title' ? 'title' : 'tag'}'` : '';
+ const lim = LIMIT > 0 ? `LIMIT ${LIMIT}` : '';
+ const { rows } = await pool.query(`
+ SELECT id, shopify_id, fix_type, mfr_sku, old_value, new_value, status
+ FROM thibaut_mfr_fix_staging
+ WHERE status IN ('queued','staged','failed')
+ AND shopify_id ~ '^gid://shopify/Product/[0-9]+$' ${where}
+ ORDER BY fix_type, id ${lim}`);
+ console.log(`Store: ${STORE} · token …${TOKEN.slice(-4)} · rows to push: ${rows.length} · mode: ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
+
+ if (!APPLY) {
+ // confirm domain+token + show first 3 planned changes
+ for (const r of rows.slice(0, 3)) {
+ const pid = r.shopify_id.split('/').pop();
+ const got = await shopify('GET', `/products/${pid}.json?fields=id,title`);
+ const field = r.fix_type === 'title' ? 'title' : 'tags';
+ console.log(`\n[${r.fix_type}] product ${pid} (GET HTTP ${got.status})`);
+ console.log(` live title : ${got.body?.product?.title ?? '(not found / auth fail)'}`);
+ console.log(` will set ${field}: ${r.new_value.slice(0, 160)}${r.new_value.length > 160 ? '…' : ''}`);
+ await sleep(550);
+ }
+ console.log(`\nDRY-RUN only. Re-run with --apply to push all ${rows.length}.`);
+ await pool.end(); return;
+ }
+
+ let ok = 0, fail = 0;
+ for (const r of rows) {
+ const pid = r.shopify_id.split('/').pop();
+ const field = r.fix_type === 'title' ? 'title' : 'tags';
+ try {
+ const res = await shopify('PUT', `/products/${pid}.json`,
+ { product: { id: Number(pid), [field]: r.new_value } });
+ if (res.status === 200) {
+ ok++;
+ await pool.query(`UPDATE thibaut_mfr_fix_staging SET status='pushed' WHERE id=$1 AND fix_type=$2`, [r.id, r.fix_type]);
+ } else {
+ fail++;
+ await pool.query(`UPDATE thibaut_mfr_fix_staging SET status='failed' WHERE id=$1 AND fix_type=$2`, [r.id, r.fix_type]);
+ console.error(` FAIL ${r.fix_type} ${pid}: HTTP ${res.status} ${JSON.stringify(res.body).slice(0,140)}`);
+ }
+ if ((ok + fail) % 100 === 0) console.log(` ...${ok + fail}/${rows.length} (ok=${ok} fail=${fail})`);
+ // throttle ~2 req/sec; back off if near the 40-call bucket
+ const [used] = (res.callLimit || '0/40').split('/').map(Number);
+ await sleep(used > 30 ? 1500 : 520);
+ } catch (e) {
+ fail++; console.error(` ERR ${r.fix_type} ${pid}: ${e.message}`);
+ await sleep(520);
+ }
+ }
+ console.log(`\nDONE. pushed=${ok} failed=${fail} of ${rows.length}`);
+ await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });
← 298b81b Thibaut mfr-fix: flag jobs are in wrong queue DB (worker rea
·
back to Dw Yolo Loop
·
Thibaut mfr-fix: one-line run-push.sh wrapper (no line-wrap 8da5a20 →