Expose project revision timeline
This commit is contained in:
@@ -227,6 +227,9 @@ and CircuitDeviceRow writes execute persistent project commands. Applying a
|
||||
sorted view across multiple sections is one atomic `circuit.reorder-sections`
|
||||
command and one undo step. Explicit renumbering uses the collision-safe
|
||||
`circuit.renumber-section` command and is never triggered implicitly.
|
||||
Immutable revision metadata is available through the paginated
|
||||
`GET /api/projects/:projectId/history/revisions` endpoint; it does not expose
|
||||
stored command payloads.
|
||||
Insertions and generated move targets use client-generated stable UUIDs, and
|
||||
undo restores the same ids from complete server snapshots. The tree response
|
||||
supplies the optimistic `currentRevision`; obsolete direct field PATCH,
|
||||
|
||||
@@ -14,6 +14,16 @@ this value for optimistic concurrency checks.
|
||||
- `GET /projects/:projectId/history`
|
||||
- returns `currentRevision`, `undoDepth`, `redoDepth` and the current top
|
||||
change-set id for both persistent stacks
|
||||
- `GET /projects/:projectId/history/revisions`
|
||||
- returns revision metadata in descending `revisionNumber` order
|
||||
- optional query: `limit` (default `25`, maximum `100`) and exclusive
|
||||
`beforeRevision`
|
||||
- returns `projectId`, `currentRevision`, `revisions` and
|
||||
`nextBeforeRevision`; use the latter as the next page's
|
||||
`beforeRevision`
|
||||
- each revision contains ids, number, timestamp, actor, source, description,
|
||||
command type and payload schema version
|
||||
- stored forward/inverse command payloads are intentionally not exposed
|
||||
- `POST /projects/:projectId/commands`
|
||||
- executes a supported versioned command as a new user revision
|
||||
- body: `{ "expectedRevision": 0, "command": { ... } }`
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
persistent project commands and project-wide toolbar undo/redo, including
|
||||
moves, atomic multi-section reorders and explicit renumbering. Undo/redo
|
||||
eligibility is restored from server history after a page reload.
|
||||
- There is no browsable revision timeline yet; the editor currently exposes
|
||||
only the next eligible Undo/Redo actions.
|
||||
- A paginated revision-metadata API exists, but there is no browsable timeline
|
||||
UI yet; the editor currently exposes only the next eligible Undo/Redo
|
||||
actions.
|
||||
- Named logical project snapshots and restore are not implemented yet.
|
||||
- No Revit/CSV/IFCGUID import and export workflow is implemented yet.
|
||||
- Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime.
|
||||
|
||||
@@ -78,7 +78,9 @@ Gerätezeilen-Kommandos bewahren dabei auch lokale ProjectDevice-Overrides und
|
||||
prüfen Projektzugehörigkeit von Verknüpfungen und Räumen. Projektweite,
|
||||
persistente Undo-/Redo-Stacks verwalten die zulässige LIFO-Reihenfolge und
|
||||
verwerfen den Redo-Zweig bei einem neuen Benutzerkommando. Ihr Status ist über
|
||||
`GET /api/projects/:projectId/history` lesbar. Ein zentraler Dispatcher führt
|
||||
`GET /api/projects/:projectId/history` lesbar. Die unveränderliche,
|
||||
absteigend paginierte Revisions-Timeline ist ohne Befehls-Payloads über
|
||||
`GET /api/projects/:projectId/history/revisions` lesbar. Ein zentraler Dispatcher führt
|
||||
die unterstützten Typen `circuit.update` und `circuit-device-row.update` über
|
||||
öffentliche Command-, Undo- und Redo-Endpunkte aus. Zusätzlich sind
|
||||
`circuit-device-row.insert` und `circuit-device-row.delete` als atomare
|
||||
|
||||
@@ -170,6 +170,9 @@ Completed foundation:
|
||||
a forced stack failure rolls all of them back
|
||||
- `GET /api/projects/:projectId/history` exposes current revision, stack depths
|
||||
and top change-set ids
|
||||
- `GET /api/projects/:projectId/history/revisions` exposes a descending,
|
||||
cursor-paginated metadata timeline without leaking persisted forward or
|
||||
inverse command payloads
|
||||
- a central application dispatcher executes the supported `circuit.update`
|
||||
and `circuit-device-row.update` envelopes without coupling the domain service
|
||||
to SQLite
|
||||
@@ -226,7 +229,7 @@ Completed foundation:
|
||||
|
||||
Remaining constraints before completing project version history:
|
||||
|
||||
- add a browsable revision timeline plus named logical snapshots and restore
|
||||
- add a browsable timeline UI plus named logical snapshots and restore
|
||||
- extend project-scoped commands only when further project mutations are
|
||||
deliberately added to history; the generic versioned envelope is complete
|
||||
- do not model IFCGUID as an overloaded circuit equipment identifier
|
||||
|
||||
@@ -364,6 +364,8 @@ Tasks:
|
||||
- [x] persist undo/redo eligibility on the server
|
||||
- [x] expose public command, undo and redo endpoints for the first supported
|
||||
update command types
|
||||
- [x] expose a cursor-paginated, read-only revision metadata timeline without
|
||||
command payloads
|
||||
- add named and periodic logical project snapshots
|
||||
- restore a historical snapshot as a new revision
|
||||
- keep database backups separate from logical history
|
||||
@@ -387,6 +389,8 @@ Implemented foundation:
|
||||
late-failure rollback coverage
|
||||
- a read-only project history endpoint exposes revision, stack depths and top
|
||||
change-set ids
|
||||
- a separate read-only timeline endpoint exposes immutable revision metadata
|
||||
in descending, cursor-paginated pages without forward/inverse payloads
|
||||
- a central application dispatcher reconstructs persisted commands and exposes
|
||||
optimistic command, undo and redo endpoints for Circuit and CircuitDeviceRow
|
||||
field updates
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@
|
||||
"build:api": "tsc -p tsconfig.json",
|
||||
"build:web": "next build",
|
||||
"start": "node dist/server/index.js",
|
||||
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:backup": "tsx scripts/db-backup.ts",
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { and, asc, desc, eq } from "drizzle-orm";
|
||||
import { and, asc, desc, eq, lt } from "drizzle-orm";
|
||||
import { deserializeProjectCommand } from "../../domain/models/project-command.model.js";
|
||||
import type {
|
||||
ProjectHistoryCommand,
|
||||
ProjectHistoryDirection,
|
||||
ListProjectRevisionsInput,
|
||||
ProjectRevisionPage,
|
||||
ProjectHistoryState,
|
||||
ProjectHistoryStore,
|
||||
} from "../../domain/ports/project-history.store.js";
|
||||
import type { ProjectRevisionSource } from "../../domain/ports/project-revision.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { projectChangeSets } from "../schema/project-change-sets.js";
|
||||
import { projectHistoryStackEntries } from "../schema/project-history-stack-entries.js";
|
||||
@@ -47,6 +50,74 @@ export class ProjectHistoryRepository implements ProjectHistoryStore {
|
||||
};
|
||||
}
|
||||
|
||||
listRevisions(
|
||||
input: ListProjectRevisionsInput
|
||||
): ProjectRevisionPage | null {
|
||||
validateRevisionPageInput(input);
|
||||
const project = this.database
|
||||
.select({ currentRevision: projects.currentRevision })
|
||||
.from(projects)
|
||||
.where(eq(projects.id, input.projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const projectCondition = eq(
|
||||
projectRevisions.projectId,
|
||||
input.projectId
|
||||
);
|
||||
const rows = this.database
|
||||
.select({
|
||||
revisionId: projectRevisions.id,
|
||||
changeSetId: projectChangeSets.id,
|
||||
revisionNumber: projectRevisions.revisionNumber,
|
||||
createdAtIso: projectRevisions.createdAtIso,
|
||||
actorId: projectRevisions.actorId,
|
||||
source: projectRevisions.source,
|
||||
description: projectRevisions.description,
|
||||
commandType: projectChangeSets.commandType,
|
||||
payloadSchemaVersion: projectChangeSets.payloadSchemaVersion,
|
||||
})
|
||||
.from(projectRevisions)
|
||||
.innerJoin(
|
||||
projectChangeSets,
|
||||
eq(
|
||||
projectChangeSets.projectRevisionId,
|
||||
projectRevisions.id
|
||||
)
|
||||
)
|
||||
.where(
|
||||
input.beforeRevision === undefined
|
||||
? projectCondition
|
||||
: and(
|
||||
projectCondition,
|
||||
lt(
|
||||
projectRevisions.revisionNumber,
|
||||
input.beforeRevision
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(projectRevisions.revisionNumber))
|
||||
.limit(input.limit + 1)
|
||||
.all();
|
||||
const hasMore = rows.length > input.limit;
|
||||
const revisions = rows.slice(0, input.limit).map((row) => ({
|
||||
...row,
|
||||
source: row.source as ProjectRevisionSource,
|
||||
}));
|
||||
|
||||
return {
|
||||
projectId: input.projectId,
|
||||
currentRevision: project.currentRevision,
|
||||
revisions,
|
||||
nextBeforeRevision:
|
||||
hasMore && revisions.length > 0
|
||||
? revisions.at(-1)!.revisionNumber
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
getNextCommand(
|
||||
projectId: string,
|
||||
direction: ProjectHistoryDirection
|
||||
@@ -93,3 +164,22 @@ export class ProjectHistoryRepository implements ProjectHistoryStore {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function validateRevisionPageInput(input: ListProjectRevisionsInput) {
|
||||
if (
|
||||
!Number.isSafeInteger(input.limit) ||
|
||||
input.limit < 1 ||
|
||||
input.limit > 100
|
||||
) {
|
||||
throw new Error("Project revision page limit must be between 1 and 100.");
|
||||
}
|
||||
if (
|
||||
input.beforeRevision !== undefined &&
|
||||
(!Number.isSafeInteger(input.beforeRevision) ||
|
||||
input.beforeRevision < 1)
|
||||
) {
|
||||
throw new Error(
|
||||
"Project revision cursor must be a positive integer."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SerializedProjectCommand } from "../models/project-command.model.js";
|
||||
import type { ProjectRevisionSource } from "./project-revision.store.js";
|
||||
|
||||
export type ProjectHistoryDirection = "undo" | "redo";
|
||||
|
||||
@@ -16,8 +17,36 @@ export interface ProjectHistoryCommand {
|
||||
command: SerializedProjectCommand;
|
||||
}
|
||||
|
||||
export interface ProjectRevisionSummary {
|
||||
revisionId: string;
|
||||
changeSetId: string;
|
||||
revisionNumber: number;
|
||||
createdAtIso: string;
|
||||
actorId: string | null;
|
||||
source: ProjectRevisionSource;
|
||||
description: string | null;
|
||||
commandType: string;
|
||||
payloadSchemaVersion: number;
|
||||
}
|
||||
|
||||
export interface ProjectRevisionPage {
|
||||
projectId: string;
|
||||
currentRevision: number;
|
||||
revisions: ProjectRevisionSummary[];
|
||||
nextBeforeRevision: number | null;
|
||||
}
|
||||
|
||||
export interface ListProjectRevisionsInput {
|
||||
projectId: string;
|
||||
limit: number;
|
||||
beforeRevision?: number;
|
||||
}
|
||||
|
||||
export interface ProjectHistoryStore {
|
||||
getState(projectId: string): ProjectHistoryState | null;
|
||||
listRevisions(
|
||||
input: ListProjectRevisionsInput
|
||||
): ProjectRevisionPage | null;
|
||||
getNextCommand(
|
||||
projectId: string,
|
||||
direction: ProjectHistoryDirection
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { listProjectRevisionsQuerySchema } from "../../shared/validation/project-history.schemas.js";
|
||||
import { projectHistoryStore } from "../composition/project-command-stores.js";
|
||||
|
||||
export function getProjectHistory(req: Request, res: Response) {
|
||||
@@ -13,3 +14,23 @@ export function getProjectHistory(req: Request, res: Response) {
|
||||
}
|
||||
return res.json(state);
|
||||
}
|
||||
|
||||
export function listProjectRevisions(req: Request, res: Response) {
|
||||
const { projectId } = req.params;
|
||||
if (typeof projectId !== "string" || !projectId.trim()) {
|
||||
return res.status(400).json({ error: "Invalid projectId" });
|
||||
}
|
||||
const parsed = listProjectRevisionsQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
const page = projectHistoryStore.listRevisions({
|
||||
projectId,
|
||||
...parsed.data,
|
||||
});
|
||||
if (!page) {
|
||||
return res.status(404).json({ error: "Project not found" });
|
||||
}
|
||||
return res.json(page);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ import { listCircuitListsByProject } from "../controllers/circuit-list.controlle
|
||||
import { createFloor, listFloorsByProject } from "../controllers/floor.controller.js";
|
||||
import { createRoom, listRoomsByProject } from "../controllers/room.controller.js";
|
||||
import { getCircuitTree } from "../controllers/circuit-tree.controller.js";
|
||||
import { getProjectHistory } from "../controllers/project-history.controller.js";
|
||||
import {
|
||||
getProjectHistory,
|
||||
listProjectRevisions,
|
||||
} from "../controllers/project-history.controller.js";
|
||||
import {
|
||||
executeProjectCommand,
|
||||
redoProjectCommand,
|
||||
@@ -26,6 +29,7 @@ projectRouter.get("/", listProjects);
|
||||
projectRouter.post("/", createProject);
|
||||
projectRouter.get("/:projectId", getProject);
|
||||
projectRouter.get("/:projectId/history", getProjectHistory);
|
||||
projectRouter.get("/:projectId/history/revisions", listProjectRevisions);
|
||||
projectRouter.post("/:projectId/commands", executeProjectCommand);
|
||||
projectRouter.post("/:projectId/history/undo", undoProjectCommand);
|
||||
projectRouter.post("/:projectId/history/redo", redoProjectCommand);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const listProjectRevisionsQuerySchema = z
|
||||
.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).default(25),
|
||||
beforeRevision: z.coerce.number().int().positive().optional(),
|
||||
})
|
||||
.strict();
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user