Files
dashfront/test/dummyBackend.mjs
2026-10-08 04:45:35 +02:00

522 lines
25 KiB
JavaScript

/**
* Dummy backend for the visual testing rig.
*
* Serves the frozen fixture snapshot (test/fixtures/snapshot.json) on every
* GET endpoint the frontend reads during screenshot runs. Always-authed stub:
* every request is treated as the fixture user, so headless --screenshot runs
* need no cookie juggling. (There is no logged-out/login screenshot.)
*
* Mutating methods (POST/PATCH/PUT/DELETE) return the stored fixture payload
* (or a minimal echo) so UI actions don't crash mid-run — persistence is
* in-memory only and resets on restart.
*
* Usage: node test/dummyBackend.mjs [--port 4218]
*/
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE_DIR = path.join(__dirname, 'fixtures');
const IMAGE_DIR = path.join(FIXTURE_DIR, 'images');
const portArg = process.argv.find((a) => a.startsWith('--port='))?.split('=')[1];
const PORT = Number(portArg ?? process.env.RIG_BACKEND_PORT ?? 4208);
function loadSnapshot() {
const file = path.join(FIXTURE_DIR, 'snapshot.json');
if (!fs.existsSync(file)) {
console.error(
`[dummy-backend] missing ${file}. Run: npm run rig:collect (live backend at :4208 required)`,
);
process.exit(1);
}
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
const snap = loadSnapshot();
const { auth, company, catalog, parties, documents, misc } = snap;
const googleClientId = snap.meta?.googleClientId ?? null;
const user = auth.me.user;
function json(res, status, body) {
const text = JSON.stringify(body);
res.writeHead(status, {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(text),
'Cache-Control': 'no-store',
});
res.end(text);
}
function readBody(req) {
return new Promise((resolve) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
if (!raw) return resolve(null);
try {
resolve(JSON.parse(raw));
} catch {
resolve(raw);
}
});
});
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, 'http://localhost');
const p = url.pathname;
const method = req.method;
// Public / auth bootstrap. The dummy is always-authed: every request is
// treated as the fixture user, so headless --screenshot runs need no
// cookie juggling. (There is no logged-out/login screenshot in the rig.)
if (p === '/api/health') return json(res, 200, { ok: true });
if (p === '/api/auth/me' && method === 'GET') {
return json(res, 200, { user, googleClientId });
}
if (p === '/api/auth/login' && method === 'POST') {
await readBody(req);
res.setHeader('Set-Cookie', 'token=dummy-rig-token; Path=/; HttpOnly');
return json(res, 200, { message: 'Login successful', user });
}
if (p === '/api/auth/logout' && method === 'POST') {
res.setHeader('Set-Cookie', 'token=; Path=/; HttpOnly; Max-Age=0');
return json(res, 200, { ok: true });
}
if (p === '/api/auth/selected-company' && method === 'PUT') {
return json(res, 200, { user });
}
// Everything else is served as the fixture user (always-authed stub).
// SSE: hold the stream open with heartbeats, never emit (frozen fixtures).
if (p === '/api/events' && method === 'GET') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
res.write(': connected\n\n');
const beat = setInterval(() => {
try {
res.write(': heartbeat\n\n');
} catch {
clearInterval(beat);
}
}, 20000);
req.on('close', () => clearInterval(beat));
return;
}
// Binary images (byte-identical to the live backend at collect time).
// Backend fullHash is an 8-char alphanumeric string (see dashback pic.js).
const thumbMatch = p.match(/^\/api\/thumb\/([A-Za-z0-9]{8})$/);
if (thumbMatch && method === 'GET') {
const file = path.join(IMAGE_DIR, `${thumbMatch[1]}.avif`);
if (!fs.existsSync(file)) return json(res, 404, { error: 'Image not found' });
const buf = fs.readFileSync(file);
res.writeHead(200, {
'Content-Type': 'image/avif',
'Content-Length': buf.length,
'Cache-Control': 'private, max-age=31536000, immutable',
ETag: `"${thumbMatch[1]}"`,
});
return res.end(buf);
}
const imageMatch = p.match(/^\/api\/image\/([A-Za-z0-9]{8})$/);
if (imageMatch && method === 'GET') {
const file = path.join(IMAGE_DIR, `${imageMatch[1]}.avif`);
if (!fs.existsSync(file)) return json(res, 404, { error: 'Image not found' });
const buf = fs.readFileSync(file);
res.writeHead(200, {
'Content-Type': 'image/avif',
'Content-Length': buf.length,
'Cache-Control': 'private, max-age=31536000, immutable',
ETag: `"${imageMatch[1]}"`,
});
return res.end(buf);
}
// ── Reads ──────────────────────────────────────────────
if (method === 'GET') {
if (p === '/api/company') return json(res, 200, { companies: auth.companies });
if (p === '/api/category/tree') return json(res, 200, catalog.tree);
const catArticles = p.match(/^\/api\/category\/(\d+)\/articles$/);
if (catArticles) {
return json(res, 200, {
articles: catalog.articlesByCategory[catArticles[1]] ?? [],
});
}
const articleMatch = p.match(/^\/api\/article\/([^/]+)$/);
if (articleMatch && !['search', 'history', 'export.zip'].includes(articleMatch[1])) {
const sku = decodeURIComponent(articleMatch[1]);
if (sku === catalog.articleDetail?.sku) return json(res, 200, { article: catalog.articleDetail });
// Any other SKU: clone the known detail so deep links still render.
if (catalog.articleDetail) {
return json(res, 200, {
article: { ...catalog.articleDetail, sku, name: catalog.articleDetail.name },
});
}
const catArticle = Object.values(catalog.articlesByCategory ?? {}).flat().find((a) => a.sku === sku);
if (catArticle) {
return json(res, 200, {
article: {
id: catArticle.id,
sku: catArticle.sku,
name: catArticle.name,
unitamount: 1,
unit: { si: 'stk', name: 'Stück' },
prices: [{ minmultiplier: 1, net: 10, taxRate: 19, customergroupId: 1 }],
categoryIds: [],
photos: catArticle.fullHash ? [{ fullHash: catArticle.fullHash, thumbHash: catArticle.thumbHash }] : [],
children: [],
},
});
}
return json(res, 404, { error: 'Article not found' });
}
if (p === '/api/article/history') return json(res, 200, { history: misc.articleHistoryGrid ?? [] });
const skuHistory = p.match(/^\/api\/article\/([^/]+)\/history$/);
if (skuHistory) return json(res, 200, { history: catalog.articleHistory ?? [] });
const skuSuppliers = p.match(/^\/api\/article\/([^/]+)\/supplier$/);
if (skuSuppliers) return json(res, 200, { suppliers: catalog.articleSuppliers ?? [] });
const skuChildren = p.match(/^\/api\/article\/([^/]+)\/children$/);
if (skuChildren) return json(res, 200, { children: [] });
const skuAttr = p.match(/^\/api\/article\/([^/]+)\/attributevalue$/);
if (skuAttr) return json(res, 200, { attributevalues: [] });
const skuProp = p.match(/^\/api\/article\/([^/]+)\/property$/);
if (skuProp) return json(res, 200, { properties: [] });
const skuArticlelist = p.match(/^\/api\/article\/([^/]+)\/articlelist$/);
if (skuArticlelist) return json(res, 200, { items: [] });
const skuOptions = p.match(/^\/api\/article\/([^/]+)\/optionslist$/);
if (skuOptions) return json(res, 200, { options: [] });
if (p === '/api/brand') return json(res, 200, { brands: catalog.brands ?? [] });
if (p === '/api/unit') return json(res, 200, { units: catalog.units ?? [] });
if (p === '/api/warehouse') return json(res, 200, { warehouses: catalog.warehouses ?? [] });
const warehouseById = p.match(/^\/api\/warehouse\/(\d+)$/);
if (warehouseById) {
const found = (catalog.warehouses ?? []).find((w) => String(w.id) === warehouseById[1]);
if (!found) return json(res, 404, { error: 'Warehouse not found' });
return json(res, 200, { warehouse: found });
}
if (p === '/api/attributename') return json(res, 200, { attributenames: catalog.attributes ?? [] });
const attrValues = p.match(/^\/api\/attributename\/(\d+)\/value$/);
if (attrValues) {
const attr = (catalog.attributes ?? []).find((a) => String(a.id) === attrValues[1]);
return json(res, 200, { values: attr?.values ?? [] });
}
if (p === '/api/propertyname') return json(res, 200, { propertynames: catalog.properties ?? [] });
if (p === '/api/supplier') return json(res, 200, { suppliers: parties.suppliers ?? [] });
const supplierArticles = p.match(/^\/api\/supplier\/(\d+)\/articles$/);
if (supplierArticles) return json(res, 200, { articles: [] });
if (p === '/api/entity') return json(res, 200, { entities: parties.entities ?? [] });
if (p === '/api/entity/next-number') return json(res, 200, { number: 'KD-001001' });
const entityMatch = p.match(/^\/api\/entity\/(\d+)$/);
if (entityMatch) {
const found = (parties.entities ?? []).find((e) => String(e.id) === entityMatch[1]);
if (!found && parties.entityDetail && String(parties.entityDetail.id) === entityMatch[1]) {
return json(res, 200, { entity: parties.entityDetail });
}
if (!found) return json(res, 404, { error: 'Entity not found' });
return json(res, 200, { entity: found });
}
const entityAddr = p.match(/^\/api\/entity\/(\d+)\/address$/);
if (entityAddr) return json(res, 200, { addresses: parties.entityAddresses ?? [] });
const entityContact = p.match(/^\/api\/entity\/(\d+)\/contact$/);
if (entityContact) return json(res, 200, { contacts: parties.entityContacts ?? [] });
if (p === '/api/customergroup') return json(res, 200, { customergroups: parties.customergroups ?? [] });
if (p === '/api/delivery') return json(res, 200, { deliveries: documents.deliveries ?? [] });
const deliveryMatch = p.match(/^\/api\/delivery\/(\d+)$/);
if (deliveryMatch) {
const found = (documents.deliveries ?? []).find((d) => String(d.id) === deliveryMatch[1]);
const detail = documents.deliveryDetail && String(documents.deliveryDetail.id) === deliveryMatch[1]
? documents.deliveryDetail
: null;
if (detail) return json(res, 200, { delivery: detail });
if (found && !detail) {
return json(res, 200, {
delivery: { ...found, lines: [], files: found.files ?? [] },
});
}
return json(res, 404, { error: 'Delivery not found' });
}
if (p === '/api/quote') return json(res, 200, { quotes: documents.quotes ?? [] });
const quoteMatch = p.match(/^\/api\/quote\/(\d+)$/);
if (quoteMatch) {
const found = (documents.quotes ?? []).find((q) => String(q.id) === quoteMatch[1]);
if (!found) return json(res, 404, { error: 'Quote not found' });
return json(res, 200, { quote: { ...found, lines: found.lines ?? [] } });
}
if (p === '/api/order') return json(res, 200, { orders: documents.orders ?? [] });
const orderMatch = p.match(/^\/api\/order\/(\d+)$/);
if (orderMatch) {
const found = (documents.orders ?? []).find((o) => String(o.id) === orderMatch[1]);
if (!found) return json(res, 404, { error: 'Order not found' });
return json(res, 200, { order: { ...found, lines: found.lines ?? [] } });
}
if (p === '/api/number') return json(res, 200, { numbers: documents.numbers ?? [] });
if (p === '/api/posterminal') return json(res, 200, { posterminals: misc.posterminals ?? [] });
if (p === '/api/printer') return json(res, 200, { printers: misc.printers ?? [] });
const printerById = p.match(/^\/api\/printer\/(\d+)$/);
if (printerById) {
const found = (misc.printers ?? []).find((x) => String(x.id) === printerById[1]);
if (!found) return json(res, 404, { error: 'Printer not found' });
return json(res, 200, { printer: found });
}
if (p === '/api/labeltemplate') return json(res, 200, { labeltemplates: misc.labeltemplates ?? [] });
const labelById = p.match(/^\/api\/labeltemplate\/(\d+)$/);
if (labelById) {
const found = (misc.labeltemplates ?? []).find((x) => String(x.id) === labelById[1]);
if (!found) return json(res, 404, { error: 'Label template not found' });
return json(res, 200, { labeltemplate: found });
}
if (p === '/api/company/setting') return json(res, 200, { settings: company.settings ?? [] });
const settingByKey = p.match(/^\/api\/company\/setting\/(.+)$/);
if (settingByKey) {
const found = (company.settings ?? []).find((s) => s.key === decodeURIComponent(settingByKey[1]));
if (!found) return json(res, 404, { error: 'Setting not found' });
return json(res, 200, { setting: found });
}
if (p === '/api/vfs') return json(res, 200, { files: misc.vfs ?? [] });
if (p === '/api/history') return json(res, 200, { history: misc.globalHistory ?? [], rows: misc.globalHistory ?? [] });
if (p === '/api/modelconversation') return json(res, 200, { conversations: [] });
const conversationMsgs = p.match(/^\/api\/modelconversation\/(\d+)\/message$/);
if (conversationMsgs) return json(res, 200, { messages: [] });
const ledgerSku = p.match(/^\/api\/ledger\/sku\/(.+)$/);
if (ledgerSku) return json(res, 200, { ledger: [] });
const ledgerMa = p.match(/^\/api\/ledger\/manifestarticle\/(\d+)$/);
if (ledgerMa) return json(res, 200, { ledger: [] });
if (p === '/api/ledger') return json(res, 200, { ledger: [] });
if (p === '/api/article/search') {
const q = (url.searchParams.get('target') ?? '').toLowerCase();
const all = Object.values(catalog.articlesByCategory ?? {}).flat();
const seen = new Map();
for (const a of all) seen.set(String(a.sku), a);
const rows = [...seen.values()].filter(
(a) => !q || String(a.sku).toLowerCase().includes(q) || String(a.name ?? '').toLowerCase().includes(q),
);
return json(res, 200, { articles: rows.slice(0, 50), total: rows.length });
}
return json(res, 404, { error: 'Not found' });
}
// ── Mutations: echo fixtures so the UI never crashes mid-run ──
const body = await readBody(req);
if (method === 'POST' && p === '/api/article') {
return json(res, 201, { article: { ...catalog.articleDetail, ...(body ?? {}) } });
}
const skuPatch = p.match(/^\/api\/article\/([^/]+)$/);
if (skuPatch && (method === 'PATCH' || method === 'PUT')) {
return json(res, 200, { article: { ...catalog.articleDetail, ...(body ?? {}) } });
}
if (method === 'DELETE' && p.match(/^\/api\/article\/[^/]+(\/image\/.+)?$/)) {
res.writeHead(204);
return res.end();
}
if (p.match(/^\/api\/article\/[^/]+\/(prices|weight|brand|eans|categories|images\/order|supplier\/order|attributevalue|property|articlelist|optionslist|parent|children|print)$/) && ['PUT', 'PATCH', 'POST'].includes(method)) {
if (p.endsWith('/supplier/order')) return json(res, 200, { ids: body?.ids ?? [] });
if (p.endsWith('/images/order')) return json(res, 200, { fullHashes: body?.fullHashes ?? [] });
if (p.endsWith('/articlelist')) return json(res, 200, { items: body?.items ?? [] });
if (p.endsWith('/optionslist')) return json(res, 200, { options: body?.options ?? [] });
if (p.endsWith('/attributevalue')) return json(res, 200, { attributevalues: [] });
if (p.endsWith('/property')) return json(res, 200, { properties: [] });
return json(res, 200, { article: { ...catalog.articleDetail, ...(body ?? {}) } });
}
if (p.match(/^\/api\/article\/[^/]+\/supplier(\/\d+)?$/) && ['POST', 'PATCH'].includes(method)) {
return json(res, method === 'POST' ? 201 : 200, {
supplier: { id: 1, entityId: 1, ...(body ?? {}) },
});
}
if (method === 'DELETE' && p.match(/^\/api\/article\/[^/]+\/(supplier|property|children)\/.+$/)) {
return json(res, 200, { ok: true });
}
if (p === '/api/category' && method === 'POST') {
return json(res, 201, { category: { id: 999001, parentId: body?.parentId ?? 1, sort: 0, ...(body ?? {}) } });
}
if (p.match(/^\/api\/category\/\d+$/) && ['PATCH', 'PUT'].includes(method)) {
const id = Number(p.split('/').pop());
return json(res, 200, { category: { id, ...(body ?? {}) } });
}
if (p === '/api/category/order' && method === 'PUT') {
return json(res, 200, { categoryIds: body?.categoryIds ?? [] });
}
if (method === 'DELETE' && p.match(/^\/api\/category\/\d+(\/image)?$/)) {
return json(res, 200, { category: { id: Number(p.split('/')[3]) } });
}
if (p === '/api/brand' && method === 'PUT') {
return json(res, 200, { brand: { id: 999001, ...(body ?? {}) } });
}
if (method === 'DELETE' && p.match(/^\/api\/brand\/[^/]+(\/image)?$/)) {
return json(res, 200, { brand: { id: 999001 } });
}
if (p === '/api/unit' && method === 'PUT') {
return json(res, 200, { unit: { ...(body ?? {}) } });
}
if (method === 'DELETE' && p.startsWith('/api/unit')) {
res.writeHead(204);
return res.end();
}
if (p === '/api/supplier' && method === 'POST') {
return json(res, 201, { supplier: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/supplier\/\d+$/) && method === 'PATCH') {
return json(res, 200, { supplier: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (method === 'DELETE' && p.match(/^\/api\/(supplier|warehouse|printer|labeltemplate|customergroup|entity|delivery|quote|order|posterminal)\/[^/]+(\/(address|contact|category|all))?$/)) {
if (p.includes('/posterminal/') && (p.endsWith('/all') || p.includes('/category/'))) {
return json(res, 200, { id: Number(p.split('/')[3]), categoryIds: [] });
}
res.writeHead(204);
return res.end();
}
if (p === '/api/warehouse' && method === 'POST') {
return json(res, 201, { warehouse: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/warehouse\/\d+$/) && method === 'PATCH') {
return json(res, 200, { warehouse: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p === '/api/warehouse/order' && method === 'PUT') {
return json(res, 200, { warehouseIds: body?.warehouseIds ?? [] });
}
if (p === '/api/stock/adjust' && method === 'POST') {
return json(res, 200, { ok: true });
}
if (p === '/api/delivery' && method === 'POST') {
return json(res, 201, { delivery: { ...(documents.deliveryDetail ?? {}), ...(body ?? {}) } });
}
if (p.match(/^\/api\/delivery\/\d+$/) && method === 'PATCH') {
return json(res, 200, { delivery: { ...(documents.deliveryDetail ?? {}), ...(body ?? {}) } });
}
if (p === '/api/quote' && method === 'POST') {
return json(res, 201, { quote: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/quote\/\d+$/) && method === 'PATCH') {
return json(res, 200, { quote: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p === '/api/order' && method === 'POST') {
return json(res, 201, { order: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/order\/\d+$/) && method === 'PATCH') {
return json(res, 200, { order: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p === '/api/entity' && method === 'POST') {
return json(res, 201, { entity: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/entity\/\d+$/) && method === 'PATCH') {
return json(res, 200, { entity: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p.match(/^\/api\/entity\/\d+\/(address|contact)$/) && method === 'POST') {
const kind = p.endsWith('address') ? 'address' : 'contact';
return json(res, 201, { [kind]: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/entity\/\d+\/(address|contact)\/\d+$/) && method === 'PATCH') {
const kind = p.includes('/address/') ? 'address' : 'contact';
return json(res, 200, { [kind]: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p === '/api/customergroup' && method === 'POST') {
return json(res, 201, { customergroup: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/customergroup\/\d+$/) && method === 'PATCH') {
return json(res, 200, { customergroup: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p === '/api/customergroup/order' && method === 'PUT') {
return json(res, 200, { customergroupIds: body?.customergroupIds ?? [] });
}
if (p === '/api/company' && method === 'POST') {
return json(res, 201, { company: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/company\/\d+$/) && method === 'PUT') {
return json(res, 200, { company: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p.match(/^\/api\/company\/setting\/.+$/) && method === 'PUT') {
return json(res, 200, { setting: { key: decodeURIComponent(p.split('/').pop()), ...(body ?? {}) } });
}
if (p === '/api/company/import' && method === 'POST') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
res.write('event: status\ndata: {"message": "Import gestartet. Daten werden verarbeitet..."}\n\n');
res.write('event: progress\ndata: {"phase": "categories", "current": 1, "total": 1, "item": "Katalog"}\n\n');
res.write('event: done\ndata: {"ok": true, "message": "Import erfolgreich abgeschlossen.", "stats": {"categories": 1, "categoriesCreated": 1, "categoryPictures": 0, "articlesTotal": 1, "articlesImported": 1, "articlesUpdated": 0, "variantsLinked": 0, "articlePictures": 1}}\n\n');
res.end();
return;
}
if (p.match(/^\/api\/number\/\d+$/) && method === 'PATCH') {
return json(res, 200, { number: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p === '/api/posterminal' && method === 'PUT') {
return json(res, 200, { posterminal: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/posterminal\/\d+$/) && method === 'PATCH') {
return json(res, 200, { posterminal: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p.match(/^\/api\/posterminal\/\d+\/(all|category\/\d+)$/) && ['PUT', 'DELETE'].includes(method)) {
return json(res, 200, { id: Number(p.split('/')[3]), categoryIds: [] });
}
if (p === '/api/printer' && method === 'POST') {
return json(res, 201, { printer: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/printer\/\d+$/) && method === 'PATCH') {
return json(res, 200, { printer: { id: Number(p.split('/').pop()), ...(body ?? {}) } });
}
if (p === '/api/printer/order' && method === 'PUT') {
return json(res, 200, { printerIds: body?.printerIds ?? [] });
}
if (p === '/api/labeltemplate' && method === 'POST') {
return json(res, 201, { labeltemplate: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/labeltemplate\/.+$/) && method === 'PATCH') {
return json(res, 200, { labeltemplate: { id: p.split('/').pop(), ...(body ?? {}) } });
}
if (p === '/api/ledger' && method === 'POST') {
return json(res, 201, { ledger: { id: 999001, ...(body ?? {}) } });
}
if (p === '/api/attributename' && method === 'POST') {
return json(res, 201, { attributename: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/attributename\/.+$/) && ['PATCH', 'PUT'].includes(method)) {
return json(res, 200, { attributename: { id: p.split('/').pop(), ...(body ?? {}) } });
}
if (method === 'DELETE' && p.match(/^\/api\/(attributename|attributevalue|propertyname)\/.+$/)) {
return json(res, 200, { ok: true });
}
if (p === '/api/propertyname' && method === 'POST') {
return json(res, 201, { propertyname: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/propertyname\/.+$/) && ['PATCH', 'PUT'].includes(method)) {
return json(res, 200, { propertyname: { id: p.split('/').pop(), ...(body ?? {}) } });
}
if (p === '/api/modelconversation' && method === 'POST') {
return json(res, 201, { conversation: { id: 999001, ...(body ?? {}) } });
}
if (p.match(/^\/api\/modelconversation\/\d+\/message$/) && method === 'POST') {
return json(res, 201, { message: { id: 999001, ...(body ?? {}) } });
}
if (method === 'DELETE' && p.match(/^\/api\/modelconversation\/\d+$/)) {
res.writeHead(204);
return res.end();
}
if (p === '/api/vfs/upload' && method === 'POST') {
return json(res, 200, { files: [] });
}
if (p.startsWith('/api/vfs/') && method === 'PUT') {
return json(res, 200, { ok: true });
}
if (method === 'DELETE' && p.startsWith('/api/vfs/')) {
res.writeHead(204);
return res.end();
}
return json(res, 404, { error: 'Not found' });
});
server.listen(PORT, () => {
console.log(`[dummy-backend] fixtures on http://localhost:${PORT} (${Object.keys(catalog.articlesByCategory ?? {}).length} categories)`);
});