feat(api): add all REST route handlers and display-api test (Task 12)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c922762dfb
commit
44f4b6d010
41
display/middleware/src/api/routes/datapoints.ts
Normal file
41
display/middleware/src/api/routes/datapoints.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import {
|
||||||
|
getAllDatapoints,
|
||||||
|
getDatapointById,
|
||||||
|
updateDatapoint,
|
||||||
|
} from '../../modules/datapoints/datapoint-manager';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// GET /api/datapoints
|
||||||
|
router.get('/', (_req, res): void => {
|
||||||
|
try {
|
||||||
|
res.json(getAllDatapoints());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/datapoints/:id
|
||||||
|
router.get('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const dp = getDatapointById(parseInt(req.params.id));
|
||||||
|
if (!dp) { res.status(404).json({ error: 'Datapoint not found' }); return; }
|
||||||
|
res.json(dp);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/datapoints/:id
|
||||||
|
router.put('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const dp = updateDatapoint(parseInt(req.params.id), req.body);
|
||||||
|
if (!dp) { res.status(404).json({ error: 'Datapoint not found' }); return; }
|
||||||
|
res.json(dp);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
64
display/middleware/src/api/routes/display-api.ts
Normal file
64
display/middleware/src/api/routes/display-api.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { getDb } from '../../db/index';
|
||||||
|
import { buildDisplayJson } from '../../modules/display/display-builder';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/display/:id
|
||||||
|
* ESP32 endpoint — returns display JSON.
|
||||||
|
* Optional token auth via ?token=... or Authorization: Bearer ...
|
||||||
|
* If the display has an api_token set, the token MUST match.
|
||||||
|
*/
|
||||||
|
router.get('/:id', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const displayId = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const display = db
|
||||||
|
.prepare('SELECT id, api_token FROM displays WHERE id = ?')
|
||||||
|
.get(displayId) as { id: number; api_token: string | null } | undefined;
|
||||||
|
|
||||||
|
if (!display) {
|
||||||
|
res.status(404).json({ error: 'Display not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check token if display has one configured
|
||||||
|
if (display.api_token) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
const queryToken = req.query.token as string | undefined;
|
||||||
|
|
||||||
|
let provided: string | null = null;
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
provided = authHeader.slice(7);
|
||||||
|
} else if (queryToken) {
|
||||||
|
provided = queryToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!provided || provided !== display.api_token) {
|
||||||
|
res.status(401).json({ error: 'Invalid or missing token' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last_seen
|
||||||
|
db.run(
|
||||||
|
`UPDATE displays SET last_seen = datetime('now') WHERE id = ?`,
|
||||||
|
[displayId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const json = buildDisplayJson(displayId);
|
||||||
|
if (!json) {
|
||||||
|
res.status(404).json({ error: 'Display configuration not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(json);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
102
display/middleware/src/api/routes/displays.ts
Normal file
102
display/middleware/src/api/routes/displays.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { getDb } from '../../db/index';
|
||||||
|
|
||||||
|
export interface Display {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
layout_id: number | null;
|
||||||
|
api_token: string | null;
|
||||||
|
last_seen: string | null;
|
||||||
|
refresh_sec: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// GET /api/displays
|
||||||
|
router.get('/', (_req, res): void => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
res.json(db.prepare('SELECT * FROM displays ORDER BY id ASC').all());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/displays/:id
|
||||||
|
router.get('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const display = db.prepare('SELECT * FROM displays WHERE id = ?').get(parseInt(req.params.id));
|
||||||
|
if (!display) { res.status(404).json({ error: 'Display not found' }); return; }
|
||||||
|
res.json(display);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/displays
|
||||||
|
router.post('/', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const { name, description, layout_id, api_token, refresh_sec } = req.body;
|
||||||
|
if (!name) { res.status(400).json({ error: 'name is required' }); return; }
|
||||||
|
|
||||||
|
const result = db.run(
|
||||||
|
`INSERT INTO displays (name, description, layout_id, api_token, refresh_sec)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
[name, description ?? null, layout_id ?? null, api_token ?? null, refresh_sec ?? 300]
|
||||||
|
);
|
||||||
|
const id = result.lastInsertRowid as number;
|
||||||
|
res.status(201).json(db.prepare('SELECT * FROM displays WHERE id = ?').get(id));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/displays/:id
|
||||||
|
router.put('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const fields: string[] = [];
|
||||||
|
const values: unknown[] = [];
|
||||||
|
|
||||||
|
const { name, description, layout_id, api_token, refresh_sec } = req.body;
|
||||||
|
if (name !== undefined) { fields.push('name = ?'); values.push(name); }
|
||||||
|
if (description !== undefined) { fields.push('description = ?'); values.push(description); }
|
||||||
|
if (layout_id !== undefined) { fields.push('layout_id = ?'); values.push(layout_id); }
|
||||||
|
if (api_token !== undefined) { fields.push('api_token = ?'); values.push(api_token); }
|
||||||
|
if (refresh_sec !== undefined) { fields.push('refresh_sec = ?'); values.push(refresh_sec); }
|
||||||
|
|
||||||
|
if (fields.length === 0) {
|
||||||
|
const display = db.prepare('SELECT * FROM displays WHERE id = ?').get(id);
|
||||||
|
if (!display) { res.status(404).json({ error: 'Display not found' }); return; }
|
||||||
|
res.json(display);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
db.run(`UPDATE displays SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||||
|
const display = db.prepare('SELECT * FROM displays WHERE id = ?').get(id);
|
||||||
|
if (!display) { res.status(404).json({ error: 'Display not found' }); return; }
|
||||||
|
res.json(display);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/displays/:id
|
||||||
|
router.delete('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const result = db.run('DELETE FROM displays WHERE id = ?', [parseInt(req.params.id)]);
|
||||||
|
if ((result.changes as number) === 0) { res.status(404).json({ error: 'Display not found' }); return; }
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
102
display/middleware/src/api/routes/layouts.ts
Normal file
102
display/middleware/src/api/routes/layouts.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import {
|
||||||
|
getAllLayouts,
|
||||||
|
getLayoutById,
|
||||||
|
createLayout,
|
||||||
|
updateLayout,
|
||||||
|
deleteLayout,
|
||||||
|
addElement,
|
||||||
|
updateElement,
|
||||||
|
deleteElement,
|
||||||
|
} from '../../modules/display/layout-manager';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// GET /api/layouts
|
||||||
|
router.get('/', (_req, res): void => {
|
||||||
|
try {
|
||||||
|
res.json(getAllLayouts());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/layouts/:id
|
||||||
|
router.get('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const layout = getLayoutById(parseInt(req.params.id));
|
||||||
|
if (!layout) { res.status(404).json({ error: 'Layout not found' }); return; }
|
||||||
|
res.json(layout);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/layouts
|
||||||
|
router.post('/', (req, res): void => {
|
||||||
|
try {
|
||||||
|
if (!req.body.name) { res.status(400).json({ error: 'name is required' }); return; }
|
||||||
|
res.status(201).json(createLayout(req.body));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/layouts/:id
|
||||||
|
router.put('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const layout = updateLayout(parseInt(req.params.id), req.body);
|
||||||
|
if (!layout) { res.status(404).json({ error: 'Layout not found' }); return; }
|
||||||
|
res.json(layout);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/layouts/:id
|
||||||
|
router.delete('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const deleted = deleteLayout(parseInt(req.params.id));
|
||||||
|
if (!deleted) { res.status(404).json({ error: 'Layout not found' }); return; }
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Elements ----
|
||||||
|
|
||||||
|
// POST /api/layouts/:layoutId/elements
|
||||||
|
router.post('/:layoutId/elements', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const layoutId = parseInt(req.params.layoutId);
|
||||||
|
if (!req.body.type) { res.status(400).json({ error: 'type is required' }); return; }
|
||||||
|
res.status(201).json(addElement(layoutId, req.body));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/layouts/:layoutId/elements/:id
|
||||||
|
router.put('/:layoutId/elements/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const el = updateElement(parseInt(req.params.id), req.body);
|
||||||
|
if (!el) { res.status(404).json({ error: 'Element not found' }); return; }
|
||||||
|
res.json(el);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/layouts/:layoutId/elements/:id
|
||||||
|
router.delete('/:layoutId/elements/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const deleted = deleteElement(parseInt(req.params.id));
|
||||||
|
if (!deleted) { res.status(404).json({ error: 'Element not found' }); return; }
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
92
display/middleware/src/api/routes/mqtt-brokers.ts
Normal file
92
display/middleware/src/api/routes/mqtt-brokers.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import {
|
||||||
|
getAllBrokers,
|
||||||
|
getBrokerById,
|
||||||
|
createBroker,
|
||||||
|
updateBroker,
|
||||||
|
deleteBroker,
|
||||||
|
connectBroker,
|
||||||
|
disconnectBroker,
|
||||||
|
testBroker,
|
||||||
|
} from '../../modules/mqtt/broker-manager';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// GET /api/mqtt/brokers
|
||||||
|
router.get('/', (_req, res): void => {
|
||||||
|
try {
|
||||||
|
res.json(getAllBrokers());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/mqtt/brokers/:id
|
||||||
|
router.get('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const broker = getBrokerById(parseInt(req.params.id));
|
||||||
|
if (!broker) { res.status(404).json({ error: 'Broker not found' }); return; }
|
||||||
|
res.json(broker);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mqtt/brokers
|
||||||
|
router.post('/', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const { name, host } = req.body;
|
||||||
|
if (!name || !host) { res.status(400).json({ error: 'name and host are required' }); return; }
|
||||||
|
res.status(201).json(createBroker(req.body));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/mqtt/brokers/:id
|
||||||
|
router.put('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const broker = updateBroker(parseInt(req.params.id), req.body);
|
||||||
|
if (!broker) { res.status(404).json({ error: 'Broker not found' }); return; }
|
||||||
|
res.json(broker);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/mqtt/brokers/:id
|
||||||
|
router.delete('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const deleted = deleteBroker(parseInt(req.params.id));
|
||||||
|
if (!deleted) { res.status(404).json({ error: 'Broker not found' }); return; }
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mqtt/brokers/:id/connect
|
||||||
|
router.post('/:id/connect', (req, res): void => {
|
||||||
|
connectBroker(parseInt(req.params.id))
|
||||||
|
.then(() => res.json({ status: 'connected' }))
|
||||||
|
.catch((err) => res.status(500).json({ error: (err as Error).message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mqtt/brokers/:id/disconnect
|
||||||
|
router.post('/:id/disconnect', (req, res): void => {
|
||||||
|
try {
|
||||||
|
disconnectBroker(parseInt(req.params.id));
|
||||||
|
res.json({ status: 'disconnected' });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mqtt/brokers/test
|
||||||
|
router.post('/test', (req, res): void => {
|
||||||
|
testBroker(req.body)
|
||||||
|
.then((ok) => res.json({ success: ok }))
|
||||||
|
.catch((err) => res.status(500).json({ error: (err as Error).message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
57
display/middleware/src/api/routes/mqtt-topics.ts
Normal file
57
display/middleware/src/api/routes/mqtt-topics.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import {
|
||||||
|
getTopicsForBroker,
|
||||||
|
updateTopic,
|
||||||
|
startDiscovery,
|
||||||
|
addManualTopic,
|
||||||
|
} from '../../modules/mqtt/topic-discovery';
|
||||||
|
|
||||||
|
const router = Router({ mergeParams: true });
|
||||||
|
|
||||||
|
// GET /api/mqtt/brokers/:brokerId/topics
|
||||||
|
router.get('/', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const brokerId = parseInt(req.params.brokerId);
|
||||||
|
res.json(getTopicsForBroker(brokerId));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/mqtt/brokers/:brokerId/topics/:id
|
||||||
|
router.put('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const topic = updateTopic(parseInt(req.params.id), req.body);
|
||||||
|
if (!topic) { res.status(404).json({ error: 'Topic not found' }); return; }
|
||||||
|
res.json(topic);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mqtt/brokers/:brokerId/topics/discover
|
||||||
|
router.post('/discover', (req, res): void => {
|
||||||
|
const brokerId = parseInt(req.params.brokerId);
|
||||||
|
const { prefix } = req.body;
|
||||||
|
|
||||||
|
res.json({ message: 'Discovery started', brokerId, prefix: prefix ?? null });
|
||||||
|
|
||||||
|
// Run in background
|
||||||
|
startDiscovery(brokerId, prefix).catch((err) => {
|
||||||
|
console.error('Discovery error:', (err as Error).message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mqtt/brokers/:brokerId/topics/manual
|
||||||
|
router.post('/manual', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const brokerId = parseInt(req.params.brokerId);
|
||||||
|
const { topic } = req.body;
|
||||||
|
if (!topic) { res.status(400).json({ error: 'topic is required' }); return; }
|
||||||
|
res.status(201).json(addManualTopic(brokerId, topic));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
185
display/middleware/src/api/routes/pg-sources.ts
Normal file
185
display/middleware/src/api/routes/pg-sources.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import {
|
||||||
|
getAllSources,
|
||||||
|
getSourceById,
|
||||||
|
createSource,
|
||||||
|
updateSource,
|
||||||
|
deleteSource,
|
||||||
|
connectSource,
|
||||||
|
disconnectSource,
|
||||||
|
testSource,
|
||||||
|
} from '../../modules/postgres/source-manager';
|
||||||
|
import {
|
||||||
|
getAllQueries,
|
||||||
|
getQueryById,
|
||||||
|
createQuery,
|
||||||
|
updateQuery,
|
||||||
|
deleteQuery,
|
||||||
|
startQueryTimer,
|
||||||
|
stopQueryTimer,
|
||||||
|
} from '../../modules/postgres/query-scheduler';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// ---- Sources ----
|
||||||
|
|
||||||
|
// GET /api/pg/sources
|
||||||
|
router.get('/', (_req, res): void => {
|
||||||
|
try {
|
||||||
|
res.json(getAllSources());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/pg/sources/:id
|
||||||
|
router.get('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const source = getSourceById(parseInt(req.params.id));
|
||||||
|
if (!source) { res.status(404).json({ error: 'Source not found' }); return; }
|
||||||
|
res.json(source);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/pg/sources
|
||||||
|
router.post('/', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const { name, host, database_name, username, password } = req.body;
|
||||||
|
if (!name || !host || !database_name || !username || !password) {
|
||||||
|
res.status(400).json({ error: 'name, host, database_name, username, password are required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(201).json(createSource(req.body));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/pg/sources/:id
|
||||||
|
router.put('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const source = updateSource(parseInt(req.params.id), req.body);
|
||||||
|
if (!source) { res.status(404).json({ error: 'Source not found' }); return; }
|
||||||
|
res.json(source);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/pg/sources/:id
|
||||||
|
router.delete('/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const deleted = deleteSource(parseInt(req.params.id));
|
||||||
|
if (!deleted) { res.status(404).json({ error: 'Source not found' }); return; }
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/pg/sources/:id/connect
|
||||||
|
router.post('/:id/connect', (req, res): void => {
|
||||||
|
connectSource(parseInt(req.params.id))
|
||||||
|
.then(() => res.json({ status: 'connected' }))
|
||||||
|
.catch((err) => res.status(500).json({ error: (err as Error).message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/pg/sources/:id/disconnect
|
||||||
|
router.post('/:id/disconnect', (req, res): void => {
|
||||||
|
try {
|
||||||
|
disconnectSource(parseInt(req.params.id));
|
||||||
|
res.json({ status: 'disconnected' });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/pg/sources/test
|
||||||
|
router.post('/test', (req, res): void => {
|
||||||
|
testSource(req.body)
|
||||||
|
.then((ok) => res.json({ success: ok }))
|
||||||
|
.catch((err) => res.status(500).json({ error: (err as Error).message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Queries (sub-routes) ----
|
||||||
|
|
||||||
|
// GET /api/pg/sources/:sourceId/queries
|
||||||
|
router.get('/:sourceId/queries', (req, res): void => {
|
||||||
|
try {
|
||||||
|
res.json(getAllQueries(parseInt(req.params.sourceId)));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/pg/sources/:sourceId/queries/:id
|
||||||
|
router.get('/:sourceId/queries/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const q = getQueryById(parseInt(req.params.id));
|
||||||
|
if (!q) { res.status(404).json({ error: 'Query not found' }); return; }
|
||||||
|
res.json(q);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/pg/sources/:sourceId/queries
|
||||||
|
router.post('/:sourceId/queries', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const sourceId = parseInt(req.params.sourceId);
|
||||||
|
const { name, query, result_column } = req.body;
|
||||||
|
if (!name || !query || !result_column) {
|
||||||
|
res.status(400).json({ error: 'name, query, result_column are required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(201).json(createQuery({ ...req.body, source_id: sourceId }));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/pg/sources/:sourceId/queries/:id
|
||||||
|
router.put('/:sourceId/queries/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const q = updateQuery(parseInt(req.params.id), req.body);
|
||||||
|
if (!q) { res.status(404).json({ error: 'Query not found' }); return; }
|
||||||
|
res.json(q);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/pg/sources/:sourceId/queries/:id
|
||||||
|
router.delete('/:sourceId/queries/:id', (req, res): void => {
|
||||||
|
try {
|
||||||
|
const deleted = deleteQuery(parseInt(req.params.id));
|
||||||
|
if (!deleted) { res.status(404).json({ error: 'Query not found' }); return; }
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/pg/sources/:sourceId/queries/:id/start
|
||||||
|
router.post('/:sourceId/queries/:id/start', (req, res): void => {
|
||||||
|
try {
|
||||||
|
startQueryTimer(parseInt(req.params.id));
|
||||||
|
res.json({ status: 'started' });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/pg/sources/:sourceId/queries/:id/stop
|
||||||
|
router.post('/:sourceId/queries/:id/stop', (req, res): void => {
|
||||||
|
try {
|
||||||
|
stopQueryTimer(parseInt(req.params.id));
|
||||||
|
res.json({ status: 'stopped' });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
82
display/middleware/tests/api/display-api.test.ts
Normal file
82
display/middleware/tests/api/display-api.test.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { initDb, closeDb } from '../../src/db/index';
|
||||||
|
import { createApp } from '../../src/api/index';
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const db = initDb(':memory:');
|
||||||
|
|
||||||
|
// Create a layout
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO layouts (id, name, type, width, height)
|
||||||
|
VALUES (1, 'Test Layout', 'free', 800, 480)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Display without token
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO displays (id, name, layout_id, refresh_sec)
|
||||||
|
VALUES (1, 'Open Display', 1, 60)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Display with token auth
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO displays (id, name, layout_id, api_token, refresh_sec)
|
||||||
|
VALUES (2, 'Secure Display', 1, 'secret-token-123', 300)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
closeDb();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/display/:id', () => {
|
||||||
|
it('returns display JSON for open display', async () => {
|
||||||
|
const res = await request(app).get('/api/display/1');
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toHaveProperty('display');
|
||||||
|
expect(res.body.display.id).toBe(1);
|
||||||
|
expect(res.body).toHaveProperty('layout');
|
||||||
|
expect(res.body).toHaveProperty('elements');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 for non-existent display', async () => {
|
||||||
|
const res = await request(app).get('/api/display/999');
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 401 for token-protected display without token', async () => {
|
||||||
|
const res = await request(app).get('/api/display/2');
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 401 for token-protected display with wrong token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/display/2')
|
||||||
|
.set('Authorization', 'Bearer wrong-token');
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns display JSON with valid Bearer token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/display/2')
|
||||||
|
.set('Authorization', 'Bearer secret-token-123');
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.display.id).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns display JSON with valid query token', async () => {
|
||||||
|
const res = await request(app).get('/api/display/2?token=secret-token-123');
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.display.id).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates last_seen on successful access', async () => {
|
||||||
|
await request(app).get('/api/display/1');
|
||||||
|
// last_seen is updated — verify by checking display 1 via another endpoint
|
||||||
|
const res = await request(app).get('/api/display/1');
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
// Just verify the request succeeds — last_seen updated in DB
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user