[object Object]

← back to Designer Wallcoverings

Osborne width backfill tool — found catalog overlap exhausted: 896 live gaps have no osborne_catalog source (need re-scrape, not metafield push)

066739934f53f5f7ab4deae58c2eec9c670f9195 · 2026-06-20 11:39:33 -0700 · Steve

Files touched

Diff

commit 066739934f53f5f7ab4deae58c2eec9c670f9195
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 20 11:39:33 2026 -0700

    Osborne width backfill tool — found catalog overlap exhausted: 896 live gaps have no osborne_catalog source (need re-scrape, not metafield push)
---
 shopify/scripts/cadence/backfill-osborne-width.js | 54 +++++++++++++++++++++++
 1 file changed, 54 insertions(+)

diff --git a/shopify/scripts/cadence/backfill-osborne-width.js b/shopify/scripts/cadence/backfill-osborne-width.js
new file mode 100644
index 00000000..872612c2
--- /dev/null
+++ b/shopify/scripts/cadence/backfill-osborne-width.js
@@ -0,0 +1,54 @@
+'use strict';
+// Backfill width (+ mfr_sku) metafields on ALL Osborne ACTIVE products missing them,
+// sourced from osborne_catalog.width_inches via mfr_sku. Skips material (data quality).
+// DRY-RUN default; --commit writes. Idempotent (helper skips existing). $0 (Shopify API).
+const fs = require('fs'), path = require('path'), { execFileSync } = require('child_process');
+for (const p of [path.join(__dirname, '..', '..', '.env'), path.join(__dirname, '..', '.env')])
+  if (fs.existsSync(p)) for (const l of fs.readFileSync(p, 'utf8').split('\n')) { const m = l.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, ''); }
+const { pushSpecMetafields } = require('../lib/push-spec-metafields');
+const STORE = 'designer-laboratory-sandbox.myshopify.com', TOKEN = process.env.SHOPIFY_ADMIN_TOKEN, VER = '2024-10';
+const COMMIT = process.argv.includes('--commit');
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function gqlRetry(q, v) { for (let a = 0; a < 4; a++) { const r = await fetch(`https://${STORE}/admin/api/${VER}/graphql.json`, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) }); if (r.status === 429) { await sleep(2000 * (a + 1)); continue; } return { json: await r.json() }; } return { json: {} }; }
+const widthMf = mfs => (mfs || []).some(m => /width/i.test(m.key) && m.value && m.value.trim());
+
+(async () => {
+  // 1. preload osborne_catalog mfr_sku -> normalized width (one query)
+  const cat = new Map();
+  execFileSync('psql', ['-d', 'dw_unified', '-F\t', '-tAc', `SELECT upper(mfr_sku), width_inches::text FROM osborne_catalog WHERE width_inches IS NOT NULL AND mfr_sku IS NOT NULL`], { encoding: 'utf8' })
+    .trim().split('\n').forEach(l => { const [m, w] = l.split('\t'); if (m && w) cat.set(m, `${parseFloat(w).toFixed(2)} Inches`); });
+  console.log(`catalog width-map: ${cat.size} Osborne SKUs\n`);
+
+  // 2. paginate all Osborne actives
+  const Q = `query($cur:String){ products(first:200, query:"status:active vendor:'Osborne & Little'", after:$cur){ pageInfo{hasNextPage endCursor} nodes{ id variants(first:4){nodes{sku}} metafields(first:50){nodes{namespace key value}} } } }`;
+  let cur = null, total = 0, missing = 0, linkable = 0, pushed = 0, noCat = 0, errors = 0;
+  for (let page = 0; page < 30; page++) {
+    const pg = (await gqlRetry(Q, { cur })).json.data.products;
+    for (const n of pg.nodes) {
+      total++;
+      if (widthMf(n.metafields.nodes)) continue;
+      missing++;
+      // derive mfr from any variant sku; strip -Sample then the -OSB vendor suffix
+      const skus = n.variants.nodes.map(v => v.sku || '').filter(Boolean);
+      const baseSku = skus.find(s => /-OSB$/i.test(s)) || skus[0] || '';
+      const mfr = baseSku.replace(/-sample$/i, '').replace(/-OSB$/i, '').toUpperCase();
+      const width = cat.get(mfr);
+      if (!width) { noCat++; continue; }
+      linkable++;
+      const row = { width, material: '', roll_length: '', pattern_repeat: '', mfr_sku: mfr, dw_sku: '' };
+      const res = await pushSpecMetafields(n.id, row, n.metafields.nodes, gqlRetry, COMMIT);
+      if (res.errors) { errors++; console.log(`  ✗ ${mfr}: ${JSON.stringify(res.errors).slice(0, 80)}`); }
+      else if (COMMIT && res.pushed) { pushed++; if (pushed % 25 === 0) console.log(`  …pushed ${pushed} (latest ${mfr} = ${width})`); }
+      if (COMMIT) await sleep(120);
+    }
+    if (!pg.pageInfo.hasNextPage) break; cur = pg.pageInfo.endCursor;
+  }
+  console.log(`\n=== Osborne width backfill ${COMMIT ? '(COMMITTED)' : '(DRY-RUN)'} ===`);
+  console.log(`  active scanned    : ${total}`);
+  console.log(`  missing width     : ${missing}`);
+  console.log(`  linkable (catalog): ${linkable}`);
+  console.log(`  ${COMMIT ? 'PUSHED' : 'would push'}        : ${COMMIT ? pushed : linkable}`);
+  console.log(`  no catalog source : ${noCat}  (need re-scrape/enrich — not backfillable from catalog)`);
+  if (errors) console.log(`  errors            : ${errors}`);
+  console.log(`  cost              : $0 (Shopify API only)`);
+})().catch(e => { console.error('ERR:', e.message); process.exit(1); });

← 87886372 Live: pushed width+mfr_sku metafields to 3 Osborne actives (  ·  back to Designer Wallcoverings  ·  cadence: backfill+normalize widths to DW canonical 'N Inches 84e9e2e1 →