2026-07-01 12:10:12 +02:00
|
|
|
import express from 'express';
|
2026-07-03 23:57:26 +02:00
|
|
|
import cookieParser from 'cookie-parser';
|
2026-07-01 12:10:12 +02:00
|
|
|
import { dirname, resolve } from 'node:path';
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
2026-07-03 23:57:26 +02:00
|
|
|
import { requireLabAccess } from './labAuth.js';
|
2026-07-01 12:10:12 +02:00
|
|
|
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();
|
2026-07-03 23:57:26 +02:00
|
|
|
const labAccess = requireLabAccess();
|
|
|
|
|
app.use(cookieParser());
|
2026-07-01 12:10:12 +02:00
|
|
|
app.use(express.json({ limit: '1mb' }));
|
2026-07-03 23:57:26 +02:00
|
|
|
app.use('/admin', labAccess);
|
|
|
|
|
app.use('/view', labAccess);
|
|
|
|
|
app.use('/api/ca', labAccess);
|
2026-07-01 12:10:12 +02:00
|
|
|
app.use('/admin', express.static(adminPublicPath));
|
2026-07-03 23:57:26 +02:00
|
|
|
app.get('/', labAccess, (_request, response) => {
|
2026-07-01 12:10:12 +02:00
|
|
|
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
|