feat: append-only version history + bliss CLI ergonomics
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>
This commit is contained in:
parent
03a9d2143d
commit
42f4aacf7c
6 changed files with 211 additions and 7 deletions
71
db.js
71
db.js
|
|
@ -137,6 +137,8 @@ function createRoute(verb, path, structureId, handler) {
|
|||
createScaffoldPage(routeId);
|
||||
}
|
||||
|
||||
recordVersion("route", routeId, structureId, { verb, path, handler });
|
||||
|
||||
return routeId; // Returns the route_id of the newly created route
|
||||
}
|
||||
|
||||
|
|
@ -175,12 +177,66 @@ function update(table, fields, obj) {
|
|||
return db.prepare(`UPDATE ${table} SET ${placeholders} WHERE id = ?`).run(...values);
|
||||
}
|
||||
|
||||
// ---- version history (append-only) --------------------------------------
|
||||
//
|
||||
// Snapshot the code-bearing fields of a route/template/db every time they
|
||||
// change, so nothing a user (or the plumber) edits is ever truly lost. Deduped
|
||||
// against the latest snapshot for that entity, so no-op saves, error-flag-only
|
||||
// route writes, and server-restart WS re-bootstraps don't pile up noise.
|
||||
// snapshotFor() defines exactly which fields are versioned per type; keep the
|
||||
// key order stable so the dedup string compare is reliable.
|
||||
function snapshotFor(entityType, obj) {
|
||||
switch (entityType) {
|
||||
case "route":
|
||||
return { verb: obj.verb, path: obj.path, handler: obj.handler };
|
||||
case "template":
|
||||
return { name: obj.name, content: obj.content, test_object: obj.test_object };
|
||||
case "db":
|
||||
return { name: obj.name, library: obj.library };
|
||||
default:
|
||||
throw new Error(`unknown version entity type: ${entityType}`);
|
||||
}
|
||||
}
|
||||
|
||||
function recordVersion(entityType, entityId, structureId, obj) {
|
||||
const json = JSON.stringify(snapshotFor(entityType, obj));
|
||||
const last = db
|
||||
.prepare(
|
||||
"SELECT snapshot FROM versions WHERE entity_type = ? AND entity_id = ? ORDER BY id DESC LIMIT 1",
|
||||
)
|
||||
.get(entityType, entityId);
|
||||
if (last && last.snapshot === json) return; // content unchanged — skip
|
||||
db.prepare(
|
||||
"INSERT INTO versions (entity_type, entity_id, structure_id, snapshot) VALUES (?, ?, ?, ?)",
|
||||
).run(entityType, entityId, structureId, json);
|
||||
}
|
||||
|
||||
// Version list for one entity (newest first). Omits the snapshot body to stay
|
||||
// scannable; fetch a single version to get its full content.
|
||||
function getVersions(entityType, entityId) {
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT id, entity_type, entity_id, structure_id, created_at, length(snapshot) AS bytes
|
||||
FROM versions WHERE entity_type = ? AND entity_id = ? ORDER BY id DESC`,
|
||||
)
|
||||
.all(entityType, entityId);
|
||||
}
|
||||
|
||||
// One version with its full snapshot parsed back into fields.
|
||||
function getVersion(versionId) {
|
||||
const row = db.prepare("SELECT * FROM versions WHERE id = ?").get(versionId);
|
||||
if (!row) return null;
|
||||
return { ...row, snapshot: JSON.parse(row.snapshot) };
|
||||
}
|
||||
|
||||
function updateRoute(route) {
|
||||
update("routes", ["verb", "path", "structure_id", "handler", "updated_at", "error"], route);
|
||||
recordVersion("route", route.id, route.structure_id, route);
|
||||
}
|
||||
|
||||
function updateDb(appDb) {
|
||||
update("dbs", ["name", "library"], appDb);
|
||||
recordVersion("db", appDb.id, appDb.structure_id, appDb);
|
||||
}
|
||||
|
||||
function updateStruct(struct) {
|
||||
|
|
@ -189,6 +245,7 @@ function updateStruct(struct) {
|
|||
|
||||
function updateTemplate(template) {
|
||||
update("templates", ["content", "name", "test_object"], template);
|
||||
recordVersion("template", template.id, template.structure_id, template);
|
||||
}
|
||||
|
||||
function getTemplates(structureId) {
|
||||
|
|
@ -210,11 +267,17 @@ function getTemplateContentByName(structId, name) {
|
|||
}
|
||||
|
||||
function createTemplate(structureId, name, content, testObjectString) {
|
||||
return db
|
||||
const id = db
|
||||
.prepare(
|
||||
"INSERT INTO templates (structure_id, name, content, test_object) VALUES (?, ?, ?, ?)",
|
||||
)
|
||||
.run(structureId, name, content, testObjectString).lastInsertRowid;
|
||||
recordVersion("template", id, structureId, {
|
||||
name,
|
||||
content,
|
||||
test_object: testObjectString,
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
function getDbsForStructure(structureId) {
|
||||
|
|
@ -282,7 +345,9 @@ function createDb(structId, name) {
|
|||
return dbId;
|
||||
});
|
||||
|
||||
return transaction();
|
||||
const dbId = transaction();
|
||||
recordVersion("db", dbId, structId, { name, library: "" });
|
||||
return dbId;
|
||||
}
|
||||
|
||||
function attachDb(structId, dbId, alias) {
|
||||
|
|
@ -615,6 +680,8 @@ module.exports = {
|
|||
updateDb,
|
||||
updateStruct,
|
||||
updateTemplate,
|
||||
getVersions,
|
||||
getVersion,
|
||||
getTemplater,
|
||||
getTemplates,
|
||||
getTemplate,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue