Expose project revision timeline

This commit is contained in:
2026-07-25 21:57:03 +02:00
parent 4b47bc2bcc
commit 5a0c9019af
14 changed files with 365 additions and 8 deletions
+38
View File
@@ -0,0 +1,38 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { listProjectRevisionsQuerySchema } from "../src/shared/validation/project-history.schemas.js";
describe("project revision timeline query", () => {
it("applies a bounded default page size", () => {
assert.deepEqual(
listProjectRevisionsQuerySchema.parse({}),
{ limit: 25 }
);
assert.deepEqual(
listProjectRevisionsQuerySchema.parse({
limit: "100",
beforeRevision: "42",
}),
{
limit: 100,
beforeRevision: 42,
}
);
});
it("rejects invalid cursors, page sizes and unknown query fields", () => {
for (const query of [
{ limit: "0" },
{ limit: "101" },
{ limit: "1.5" },
{ beforeRevision: "0" },
{ beforeRevision: "not-a-number" },
{ unexpected: "value" },
]) {
assert.equal(
listProjectRevisionsQuerySchema.safeParse(query).success,
false
);
}
});
});
+144
View File
@@ -83,6 +83,150 @@ function getRowQuantity(context: DatabaseContext) {
}
describe("project history repository", () => {
it("lists revision metadata in stable descending pages without payloads", () => {
const context = createTestDatabase();
try {
const circuitsStore = new CircuitProjectCommandRepository(context.db);
const rowsStore = new CircuitDeviceRowProjectCommandRepository(context.db);
const first = circuitsStore.executeUpdate({
projectId: "project-1",
expectedRevision: 0,
source: "user",
actorId: "planner-1",
description: "Stromkreis umbenennen",
command: createCircuitUpdateProjectCommand("circuit-1", {
displayName: "Neu",
}),
});
const second = rowsStore.executeUpdate({
projectId: "project-1",
expectedRevision: 1,
source: "user",
description: "Anzahl ändern",
command: createCircuitDeviceRowUpdateProjectCommand("row-1", {
quantity: 2,
}),
});
const third = rowsStore.executeUpdate({
projectId: "project-1",
expectedRevision: 2,
source: "undo",
description: "Anzahl zurücknehmen",
historyTargetChangeSetId: second.revision.changeSetId,
command: second.inverse,
});
const history = new ProjectHistoryRepository(context.db);
assert.deepEqual(
history.listRevisions({
projectId: "project-1",
limit: 2,
}),
{
projectId: "project-1",
currentRevision: 3,
revisions: [
{
revisionId: third.revision.revisionId,
changeSetId: third.revision.changeSetId,
revisionNumber: 3,
createdAtIso: third.revision.createdAtIso,
actorId: null,
source: "undo",
description: "Anzahl zurücknehmen",
commandType: "circuit-device-row.update",
payloadSchemaVersion: 1,
},
{
revisionId: second.revision.revisionId,
changeSetId: second.revision.changeSetId,
revisionNumber: 2,
createdAtIso: second.revision.createdAtIso,
actorId: null,
source: "user",
description: "Anzahl ändern",
commandType: "circuit-device-row.update",
payloadSchemaVersion: 1,
},
],
nextBeforeRevision: 2,
}
);
assert.deepEqual(
history.listRevisions({
projectId: "project-1",
limit: 2,
beforeRevision: 2,
}),
{
projectId: "project-1",
currentRevision: 3,
revisions: [
{
revisionId: first.revision.revisionId,
changeSetId: first.revision.changeSetId,
revisionNumber: 1,
createdAtIso: first.revision.createdAtIso,
actorId: "planner-1",
source: "user",
description: "Stromkreis umbenennen",
commandType: "circuit.update",
payloadSchemaVersion: 1,
},
],
nextBeforeRevision: null,
}
);
} finally {
context.close();
}
});
it("lists an empty timeline and rejects invalid page inputs", () => {
const context = createTestDatabase();
try {
const history = new ProjectHistoryRepository(context.db);
assert.deepEqual(
history.listRevisions({
projectId: "project-1",
limit: 25,
}),
{
projectId: "project-1",
currentRevision: 0,
revisions: [],
nextBeforeRevision: null,
}
);
assert.equal(
history.listRevisions({
projectId: "missing",
limit: 25,
}),
null
);
assert.throws(
() =>
history.listRevisions({
projectId: "project-1",
limit: 0,
}),
/between 1 and 100/
);
assert.throws(
() =>
history.listRevisions({
projectId: "project-1",
limit: 25,
beforeRevision: 0,
}),
/positive integer/
);
} finally {
context.close();
}
});
it("persists project-wide undo and redo stacks across repository instances", () => {
const context = createTestDatabase();
try {