generated from wiz4apps/templates.OnlineShop
Implement the data layer from docs/PLAN.md §4 (card #55): - db/migrations/0001_init.sql: idempotent DDL for the 16 entities (category, product, product_category, product_image, product_variant, product_feature, cart, cart_item, customer, address, orders, order_item, payment, promo_code, content_page, delivery_option) with relations and indexes; explicit index on orders.number (product.slug/category.slug are unique-indexed by their keys). - Currency BYN (R4/Q1): STORE_CURRENCY = 'BYN', money as integer minor units (BYN * 100), no float, no interim currency, no FX conversion. - Idempotent seed: 14 categories (11 visible + 3 hidden visible=false, R13) and 93 products from docs/CATALOG.md §3 (broken #59 excluded, R14) with prices, old prices and badges; cross-listing, images, variants, PDP fields (R9), store constants (CATALOG §6), 1 admin + 1 customer and 2 orders. - scripts/{migrate,seed,verify}.ts + src/db: CLI entry points and an in-process Postgres verification harness (satisfies `migrate` + `seed` on a clean clone). - db/seed/0001_seed.sql: generated single-file SQL for the platform Data Tables (`/db-forms`, run_sql) to converge tenant_1 without a Node process. - Placeholder dev server + storefront currency/constants and money helpers. Verified on a clean Postgres (PGlite): product = 93; category = 14 with visible = 11; orders = 2; money integer BYN minor units (0 float rows).
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
import { createServer } from 'node:http';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { extname, join, normalize } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
/**
|
|
* Minimal placeholder dev server for the data-layer stage.
|
|
*
|
|
* The Next.js application shell is a later layer (docs/PLAN.md §8, cards for the
|
|
* backend/frontend); until it lands this keeps the project's dev container green.
|
|
* It serves `public/` and falls back to index.html.
|
|
*/
|
|
const PUBLIC_DIR = fileURLToPath(new URL('../public', import.meta.url));
|
|
|
|
function readPort(): number {
|
|
const inline = process.argv.find((arg) => arg.startsWith('--port='));
|
|
if (inline) return Number(inline.split('=')[1]);
|
|
const idx = process.argv.indexOf('--port');
|
|
if (idx >= 0 && process.argv[idx + 1]) return Number(process.argv[idx + 1]);
|
|
return Number(process.env.PORT ?? 3000);
|
|
}
|
|
|
|
const CONTENT_TYPES: Record<string, string> = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.svg': 'image/svg+xml',
|
|
'.json': 'application/json; charset=utf-8',
|
|
};
|
|
|
|
const server = createServer(async (req, res) => {
|
|
const requestPath = decodeURIComponent((req.url ?? '/').split('?')[0]);
|
|
const relative = normalize(requestPath === '/' ? '/index.html' : requestPath).replace(/^(\.\.[/\\])+/, '');
|
|
const filePath = join(PUBLIC_DIR, relative);
|
|
try {
|
|
const body = await readFile(filePath);
|
|
res.writeHead(200, { 'content-type': CONTENT_TYPES[extname(filePath)] ?? 'application/octet-stream' });
|
|
res.end(body);
|
|
} catch {
|
|
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
res.end('Not found');
|
|
}
|
|
});
|
|
|
|
const port = readPort();
|
|
server.listen(port, () => {
|
|
console.log(`La Rose data-layer placeholder server listening on http://localhost:${port}`);
|
|
});
|