Snapshot the code-bearing fields of routes (verb/path/handler), templates (name/content/test_object), and db libraries (name/library) on every content-changing save, so nothing edited is ever lost. Recorded in a new `versions` table via recordVersion() hooked into the create/update functions in db.js (the single write choke point), deduped against the latest snapshot so no-op saves, error-flag-only route writes, and server-restart WS re-bootstraps don't pile up. The live row stays current; old snapshots are the undo trail. Read-only, no revert UI yet. - migrations/004_add_versions.sql: versions table + indexes - db.js: recordVersion/getVersions/getVersion + create/update hooks - plumbing.js: /versions/:id and per-entity .../versions list endpoints - bliss: `versions <type> <sid> <id>` and `version <id>` reads; `update-settings` (edit route_prefix/head_injection, preserving the untouched field); `use <url>` sticky target persisted to ~/.bliss/target so BLISS_URL needn't be re-exported each call - client.js: target-file precedence (BLISS_URL > ~/.bliss/target > localhost) - SKILL.md: document the above Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
109 lines
3.2 KiB
JavaScript
109 lines
3.2 KiB
JavaScript
// 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();
|
|
|
|
// 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 });
|
|
}
|
|
};
|
|
}
|
|
|
|
// All structures.
|
|
router.get(
|
|
"/structures",
|
|
json(() => model.getStructures()),
|
|
);
|
|
|
|
// 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)),
|
|
);
|
|
|
|
// One version, including its full snapshot (the versioned fields).
|
|
router.get(
|
|
"/versions/:versionId",
|
|
json((req) => model.getVersion(req.params.versionId)),
|
|
);
|
|
|
|
// 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;
|