[object Object]

← 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 →