import express from 'express'; import cookieParser from 'cookie-parser'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { requireLabAccess } from './labAuth.js'; import { createCaEngine, createDeck, createInitialConditionGenerator, createPresetNode, createPresetTree, createScene, deleteDeck, deletePresetNode, deletePresetTree, deleteScene, getCaEngine, getDeck, getInitialConditionGenerator, getPresetNode, getScene, listCaEngines, listCaEnginePresetUsage, listDecks, listInitialConditionGenerators, listPresetNodeUsage, listPresetNodes, listPresetTrees, listScenes, resolveDeck, resolvePresetNode, resolveScene, updateCaEngine, updateDeck, updateInitialConditionGenerator, updatePresetNode, updateScene } from './caStudioRepository.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const adminPublicPath = resolve(__dirname, '../public/admin'); function asyncRoute(handler) { return (request, response, next) => { handler(request, response).catch(next); }; } function routeParam(request, key) { const value = request.params[key]; if (typeof value !== 'string') { throw new Error(`Missing route parameter: ${key}`); } return value; } function queryString(value) { return typeof value === 'string' ? value : undefined; } function parseStates(value) { const trimmed = value.trim(); if (/^\d+$/.test(trimmed)) return Number(trimmed); if (trimmed.startsWith('[')) return JSON.parse(trimmed); return trimmed.split(',').map((state) => state.trim()).filter(Boolean); } function caClassFromQuery(query) { const dimensions = queryString(query.dimensions); const states = queryString(query.states); if (!dimensions && !states) return undefined; if (!dimensions || !states) { throw new Error('Filtering initial condition generators requires dimensions and states'); } return { dimensions: Number(dimensions), states: parseStates(states) }; } export function createCaStudioApi(db) { const app = express(); const labAccess = requireLabAccess(); app.use(cookieParser()); app.use(express.json({ limit: '1mb' })); app.use(express.urlencoded({ extended: false })); app.use('/admin', labAccess); app.use('/view', labAccess); app.use('/api/ca', labAccess); app.use('/admin', express.static(adminPublicPath)); app.get('/', labAccess, (_request, response) => { response.redirect('/admin'); }); app.get(/^\/(?:admin|view)(?:\/.*)?$/, (_request, response) => { response.sendFile(resolve(adminPublicPath, 'index.html')); }); app.post('/api/ca/preset-trees', asyncRoute(async (request, response) => { const tree = await createPresetTree(db, request.body); response.status(201).json({ ...tree, node_count: 0 }); })); app.get('/api/ca/preset-trees', asyncRoute(async (_request, response) => { response.json(await listPresetTrees(db)); })); app.delete('/api/ca/preset-trees/:treeId', asyncRoute(async (request, response) => { const deleted = await deletePresetTree(db, routeParam(request, 'treeId')); response.sendStatus(deleted ? 204 : 404); })); app.get('/api/ca/preset-trees/:treeId/nodes', asyncRoute(async (request, response) => { response.json(await listPresetNodes(db, routeParam(request, 'treeId'))); })); app.post('/api/ca/preset-trees/:treeId/nodes', asyncRoute(async (request, response) => { const node = await createPresetNode(db, { ...request.body, treeId: routeParam(request, 'treeId') }); response.status(201).json(node); })); app.get('/api/ca/preset-trees/:treeId/nodes/:nodeId/resolved', asyncRoute(async (request, response) => { const resolved = await resolvePresetNode(db, routeParam(request, 'treeId'), routeParam(request, 'nodeId')); if (!resolved) { response.sendStatus(404); return; } response.json(resolved); })); app.get('/api/ca/preset-trees/:treeId/nodes/:nodeId', asyncRoute(async (request, response) => { const node = await getPresetNode(db, routeParam(request, 'treeId'), routeParam(request, 'nodeId')); if (!node) { response.sendStatus(404); return; } response.json(node); })); app.get('/api/ca/preset-trees/:treeId/node-usage', asyncRoute(async (request, response) => { response.json(await listPresetNodeUsage(db, routeParam(request, 'treeId'))); })); app.patch('/api/ca/preset-trees/:treeId/nodes/:nodeId', asyncRoute(async (request, response) => { const node = await updatePresetNode(db, routeParam(request, 'treeId'), routeParam(request, 'nodeId'), request.body); if (!node) { response.sendStatus(404); return; } response.json(node); })); app.delete('/api/ca/preset-trees/:treeId/nodes/:nodeId', asyncRoute(async (request, response) => { const deleted = await deletePresetNode(db, routeParam(request, 'treeId'), routeParam(request, 'nodeId')); response.sendStatus(deleted ? 204 : 404); })); app.post('/api/ca/initial-condition-generators', asyncRoute(async (request, response) => { response.status(201).json(await createInitialConditionGenerator(db, request.body)); })); app.get('/api/ca/initial-condition-generators', asyncRoute(async (request, response) => { response.json(await listInitialConditionGenerators(db, { caClass: caClassFromQuery(request.query) })); })); app.get('/api/ca/initial-condition-generators/:generatorId', asyncRoute(async (request, response) => { const generator = await getInitialConditionGenerator(db, routeParam(request, 'generatorId')); if (!generator) { response.sendStatus(404); return; } response.json(generator); })); app.patch('/api/ca/initial-condition-generators/:generatorId', asyncRoute(async (request, response) => { const generator = await updateInitialConditionGenerator(db, routeParam(request, 'generatorId'), request.body); if (!generator) { response.sendStatus(404); return; } response.json(generator); })); app.post('/api/ca/engines', asyncRoute(async (request, response) => { response.status(201).json(await createCaEngine(db, request.body)); })); app.get('/api/ca/engines', asyncRoute(async (request, response) => { response.json(await listCaEngines(db, { caClass: caClassFromQuery(request.query) })); })); app.get('/api/ca/engines/:engineId/usage', asyncRoute(async (request, response) => { const usage = await listCaEnginePresetUsage(db, routeParam(request, 'engineId')); if (!usage) { response.sendStatus(404); return; } response.json(usage); })); app.get('/api/ca/engines/:engineId', asyncRoute(async (request, response) => { const engine = await getCaEngine(db, routeParam(request, 'engineId')); if (!engine) { response.sendStatus(404); return; } response.json(engine); })); app.patch('/api/ca/engines/:engineId', asyncRoute(async (request, response) => { const engine = await updateCaEngine(db, routeParam(request, 'engineId'), request.body); if (!engine) { response.sendStatus(404); return; } response.json(engine); })); app.post('/api/ca/decks', asyncRoute(async (request, response) => { response.status(201).json(await createDeck(db, request.body)); })); app.get('/api/ca/decks', asyncRoute(async (_request, response) => { response.json(await listDecks(db)); })); app.get('/api/ca/decks/:deckId', asyncRoute(async (request, response) => { const deck = await getDeck(db, routeParam(request, 'deckId')); if (!deck) { response.sendStatus(404); return; } response.json(deck); })); app.patch('/api/ca/decks/:deckId', asyncRoute(async (request, response) => { const deck = await updateDeck(db, routeParam(request, 'deckId'), request.body); if (!deck) { response.sendStatus(404); return; } response.json(deck); })); app.delete('/api/ca/decks/:deckId', asyncRoute(async (request, response) => { const deleted = await deleteDeck(db, routeParam(request, 'deckId')); response.sendStatus(deleted ? 204 : 404); })); app.get('/api/ca/decks/:deckId/resolved', asyncRoute(async (request, response) => { const resolved = await resolveDeck(db, routeParam(request, 'deckId')); if (!resolved) { response.sendStatus(404); return; } response.json(resolved); })); app.get('/api/ca/decks/:deckId/scenes', asyncRoute(async (request, response) => { response.json(await listScenes(db, routeParam(request, 'deckId'))); })); app.post('/api/ca/decks/:deckId/scenes', asyncRoute(async (request, response) => { const scene = await createScene(db, { ...request.body, deckId: routeParam(request, 'deckId') }); response.status(201).json(scene); })); app.get('/api/ca/scenes/:sceneId', asyncRoute(async (request, response) => { const scene = await getScene(db, routeParam(request, 'sceneId')); if (!scene) { response.sendStatus(404); return; } response.json(scene); })); app.patch('/api/ca/scenes/:sceneId', asyncRoute(async (request, response) => { const scene = await updateScene(db, routeParam(request, 'sceneId'), request.body); if (!scene) { response.sendStatus(404); return; } response.json(scene); })); app.delete('/api/ca/scenes/:sceneId', asyncRoute(async (request, response) => { const deleted = await deleteScene(db, routeParam(request, 'sceneId')); response.sendStatus(deleted ? 204 : 404); })); app.get('/api/ca/scenes/:sceneId/resolved', asyncRoute(async (request, response) => { const resolved = await resolveScene(db, routeParam(request, 'sceneId')); if (!resolved) { response.sendStatus(404); return; } response.json(resolved); })); app.use((error, _request, response, _next) => { const message = error instanceof Error ? error.message : 'Unknown error'; response.status(400).json({ error: message }); }); return app; } //# sourceMappingURL=caStudioApi.js.map