← back to Dw Internal Gateway
initial scaffold: hostname-routed internal vendor gateway (<vendor>.internal.dw)
3ed16345c1e9e17e2a3ecc47846c4c04bf34eafe · 2026-07-23 11:10:12 -0700 · Steve Abrams
Files touched
A .gitignoreA package-lock.jsonA package.jsonA routes.jsonA server.js
Diff
commit 3ed16345c1e9e17e2a3ecc47846c4c04bf34eafe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 23 11:10:12 2026 -0700
initial scaffold: hostname-routed internal vendor gateway (<vendor>.internal.dw)
---
.gitignore | 8 ++
package-lock.json | 162 +++++++++++++++++++++++++++++++++++
package.json | 16 ++++
routes.json | 17 ++++
server.js | 249 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 452 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..04835e3
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,162 @@
+{
+ "name": "dw-internal-gateway",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dw-internal-gateway",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "pg": "^8.22.0"
+ }
+ },
+ "node_modules/pg": {
+ "version": "8.22.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
+ "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.14.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.15.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.4.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+ "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
+ "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz",
+ "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..025d0bb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "dw-internal-gateway",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "commonjs",
+ "dependencies": {
+ "pg": "^8.22.0"
+ }
+}
diff --git a/routes.json b/routes.json
new file mode 100644
index 0000000..8c459ba
--- /dev/null
+++ b/routes.json
@@ -0,0 +1,17 @@
+{
+ "comment": "Dedicated internal viewers: <slug>.internal.dw -> 127.0.0.1:<port>. Any slug NOT listed here falls through to the generic dw_unified <slug>_catalog viewer.",
+ "routes": {
+ "crezana": 10072,
+ "fromental": 10071,
+ "gracie": 10073,
+ "greenland": 9973,
+ "schumacher": 9946,
+ "zuber": 10074,
+ "japan": 9931,
+ "rigo": 9979,
+ "fentucci": 9981,
+ "whimsical": 9787,
+ "pattern-vault": 9779,
+ "domains": 9784
+ }
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..920b470
--- /dev/null
+++ b/server.js
@@ -0,0 +1,249 @@
+// dw-internal-gateway — hostname-routed internal vendor sites.
+//
+// <vendor>.internal.dw -> dedicated viewer (routes.json) OR generic
+// catalog viewer over dw_unified.<vendor>_catalog
+// internal.dw -> index of every vendor
+//
+// Internal-only: binds 127.0.0.1. Basic auth admin/DW2024! at the edge;
+// Authorization is forwarded so downstream apps using the same creds work.
+
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+
+const PORT = Number(process.env.PORT || 80);
+const BIND = process.env.IGW_BIND || '127.0.0.1'; // loopback-only; ambient $BIND must not flip this public
+const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
+const ROUTES = JSON.parse(fs.readFileSync(path.join(__dirname, 'routes.json'), 'utf8')).routes;
+
+const pool = new Pool({ host: '/tmp', database: 'dw_unified', max: 5 });
+
+// ---------- column auto-detection over heterogeneous *_catalog tables ----------
+const CANDIDATES = {
+ title: ['pattern_name', 'product_name', 'title', 'name', 'pattern', 'design_name', 'style_name'],
+ sku: ['sku', 'dw_sku', 'mfr_sku', 'item_number', 'product_code', 'sku_code', 'pattern_number', 'handle'],
+ image: ['image_url', 'image', 'img_url', 'image_src', 'thumbnail', 'thumbnail_url', 'main_image', 'primary_image'],
+ price: ['dw_sell_price', 'sell_price', 'retail_price', 'map_price', 'price', 'msrp'],
+ cost: ['cost_price', 'whls_cost', 'cost', 'wholesale_price'],
+ color: ['colorway', 'color', 'colour', 'color_name'],
+ created: ['created_at', 'scraped_at', 'imported_at', 'inserted_at', 'updated_at'],
+};
+
+const tableCache = new Map(); // slug -> {table, cols:{...}} | null
+async function resolveTable(slug) {
+ if (tableCache.has(slug)) return tableCache.get(slug);
+ const table = slug.replace(/-/g, '_') + '_catalog';
+ if (!/^[a-z0-9_]+$/.test(table)) return null;
+ const { rows } = await pool.query(
+ `select column_name, data_type from information_schema.columns
+ where table_schema='public' and table_name=$1`, [table]);
+ let info = null;
+ if (rows.length) {
+ const names = rows.map(r => r.column_name);
+ const cols = {};
+ for (const [role, cands] of Object.entries(CANDIDATES)) {
+ cols[role] = cands.find(c => names.includes(c)) || null;
+ }
+ info = { table, cols };
+ }
+ tableCache.set(slug, info);
+ return info;
+}
+
+const q = (id) => '"' + id.replace(/"/g, '""') + '"';
+
+async function apiProducts(info, url) {
+ const { table, cols } = info;
+ const page = Math.max(0, parseInt(url.searchParams.get('page') || '0', 10) || 0);
+ const limit = 120;
+ const sort = url.searchParams.get('sort') || 'newest';
+ const search = (url.searchParams.get('q') || '').trim();
+
+ const sel = [];
+ for (const [role, col] of Object.entries(cols)) if (col) sel.push(`${q(col)} as ${role}`);
+ if (!sel.length) sel.push('*');
+
+ const params = [];
+ let where = '';
+ if (search) {
+ const like = [];
+ for (const role of ['title', 'sku', 'color']) {
+ if (cols[role]) { params.push('%' + search + '%'); like.push(`${q(cols[role])}::text ilike $${params.length}`); }
+ }
+ if (like.length) where = 'where ' + like.join(' or ');
+ }
+
+ let order = '';
+ if (sort === 'newest' && cols.created) order = `order by ${q(cols.created)} desc nulls last`;
+ else if (sort === 'sku' && cols.sku) order = `order by ${q(cols.sku)} asc`;
+ else if (sort === 'title' && cols.title) order = `order by ${q(cols.title)} asc`;
+ else if (sort === 'price_asc' && cols.price) order = `order by ${q(cols.price)} asc nulls last`;
+ else if (sort === 'price_desc' && cols.price) order = `order by ${q(cols.price)} desc nulls last`;
+ else if (cols.created) order = `order by ${q(cols.created)} desc nulls last`;
+
+ const sql = `select ${sel.join(', ')} from ${q(table)} ${where} ${order} limit ${limit} offset ${page * limit}`;
+ const { rows } = await pool.query(sql, params);
+ const { rows: cnt } = await pool.query(`select count(*)::int as n from ${q(table)} ${where}`, params);
+ return { total: cnt[0].n, page, limit, hasPrice: !!cols.price, hasCreated: !!cols.created, products: rows };
+}
+
+// ---------- HTML ----------
+function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
+
+function viewerPage(slug, info, total) {
+ const name = slug.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>${esc(name)} — internal.dw</title>
+<style>
+:root{--min:220px}
+body{font-family:-apple-system,Helvetica,Arial,sans-serif;margin:0;background:#f5f4f1;color:#1e2a3a}
+header{background:#1e2a3a;color:#fff;padding:14px 22px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
+header h1{font-size:17px;margin:0;font-weight:600}header a{color:#9fc2e8;text-decoration:none;font-size:13px}
+header .count{opacity:.7;font-size:13px}
+.controls{display:flex;gap:14px;align-items:center;padding:12px 22px;background:#fff;border-bottom:1px solid #e2ded7;position:sticky;top:0;z-index:5;flex-wrap:wrap}
+.controls label{font-size:12px;color:#666;display:flex;align-items:center;gap:6px}
+select,input[type=search]{padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px}
+input[type=range]{width:140px}
+#grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--min),1fr));gap:14px;padding:18px 22px}
+.card{background:#fff;border:1px solid #e2ded7;border-radius:10px;overflow:hidden;display:flex;flex-direction:column}
+.card img{width:100%;aspect-ratio:1;object-fit:cover;background:#eee;display:block}
+.card .noimg{width:100%;aspect-ratio:1;background:repeating-linear-gradient(45deg,#eee,#eee 12px,#f7f7f7 12px,#f7f7f7 24px)}
+.card .body{padding:10px 12px;font-size:13px;display:flex;flex-direction:column;gap:4px}
+.card .t{font-weight:600;line-height:1.25}
+.card .sku{color:#777;font-size:12px}
+.card .price{color:#1e2a3a;font-weight:600}
+.card .when{color:#8a8477;font-size:11px}
+#more{display:block;margin:10px auto 40px;padding:10px 26px;font-size:14px;border:1px solid #1e2a3a;background:#fff;border-radius:8px;cursor:pointer}
+</style></head><body>
+<header><h1>${esc(name)}</h1><span class="count" id="count">${total.toLocaleString()} products</span><a href="//internal.dw${PORT === 80 ? '' : ':' + PORT}/">← all vendors</a></header>
+<div class="controls">
+ <label>Sort <select id="sort">
+ <option value="newest">Newest</option>
+ <option value="sku">SKU A→Z</option>
+ <option value="title">Title A→Z</option>
+ <option value="price_asc">Price ↑</option>
+ <option value="price_desc">Price ↓</option>
+ </select></label>
+ <label>Density <input type="range" id="density" min="120" max="420" step="20"></label>
+ <label>Search <input type="search" id="q" placeholder="pattern, sku, color…"></label>
+</div>
+<div id="grid"></div>
+<button id="more" hidden>Load more</button>
+<script>
+const LS='igw:'+location.host;
+const sortEl=document.getElementById('sort'),denEl=document.getElementById('density'),qEl=document.getElementById('q');
+const grid=document.getElementById('grid'),more=document.getElementById('more'),count=document.getElementById('count');
+sortEl.value=localStorage.getItem(LS+':sort')||'newest';
+denEl.value=localStorage.getItem(LS+':density')||'220';
+document.documentElement.style.setProperty('--min',denEl.value+'px');
+let page=0,busy=false,total=0;
+const fmt=iso=>{if(!iso)return'';const d=new Date(iso);return isNaN(d)?'':d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})};
+async function load(reset){
+ if(busy)return;busy=true;
+ if(reset){page=0;grid.innerHTML='';}
+ const r=await fetch('/api/products?sort='+sortEl.value+'&page='+page+'&q='+encodeURIComponent(qEl.value));
+ const d=await r.json();total=d.total;
+ count.textContent=total.toLocaleString()+' products';
+ for(const p of d.products){
+ const c=document.createElement('div');c.className='card';
+ c.innerHTML=(p.image?'<img loading="lazy" src="'+p.image.replace(/"/g,'"')+'">':'<div class="noimg"></div>')+
+ '<div class="body"><div class="t"></div><div class="sku"></div>'+
+ (p.price!=null?'<div class="price">$'+Number(p.price).toFixed(2)+'</div>':'')+
+ (p.created?'<div class="when" title="'+p.created+'">🕓 '+fmt(p.created)+'</div>':'')+'</div>';
+ c.querySelector('.t').textContent=p.title||'(untitled)';
+ c.querySelector('.sku').textContent=[p.sku,p.color].filter(Boolean).join(' · ');
+ grid.appendChild(c);
+ }
+ page++;more.hidden=grid.children.length>=total;busy=false;
+}
+sortEl.onchange=()=>{localStorage.setItem(LS+':sort',sortEl.value);load(true)};
+denEl.oninput=()=>{localStorage.setItem(LS+':density',denEl.value);document.documentElement.style.setProperty('--min',denEl.value+'px')};
+let t;qEl.oninput=()=>{clearTimeout(t);t=setTimeout(()=>load(true),300)};
+more.onclick=()=>load(false);
+load(true);
+</script></body></html>`;
+}
+
+async function indexPage() {
+ const { rows } = await pool.query(`
+ select t.table_name, coalesce(c.reltuples,0)::bigint as est
+ from information_schema.tables t
+ left join pg_class c on c.relname = t.table_name and c.relkind='r'
+ where t.table_schema='public' and t.table_name like '%\\_catalog'
+ order by t.table_name`);
+ const suffix = PORT === 80 ? '' : ':' + PORT;
+ const items = rows.map(r => {
+ const slug = r.table_name.replace(/_catalog$/, '').replace(/_/g, '-');
+ const dedicated = ROUTES[slug] ? ' <span class="ded">dedicated :' + ROUTES[slug] + '</span>' : '';
+ return `<li><a href="//${slug}.internal.dw${suffix}/">${esc(slug)}.internal.dw</a> <span class="n">~${Number(r.est).toLocaleString()}</span>${dedicated}</li>`;
+ });
+ const extra = Object.entries(ROUTES)
+ .filter(([slug]) => !rows.some(r => r.table_name === slug.replace(/-/g, '_') + '_catalog'))
+ .map(([slug, port]) => `<li><a href="//${slug}.internal.dw${suffix}/">${esc(slug)}.internal.dw</a> <span class="ded">dedicated :${port}</span></li>`);
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>internal.dw — vendor index</title>
+<style>body{font-family:-apple-system,Helvetica,Arial,sans-serif;background:#f5f4f1;color:#1e2a3a;margin:0}
+header{background:#1e2a3a;color:#fff;padding:16px 24px}h1{font-size:18px;margin:0}
+ul{columns:3;list-style:none;padding:20px 24px;margin:0;font-size:14px;line-height:1.9}
+a{color:#1e2a3a;text-decoration:none}a:hover{text-decoration:underline}
+.n{color:#999;font-size:12px}.ded{color:#2e7d32;font-size:11px;border:1px solid #2e7d32;border-radius:4px;padding:0 4px}
+@media(max-width:900px){ul{columns:2}}@media(max-width:600px){ul{columns:1}}</style></head>
+<body><header><h1>internal.dw — ${rows.length + extra.length} vendor internal sites</h1></header>
+<ul>${extra.join('')}${items.join('')}</ul></body></html>`;
+}
+
+// ---------- proxy ----------
+function proxy(req, res, port) {
+ const opts = {
+ host: '127.0.0.1', port, method: req.method, path: req.url,
+ headers: { ...req.headers, host: '127.0.0.1:' + port },
+ };
+ const up = http.request(opts, (ur) => { res.writeHead(ur.statusCode, ur.headers); ur.pipe(res); });
+ up.on('error', () => { res.writeHead(502, { 'content-type': 'text/plain' }); res.end('upstream on :' + port + ' is down'); });
+ req.pipe(up);
+}
+
+// ---------- server ----------
+const server = http.createServer(async (req, res) => {
+ try {
+ if (req.url === '/healthz') { res.writeHead(200); return res.end('ok'); }
+
+ if (req.headers.authorization !== AUTH) {
+ res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="internal.dw"' });
+ return res.end('auth required');
+ }
+
+ const host = (req.headers.host || '').split(':')[0].toLowerCase();
+ const m = host.match(/^(?:([a-z0-9-]+)\.)?internal\.dw$/);
+ const slug = m ? m[1] : null;
+
+ if (!m || !slug || slug === 'www') {
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
+ return res.end(await indexPage());
+ }
+
+ if (ROUTES[slug]) return proxy(req, res, ROUTES[slug]);
+
+ const info = await resolveTable(slug);
+ if (!info) {
+ res.writeHead(404, { 'content-type': 'text/plain' });
+ return res.end(`no dedicated viewer and no table ${slug.replace(/-/g, '_')}_catalog in dw_unified`);
+ }
+
+ const url = new URL(req.url, 'http://x');
+ if (url.pathname === '/api/products') {
+ const data = await apiProducts(info, url);
+ res.writeHead(200, { 'content-type': 'application/json' });
+ return res.end(JSON.stringify(data));
+ }
+ const { rows } = await pool.query(`select count(*)::int as n from ${q(info.table)}`);
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
+ return res.end(viewerPage(slug, info, rows[0].n));
+ } catch (e) {
+ res.writeHead(500, { 'content-type': 'text/plain' });
+ res.end('gateway error: ' + e.message);
+ }
+});
+
+server.listen(PORT, BIND, () => console.log(`dw-internal-gateway on http://${BIND}:${PORT}`));
(oldest)
·
back to Dw Internal Gateway
·
loopback-only bind, port 8090, pm2-managed dnsmasq wildcard 3f1098b →