bliss/plumbing.js

140 lines
4.1 KiB
JavaScript
Raw Normal View History

2026-08-02 23:00:26 -04:00
// plumbing.js — read-only JSON utility API.
//
// This is plumbing, not part of the product surface. The workshop UI renders
// HTML; anything that wants structured data (the bliss CLI, scripts, tooling)
// reads it here instead of scraping fragments. Read-only by design: all writes
// still go through the real /workshop/* endpoints a human uses, so this stays a
// thin mirror of model.* with no business logic of its own.
const express = require("express");
const model = require("./db");
const router = express.Router();
2026-08-19 09:51:07 -04:00
// Private structures are invisible through the plumbing too. Every structure-
// scoped route carries :id, so one param guard covers them all; a caller who
// isn't on the member list gets a 404, same as the workshop.
router.param("id", (req, res, next, id) => {
if (!model.canAccessStructure(req.session && req.session.userId, id)) {
return res.status(404).json({ error: "not found" });
}
next();
});
2026-08-02 23:00:26 -04:00
// Wrap a handler so thrown errors come back as JSON instead of an HTML stack.
function json(handler) {
return (req, res) => {
try {
const body = handler(req);
if (body === undefined || body === null) {
return res.status(404).json({ error: "not found" });
}
return res.json(body);
} catch (e) {
return res.status(500).json({ error: String(e), stack: e.stack });
}
};
}
2026-08-19 09:51:07 -04:00
// All structures the caller is allowed to see (private ones they aren't a
// member of are omitted).
2026-08-02 23:00:26 -04:00
router.get(
"/structures",
2026-08-19 09:51:07 -04:00
json((req) =>
model
.getStructures()
.filter((s) =>
model.canAccessStructure(req.session && req.session.userId, s.id),
),
),
2026-08-02 23:00:26 -04:00
);
// One structure with everything the sidebar shows, in one call.
router.get(
"/structures/:id",
json((req) => {
const structure = model.getStructure(req.params.id);
if (!structure) return null;
return {
structure,
routes: model.getRoutes(req.params.id),
templates: model.getTemplates(req.params.id),
dbs: model.getDbsForStructure(req.params.id),
files: model.getFilesForStruct(req.params.id),
};
}),
);
// One route, including its handler source.
router.get(
"/structures/:id/routes/:routeId",
json((req) => model.getRoute(req.params.routeId)),
);
// One template, including content + test_object.
router.get(
"/structures/:id/templates/:templateId",
json((req) => model.getTemplate(req.params.templateId)),
);
// One db (scoped to the structure), including its library source.
router.get(
"/structures/:id/dbs/:dbId",
json((req) => model.getDbForStructure(req.params.id, req.params.dbId)),
);
// Files attached to a structure.
router.get(
"/structures/:id/files",
json((req) => model.getFilesForStruct(req.params.id)),
);
// Version history (newest first) for a route handler, template, or db library.
// Each row is a summary; fetch /versions/:versionId for the full snapshot.
router.get(
"/structures/:id/routes/:routeId/versions",
json((req) => model.getVersions("route", req.params.routeId)),
);
router.get(
"/structures/:id/templates/:templateId/versions",
json((req) => model.getVersions("template", req.params.templateId)),
);
router.get(
"/structures/:id/dbs/:dbId/versions",
json((req) => model.getVersions("db", req.params.dbId)),
);
2026-08-19 09:51:07 -04:00
// One version, including its full snapshot (the versioned fields). Not scoped
// by :id, so it carries its own access check against the version's structure.
router.get(
"/versions/:versionId",
2026-08-19 09:51:07 -04:00
json((req) => {
const version = model.getVersion(req.params.versionId);
if (!version) return null;
if (
!model.canAccessStructure(
req.session && req.session.userId,
version.structure_id,
)
) {
return null;
}
return version;
}),
);
2026-08-02 23:00:26 -04:00
// Logs for a route. ?since=<id> returns only newer rows (for polling).
router.get(
"/structures/:id/routes/:routeId/logs",
json((req) => {
const { since } = req.query;
const logs =
since !== undefined
? model.getNewLogsByRoute(req.params.routeId, since)
: model.getLogsByRoute(req.params.routeId);
return { logs, since: model.getMostRecentLogIdByRoute(req.params.routeId) };
}),
);
module.exports = router;