[object Object]

← 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

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 →