diff --git a/AGENTS.md b/AGENTS.md index edbfd94..e6dddce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,8 @@ It must support circuits, device rows, project devices, drag-and-drop restructur `src/db/repositories/project-command-transaction.persistence.ts` for the atomic domain-write, revision and history transition boundary. Its applied forward-command variant preserves derived CircuitDeviceRow override metadata. +- Low-level `appendProjectRevision` persistence is adapter-internal and tested + directly; do not reintroduce a standalone runtime revision repository. - General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard repositories expose only active reads. Runtime writes belong in typed command repositories; direct integration fixtures belong under `tests/support`. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 79b5762..ec7cc80 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -93,6 +93,9 @@ aus. Für CircuitDeviceRow-Feldänderungen übernimmt eine typisierte Variante d während der Fachänderung um Override-Metadaten ergänzten Forward-Command. Die fachlichen Validierungs-, Forward- und Inversenregeln bleiben im jeweiligen Store. +Die Low-Level-Funktion `appendProjectRevision` bleibt ein internes Detail dieser +Persistenzgrenze und wird direkt mit einer realen SQLite-Transaktion getestet. +Ein eigenständiges Runtime-Revisions-Repository existiert nicht. `circuit-device-row.insert` und `circuit-device-row.delete` sind atomare Strukturkommandos. Beim Löschen wird die vollständige Zeile im inversen Kommando gesichert, sodass Undo dieselbe UUID und alle Fachwerte wiederherstellt. diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index 97ec57d..36641b6 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -350,6 +350,8 @@ Implemented foundation: restore, share one tested transaction wrapper for domain mutation, revision append and history transition; its applied-forward variant retains derived CircuitDeviceRow override metadata +- low-level revision append persistence is tested directly; the unused + standalone runtime revision repository and store interface are removed - the legacy consumer UI and application read/write endpoints are removed after verified data cutover - retained legacy rows are accessible only through explicit database upgrade tooling - project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields diff --git a/package.json b/package.json index 47a697c..dec1a70 100644 --- a/package.json +++ b/package.json @@ -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-structure-project-command.repository.test.ts tests/project-location-structure-project-command.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-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.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-structure-project-command.repository.test.ts tests/project-location-structure-project-command.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-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.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-structure-project-command.repository.test.ts tests/project-location-structure-project-command.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.persistence.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-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.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-structure-project-command.repository.test.ts tests/project-location-structure-project-command.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.persistence.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-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.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", diff --git a/src/db/repositories/project-revision.repository.ts b/src/db/repositories/project-revision.repository.ts deleted file mode 100644 index 0fd6539..0000000 --- a/src/db/repositories/project-revision.repository.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { eq } from "drizzle-orm"; -import { ProjectRevisionConflictError } from "../../domain/errors/project-revision-conflict.error.js"; -import type { - AppendProjectRevisionInput, - AppendedProjectRevision, - ProjectRevisionStore, -} from "../../domain/ports/project-revision.store.js"; -import type { AppDatabase } from "../database-context.js"; -import { projects } from "../schema/projects.js"; -import { appendProjectRevision } from "./project-revision.persistence.js"; - -export { ProjectRevisionConflictError }; - -export class ProjectRevisionRepository implements ProjectRevisionStore { - constructor(private readonly database: AppDatabase) {} - - getCurrentRevision(projectId: string) { - const project = this.database - .select({ currentRevision: projects.currentRevision }) - .from(projects) - .where(eq(projects.id, projectId)) - .get(); - return project?.currentRevision ?? null; - } - - append(input: AppendProjectRevisionInput): AppendedProjectRevision { - return this.database.transaction((tx) => - appendProjectRevision(tx, input) - ); - } -} diff --git a/src/domain/ports/project-revision.store.ts b/src/domain/ports/project-revision.store.ts index bac2bf7..fd236d1 100644 --- a/src/domain/ports/project-revision.store.ts +++ b/src/domain/ports/project-revision.store.ts @@ -24,8 +24,3 @@ export interface AppendedProjectRevision { revisionNumber: number; createdAtIso: string; } - -export interface ProjectRevisionStore { - getCurrentRevision(projectId: string): number | null; - append(input: AppendProjectRevisionInput): AppendedProjectRevision; -} diff --git a/tests/project-revision.repository.test.ts b/tests/project-revision.persistence.test.ts similarity index 77% rename from tests/project-revision.repository.test.ts rename to tests/project-revision.persistence.test.ts index 8077a82..264d8c0 100644 --- a/tests/project-revision.repository.test.ts +++ b/tests/project-revision.persistence.test.ts @@ -5,12 +5,11 @@ import { eq } from "drizzle-orm"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext, + type AppDatabase, type DatabaseContext, } from "../src/db/database-context.js"; -import { - ProjectRevisionConflictError, - ProjectRevisionRepository, -} from "../src/db/repositories/project-revision.repository.js"; +import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js"; +import { appendProjectRevision } from "../src/db/repositories/project-revision.persistence.js"; import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js"; import { projectChangeSets } from "../src/db/schema/project-change-sets.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; @@ -27,40 +26,50 @@ function createTestDatabase(): DatabaseContext { } function appendTestRevision( - repository: ProjectRevisionRepository, + database: AppDatabase, expectedRevision: number ) { - return repository.append({ - projectId: "project-1", - expectedRevision, - source: "user", - description: "Stromkreis bearbeiten", - actorId: "test-user", - forward: { - schemaVersion: 1, - type: "circuit.update", - payload: { circuitId: "circuit-1", displayName: "Neu" }, - }, - inverse: { - schemaVersion: 1, - type: "circuit.update", - payload: { circuitId: "circuit-1", displayName: "Alt" }, - }, - }); + return database.transaction((tx) => + appendProjectRevision(tx, { + projectId: "project-1", + expectedRevision, + source: "user", + description: "Stromkreis bearbeiten", + actorId: "test-user", + forward: { + schemaVersion: 1, + type: "circuit.update", + payload: { circuitId: "circuit-1", displayName: "Neu" }, + }, + inverse: { + schemaVersion: 1, + type: "circuit.update", + payload: { circuitId: "circuit-1", displayName: "Alt" }, + }, + }) + ); } -describe("project revision repository", () => { +function getCurrentRevision(database: AppDatabase, projectId: string) { + return ( + database + .select({ currentRevision: projects.currentRevision }) + .from(projects) + .where(eq(projects.id, projectId)) + .get()?.currentRevision ?? null + ); +} + +describe("project revision persistence", () => { it("appends immutable revision metadata and payloads with a monotonic number", () => { const context = createTestDatabase(); try { - const repository = new ProjectRevisionRepository(context.db); - - assert.equal(repository.getCurrentRevision("project-1"), 0); - const appended = appendTestRevision(repository, 0); + assert.equal(getCurrentRevision(context.db, "project-1"), 0); + const appended = appendTestRevision(context.db, 0); assert.equal(appended.projectId, "project-1"); assert.equal(appended.revisionNumber, 1); - assert.equal(repository.getCurrentRevision("project-1"), 1); + assert.equal(getCurrentRevision(context.db, "project-1"), 1); const revision = context.db .select() @@ -105,9 +114,9 @@ describe("project revision repository", () => { }), }); - const next = appendTestRevision(repository, 1); + const next = appendTestRevision(context.db, 1); assert.equal(next.revisionNumber, 2); - assert.equal(repository.getCurrentRevision("project-1"), 2); + assert.equal(getCurrentRevision(context.db, "project-1"), 2); } finally { context.close(); } @@ -116,18 +125,17 @@ describe("project revision repository", () => { it("rejects a stale expected revision without writing history", () => { const context = createTestDatabase(); try { - const repository = new ProjectRevisionRepository(context.db); - appendTestRevision(repository, 0); + appendTestRevision(context.db, 0); assert.throws( - () => appendTestRevision(repository, 0), + () => appendTestRevision(context.db, 0), (error) => error instanceof ProjectRevisionConflictError && error.expectedRevision === 0 && error.actualRevision === 1 ); - assert.equal(repository.getCurrentRevision("project-1"), 1); + assert.equal(getCurrentRevision(context.db, "project-1"), 1); assert.equal(context.db.select().from(projectRevisions).all().length, 1); assert.equal(context.db.select().from(projectChangeSets).all().length, 1); } finally { @@ -145,14 +153,12 @@ describe("project revision repository", () => { SELECT RAISE(ABORT, 'forced change-set failure'); END; `); - const repository = new ProjectRevisionRepository(context.db); - assert.throws( - () => appendTestRevision(repository, 0), + () => appendTestRevision(context.db, 0), /forced change-set failure/ ); - assert.equal(repository.getCurrentRevision("project-1"), 0); + assert.equal(getCurrentRevision(context.db, "project-1"), 0); assert.equal(context.db.select().from(projectRevisions).all().length, 0); assert.equal(context.db.select().from(projectChangeSets).all().length, 0); } finally { @@ -163,7 +169,6 @@ describe("project revision repository", () => { it("captures and retains automatic snapshots without deleting named snapshots", () => { const context = createTestDatabase(); try { - const revisions = new ProjectRevisionRepository(context.db); const snapshots = new ProjectSnapshotRepository(context.db); const named = snapshots.createNamed({ projectId: "project-1", @@ -177,7 +182,7 @@ describe("project revision repository", () => { expectedRevision < 325; expectedRevision += 1 ) { - appendTestRevision(revisions, expectedRevision); + appendTestRevision(context.db, expectedRevision); } const stored = context.db @@ -234,13 +239,12 @@ describe("project revision repository", () => { it("rolls back revision 25 when automatic snapshot persistence fails", () => { const context = createTestDatabase(); try { - const repository = new ProjectRevisionRepository(context.db); for ( let expectedRevision = 0; expectedRevision < 24; expectedRevision += 1 ) { - appendTestRevision(repository, expectedRevision); + appendTestRevision(context.db, expectedRevision); } context.sqlite.exec(` CREATE TRIGGER fail_automatic_project_snapshot @@ -252,10 +256,10 @@ describe("project revision repository", () => { `); assert.throws( - () => appendTestRevision(repository, 24), + () => appendTestRevision(context.db, 24), /forced automatic snapshot failure/ ); - assert.equal(repository.getCurrentRevision("project-1"), 24); + assert.equal(getCurrentRevision(context.db, "project-1"), 24); assert.equal( context.db.select().from(projectRevisions).all().length, 24