← back to All Designerwallcoverings
add internal /api/catalog-full feed (every non-archived SKU, leak-sanitized, product_id/handle/tags) for substitute + photo apps
aa87b85eaa4df4918fe832be0a1e764f9fd61f78 · 2026-07-08 09:00:14 -0700 · Steve Abrams
Files touched
Diff
commit aa87b85eaa4df4918fe832be0a1e764f9fd61f78
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 8 09:00:14 2026 -0700
add internal /api/catalog-full feed (every non-archived SKU, leak-sanitized, product_id/handle/tags) for substitute + photo apps
---
server.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 72 insertions(+), 1 deletion(-)
diff --git a/server.js b/server.js
index 42cf4a3..c6642d9 100644
--- a/server.js
+++ b/server.js
@@ -71,6 +71,27 @@ const slugify = (v) => String(v || '').toLowerCase().normalize('NFD')
// whose vendor OR title leaks one are excluded (their products surface under the label).
const BANNED = /brewster|york|wallquest|wall\s*quest|newwall|new\s*wall\b|command\s*54|justin\s*david|nicolette\s*mayer|seabrook|chesapeake|nextwall|desima|carlsten/i;
+// ── INTERNAL full-catalog feed sanitizer (/api/catalog-full) ──────────────────────
+// The internal feed (auth-gated) INCLUDES private-label products so the substitute + photo
+// tools see EVERY SKU — but the real upstream manufacturer name must NEVER be emitted, even
+// into a client letter or a public photo page (Steve HARD rule, dw-leak-scanner). So instead
+// of BLUNT-excluding those rows like the public /api/products does, we map each genuine upstream
+// brand to the private label DW actually sells it under, then strip any residual upstream token.
+// Confident label maps only (memory: WallQuest-family→Malibu, Command54/JustinDavid→Phillipe Romano).
+// NB: bare "york" is deliberately NOT a strip token — "New York" in a legit title is not a leak.
+const LABEL_MAP = [
+ [/brewster\s*&?\s*york|brewster|wall\s*quest|wallquest|chesapeake|nextwall|new\s*wall\b|seabrook/gi, 'Malibu Wallpaper'],
+ [/command\s*54|justin\s*david/gi, 'Phillipe Romano'],
+];
+const LEAK_TOKENS = /\b(brewster|wallquest|wall\s*quest|chesapeake|nextwall|newwall|new\s*wall|seabrook|command\s*54|justin\s*david|nicolette\s*mayer|desima|carlsten)\b/gi;
+function sanitizeName(s) {
+ if (!s) return s;
+ let out = String(s);
+ for (const [re, label] of LABEL_MAP) out = out.replace(re, label);
+ out = out.replace(LEAK_TOKENS, '').replace(/\s{2,}/g, ' ').replace(/\s*&\s*$/, '').trim();
+ return out || null;
+}
+
// ── LIVE stock (metered vendor-portal scrape) — the paid /livestock --live path ────
// Distinct from the FREE stored STOCK snapshot. SELF-GATE: a display-vendor can fire live ONLY
// when data/live-stock-coverage.json classifies it live_capable with a wired adapter. That file
@@ -151,6 +172,7 @@ async function snapSql() {
}
let ROWS = [];
+let ROWS_INTERNAL = []; // internal full-catalog feed (every non-archived SKU, leak-sanitized)
let LOADED_AT = null;
let pool = null;
let STOCK = new Map(); // upper(sku) → public-safe stored stock snapshot (the /livestock FREE path)
@@ -314,8 +336,24 @@ async function loadSnapshot() {
ROWS = res.rows
.filter((r) => !BANNED.test(r.vendor || '') && !BANNED.test(r.title || ''))
.map(deriveRow);
+ // INTERNAL feed — EVERY non-archived SKU (incl. private-label, NO banned-exclusion), leak-sanitized,
+ // plus the extra fields internal consumers (photo + substitute apps) need but the public API omits:
+ // product_id (numeric, for Shopify photo push), handle (for product-page URLs), tags (for material/
+ // color/style re-derivation). Archived excluded per the internal-tools scope.
+ ROWS_INTERNAL = res.rows
+ .map((r) => {
+ const d = deriveRow(r);
+ if (d.lifecycle === 'Archived') return null;
+ const { hay, ...rest } = d;
+ const pid = (String(r.shopify_id || '').match(/(\d+)\s*$/) || [])[1] || null;
+ return {
+ ...rest, product_id: pid, handle: r.handle || null, tags: r.tags || '',
+ vendor: sanitizeName(rest.vendor), title: sanitizeName(rest.title), pattern: sanitizeName(rest.pattern),
+ };
+ })
+ .filter(Boolean);
LOADED_AT = new Date().toISOString();
- console.log(`snapshot: ${ROWS.length.toLocaleString()} products in ${Date.now() - t0}ms (stock ${STOCK.size.toLocaleString()} · viewers ${VENDOR_VIEWER.size})`);
+ console.log(`snapshot: ${ROWS.length.toLocaleString()} public + ${ROWS_INTERNAL.length.toLocaleString()} internal products in ${Date.now() - t0}ms (stock ${STOCK.size.toLocaleString()} · viewers ${VENDOR_VIEWER.size})`);
}
// ── filtering / sorting (japan-viewer pattern) ──────────────────────────────────
@@ -401,6 +439,20 @@ function loadMicrosites() {
return micrositesCache;
}
+// Internal full-catalog payload — serialized ONCE per snapshot (keyed on LOADED_AT) and gzipped
+// lazily, so serving the ~82k-row feed to the photo/substitute apps costs ~nothing on repeat pulls.
+let INTERNAL_CACHE = { at: null, json: null, gz: null };
+function internalPayload() {
+ if (INTERNAL_CACHE.at !== LOADED_AT) {
+ INTERNAL_CACHE = {
+ at: LOADED_AT,
+ json: Buffer.from(JSON.stringify({ total: ROWS_INTERNAL.length, loaded_at: LOADED_AT, rows: ROWS_INTERNAL })),
+ gz: null,
+ };
+ }
+ return INTERNAL_CACHE;
+}
+
// ── LIVE stock endpoint — /api/livestock?sku=<sku>[&live=1] ─────────────────────────
// FREE default: the stored STOCK snapshot ($0, never a portal hit). live=1: the METERED
// vendor-portal scrape (spawns scripts/live-scrape.js). Self-gates on a wired adapter,
@@ -577,6 +629,25 @@ const server = http.createServer((req, res) => {
return;
}
+ // ── INTERNAL full-catalog feed ─────────────────────────────────────────────────
+ // Auth-gated (the whole server is). The single source of truth the substitute + photo
+ // apps consume so they see EVERY non-archived SKU in dw_unified — not just their own line —
+ // with the same enrichment (colors/styles/materials/price_band/lifecycle/stock) plus the
+ // internal-only product_id/handle/tags. Private-label names are leak-sanitized upstream.
+ if (u.pathname === '/api/catalog-full') {
+ const c = internalPayload();
+ const wantsGzip = /\bgzip\b/.test(req.headers['accept-encoding'] || '');
+ if (wantsGzip) {
+ if (!c.gz) { try { c.gz = zlib.gzipSync(c.json); } catch { c.gz = null; } }
+ if (c.gz) {
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Encoding': 'gzip' });
+ return res.end(c.gz);
+ }
+ }
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+ return res.end(c.json);
+ }
+
if (u.pathname === '/api/facets') {
const f = parseFilters(u);
res.writeHead(200, { 'Content-Type': 'application/json' });
← 5dfa17e deploy: infinite-scroll fix (5cdaf8a) live to all.designerwa
·
back to All Designerwallcoverings
·
Designtex dw_sku backfill on Kamatera dw_unified: backup + r 104aa8f →