← back to Dw Domain Fleet
scripts/pull-catalog.js
101 lines
#!/usr/bin/env node
/**
* pull-catalog.js — pull the full live designerwallcoverings.com Shopify catalog
* into data/catalog.json. All 66 fleet sites filter from this single snapshot.
*
* Live /products.json image URLs are guaranteed 200 at fetch time (dw-site-build
* standing rule: dw_unified snapshots have ~97% stale image URLs).
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
// Canonical showroom-vendor primitive (list + logic in fix-live-board/config).
// Showroom-only vendors (e.g. Phillip Jeffries) are addressable-but-not-discoverable —
// they must never enter the shared fleet pool. Never hardcode a vendor name; edit
// showroom-vendors.json to change the set. TK-11186.
const { isShowroomVendor } = require(path.join(process.env.HOME, 'Projects/fix-live-board/config/showroom-vendor.cjs'));
// CATALOG_OUT lets a verification run write to a scratch path and diff before
// anything touches the file the fleet actually serves. Defaults to the real one.
const OUT = process.env.CATALOG_OUT || path.join(__dirname, '..', 'data', 'catalog.json');
// A variant is the memo sample (not the sellable good) when its SKU ends in
// -Sample/_Sample/" Sample" (DW's convention, case-varying: DWQC-600488-Sample,
// DWTT-81110-SAMPLE) or its variant title is exactly "Sample". Anchored so a
// pattern legitimately named e.g. "Sampler" is never swallowed.
function isSampleVariant(v) {
if (!v) return false;
return /(^|[-_ ])sample$/i.test(String(v.sku || '')) ||
/^sample$/i.test(String(v.title || '').trim());
}
function get(url) {
return new Promise((resolve, reject) => {
https.get(url, { headers: { 'User-Agent': 'dw-domain-fleet/1.0' } }, (res) => {
let body = '';
res.on('data', (c) => (body += c));
res.on('end', () => {
try { resolve(JSON.parse(body)); }
catch (e) { reject(new Error('bad JSON from ' + url)); }
});
}).on('error', reject);
});
}
// Exported so scripts/test-pull-catalog.js can exercise the variant-selection
// rule directly; guarded by require.main so importing it never fires a live pull.
function pickVariant(p) {
const variants = Array.isArray(p && p.variants) ? p.variants : [];
return variants.find(v => !isSampleVariant(v)) || variants[0] || {};
}
module.exports = { isSampleVariant, pickVariant };
// Only pull when run directly, so `require()` in tests never fires a live fetch.
if (require.main === module) (async () => {
const all = [];
for (let page = 1; page <= 60; page++) {
const url = `https://designerwallcoverings.com/products.json?limit=250&page=${page}`;
let data;
try { data = await get(url); }
catch (e) { console.error(`page ${page}: ${e.message}`); break; }
const prods = (data && data.products) || [];
if (!prods.length) break;
for (const p of prods) {
// Showroom-only vendors never enter the shared fleet catalog (TK-11186).
if (isShowroomVendor(p.vendor)) continue;
const img = (p.images && p.images[0] && p.images[0].src) || '';
// Flatten to the SELLABLE variant, not blindly to variants[0] (TK-11463).
// DW puts the $4.25 memo Sample at position 1 on most products, so
// variants[0] is frequently the sample and not the good being sold. Taking
// it verbatim recorded e.g. sku "DWQW-61260-Sample" / price "4.25" for a
// roll that actually sells at $150.43 — 903 products store-wide. That SKU
// is what the fleet card prints as the pattern's identity (render.js card()
// data-sku + the "Designer Wallcoverings · <sku>" meta line) and that price
// is what the mandated Price ↑/↓ sort orders on, so those rows advertised a
// sample's identity and sorted at $4.25 while selling for $150–$533.
// Prefer the first non-sample variant; fall back to variants[0] when EVERY
// variant is a sample, so the 3,585 genuinely sample-only products keep
// their correct -Sample SKU and $4.25 price rather than being mislabelled
// the other way.
const v0 = pickVariant(p);
all.push({
id: p.id,
title: p.title || '',
handle: p.handle || '',
vendor: p.vendor || '',
product_type: p.product_type || '',
tags: Array.isArray(p.tags) ? p.tags : String(p.tags || '').split(',').map(s => s.trim()).filter(Boolean),
image_url: img,
sku: v0.sku || '',
price: v0.price || '',
created_at: p.created_at || '',
updated_at: p.updated_at || ''
});
}
process.stdout.write(`page ${page}: +${prods.length} (total ${all.length})\r`);
await new Promise(r => setTimeout(r, 250));
}
fs.writeFileSync(OUT, JSON.stringify(all));
console.log(`\nWrote ${all.length} products → ${OUT}`);
})();