Files
glitch_automata_lab/backend/dist/caStudioApi.js
T

243 lines
10 KiB
JavaScript

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