From 266bdb47491add1b91e806468100ffa40d5d5660 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 26 Jul 2026 11:14:24 +0200 Subject: [PATCH] Centralize structural command transactions --- AGENTS.md | 3 ++ docs/current-architecture.md | 7 +++ docs/spec/07-implementation-phases-todo.md | 3 ++ ...ow-structure-project-command.repository.ts | 38 +++++--------- ...it-structure-project-command.repository.ts | 38 +++++--------- ...rd-structure-project-command.repository.ts | 32 +++--------- ...project-command-transaction.persistence.ts | 49 +++++++++++++++++++ ...ce-structure-project-command.repository.ts | 38 +++++--------- ...on-structure-project-command.repository.ts | 32 +++--------- 9 files changed, 112 insertions(+), 128 deletions(-) create mode 100644 src/db/repositories/project-command-transaction.persistence.ts diff --git a/AGENTS.md b/AGENTS.md index b0da1f6..53d6cfb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,9 @@ It must support circuits, device rows, project devices, drag-and-drop restructur `src/frontend/components/circuit-grid-*.ts` modules. - Critical multi-write commands use injected transaction repositories with real SQLite commit/rollback tests. +- Structural command stores share + `src/db/repositories/project-command-transaction.persistence.ts` for the + atomic domain-write, revision and history transition boundary. - 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 9451835..3d0fe3d 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -84,6 +84,13 @@ absteigend paginierte Revisions-Timeline ist ohne Befehls-Payloads über `GET /api/projects/:projectId/history/revisions` lesbar. Ein zentraler Dispatcher führt die nachfolgend beschriebenen typisierten Kommandos über öffentliche Command-, Undo- und Redo-Endpunkte aus. +Die Identitäts-/Struktur-Stores für Circuit, CircuitDeviceRow, +DistributionBoard, ProjectDevice sowie Geschoss/Raum verwenden dabei +`project-command-transaction.persistence.ts` als gemeinsame äußere +Transaktionsgrenze. Sie führt Fachänderung, Revisions-Append und +History-Transition in fester Reihenfolge innerhalb derselben SQLite-Transaktion +aus; die fachlichen Validierungs- und Inversenregeln bleiben im jeweiligen +Store. `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 4c3da03..9e6ba12 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -346,6 +346,9 @@ Implemented foundation: - obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and Project repository writes are removed; integration fixtures live under `tests/support` instead of production repositories +- structural Circuit, CircuitDeviceRow, DistributionBoard, ProjectDevice and + project-location stores share one tested transaction wrapper for domain + mutation, revision append and history transition - 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/src/db/repositories/circuit-device-row-structure-project-command.repository.ts b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts index 6e1f4de..b53a16a 100644 --- a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts @@ -21,8 +21,7 @@ import { assertCircuitDeviceRowReferencesInProject, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; -import { applyProjectHistoryTransition } from "./project-history.persistence.js"; -import { appendProjectRevision } from "./project-revision.persistence.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class CircuitDeviceRowStructureProjectCommandRepository implements CircuitDeviceRowStructureProjectCommandStore @@ -30,30 +29,17 @@ export class CircuitDeviceRowStructureProjectCommandRepository constructor(private readonly database: AppDatabase) {} execute(input: ExecuteCircuitDeviceRowStructureCommandInput) { - return this.database.transaction((tx) => { - const inverse = this.applyCommand( - tx, - input.projectId, - input.source, - input.command - ); - const revision = appendProjectRevision(tx, { - projectId: input.projectId, - expectedRevision: input.expectedRevision, - source: input.source, - description: input.description, - actorId: input.actorId, - forward: input.command, - inverse, - }); - applyProjectHistoryTransition(tx, { - projectId: input.projectId, - source: input.source, - recordedChangeSetId: revision.changeSetId, - targetChangeSetId: input.historyTargetChangeSetId, - }); - return { revision, inverse }; - }); + return executeProjectCommandTransaction( + this.database, + input, + (tx) => + this.applyCommand( + tx, + input.projectId, + input.source, + input.command + ) + ); } private applyCommand( diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index 8a41ffa..0bc973b 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -22,8 +22,7 @@ import { assertCircuitDeviceRowReferencesInProject, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; -import { applyProjectHistoryTransition } from "./project-history.persistence.js"; -import { appendProjectRevision } from "./project-revision.persistence.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class CircuitStructureProjectCommandRepository implements CircuitStructureProjectCommandStore @@ -31,30 +30,17 @@ export class CircuitStructureProjectCommandRepository constructor(private readonly database: AppDatabase) {} execute(input: ExecuteCircuitStructureCommandInput) { - return this.database.transaction((tx) => { - const inverse = this.applyCommand( - tx, - input.projectId, - input.source, - input.command - ); - const revision = appendProjectRevision(tx, { - projectId: input.projectId, - expectedRevision: input.expectedRevision, - source: input.source, - description: input.description, - actorId: input.actorId, - forward: input.command, - inverse, - }); - applyProjectHistoryTransition(tx, { - projectId: input.projectId, - source: input.source, - recordedChangeSetId: revision.changeSetId, - targetChangeSetId: input.historyTargetChangeSetId, - }); - return { revision, inverse }; - }); + return executeProjectCommandTransaction( + this.database, + input, + (tx) => + this.applyCommand( + tx, + input.projectId, + input.source, + input.command + ) + ); } private applyCommand( diff --git a/src/db/repositories/distribution-board-structure-project-command.repository.ts b/src/db/repositories/distribution-board-structure-project-command.repository.ts index 017a82e..76b3ee0 100644 --- a/src/db/repositories/distribution-board-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-structure-project-command.repository.ts @@ -19,8 +19,7 @@ import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { projects } from "../schema/projects.js"; -import { applyProjectHistoryTransition } from "./project-history.persistence.js"; -import { appendProjectRevision } from "./project-revision.persistence.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class DistributionBoardStructureProjectCommandRepository implements DistributionBoardStructureProjectCommandStore @@ -28,29 +27,12 @@ export class DistributionBoardStructureProjectCommandRepository constructor(private readonly database: AppDatabase) {} execute(input: ExecuteDistributionBoardStructureCommandInput) { - return this.database.transaction((tx) => { - const inverse = this.applyCommand( - tx, - input.projectId, - input.command - ); - const revision = appendProjectRevision(tx, { - projectId: input.projectId, - expectedRevision: input.expectedRevision, - source: input.source, - description: input.description, - actorId: input.actorId, - forward: input.command, - inverse, - }); - applyProjectHistoryTransition(tx, { - projectId: input.projectId, - source: input.source, - recordedChangeSetId: revision.changeSetId, - targetChangeSetId: input.historyTargetChangeSetId, - }); - return { revision, inverse }; - }); + return executeProjectCommandTransaction( + this.database, + input, + (tx) => + this.applyCommand(tx, input.projectId, input.command) + ); } private applyCommand( diff --git a/src/db/repositories/project-command-transaction.persistence.ts b/src/db/repositories/project-command-transaction.persistence.ts new file mode 100644 index 0000000..62d661e --- /dev/null +++ b/src/db/repositories/project-command-transaction.persistence.ts @@ -0,0 +1,49 @@ +import type { SerializedProjectCommand } from "../../domain/models/project-command.model.js"; +import type { + AppendedProjectRevision, + ProjectRevisionSource, +} from "../../domain/ports/project-revision.store.js"; +import type { AppDatabase } from "../database-context.js"; +import { applyProjectHistoryTransition } from "./project-history.persistence.js"; +import { appendProjectRevision } from "./project-revision.persistence.js"; + +export interface ProjectCommandTransactionInput< + Command extends SerializedProjectCommand, +> { + projectId: string; + expectedRevision: number; + source: ProjectRevisionSource; + description?: string; + actorId?: string; + historyTargetChangeSetId?: string; + command: Command; +} + +export function executeProjectCommandTransaction< + Command extends SerializedProjectCommand, + Inverse extends SerializedProjectCommand, +>( + database: AppDatabase, + input: ProjectCommandTransactionInput, + applyCommand: (transaction: AppDatabase) => Inverse +): { revision: AppendedProjectRevision; inverse: Inverse } { + return database.transaction((tx) => { + const inverse = applyCommand(tx); + const revision = appendProjectRevision(tx, { + projectId: input.projectId, + expectedRevision: input.expectedRevision, + source: input.source, + description: input.description, + actorId: input.actorId, + forward: input.command, + inverse, + }); + applyProjectHistoryTransition(tx, { + projectId: input.projectId, + source: input.source, + recordedChangeSetId: revision.changeSetId, + targetChangeSetId: input.historyTargetChangeSetId, + }); + return { revision, inverse }; + }); +} diff --git a/src/db/repositories/project-device-structure-project-command.repository.ts b/src/db/repositories/project-device-structure-project-command.repository.ts index 94ecf2f..1561b18 100644 --- a/src/db/repositories/project-device-structure-project-command.repository.ts +++ b/src/db/repositories/project-device-structure-project-command.repository.ts @@ -27,8 +27,7 @@ import { circuits } from "../schema/circuits.js"; import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js"; -import { applyProjectHistoryTransition } from "./project-history.persistence.js"; -import { appendProjectRevision } from "./project-revision.persistence.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; const circuitDeviceRowSnapshotFields = [ "id", @@ -60,30 +59,17 @@ export class ProjectDeviceStructureProjectCommandRepository constructor(private readonly database: AppDatabase) {} execute(input: ExecuteProjectDeviceStructureCommandInput) { - return this.database.transaction((tx) => { - const inverse = this.applyCommand( - tx, - input.projectId, - input.source, - input.command - ); - const revision = appendProjectRevision(tx, { - projectId: input.projectId, - expectedRevision: input.expectedRevision, - source: input.source, - description: input.description, - actorId: input.actorId, - forward: input.command, - inverse, - }); - applyProjectHistoryTransition(tx, { - projectId: input.projectId, - source: input.source, - recordedChangeSetId: revision.changeSetId, - targetChangeSetId: input.historyTargetChangeSetId, - }); - return { revision, inverse }; - }); + return executeProjectCommandTransaction( + this.database, + input, + (tx) => + this.applyCommand( + tx, + input.projectId, + input.source, + input.command + ) + ); } private applyCommand( diff --git a/src/db/repositories/project-location-structure-project-command.repository.ts b/src/db/repositories/project-location-structure-project-command.repository.ts index aec04b3..e5226a9 100644 --- a/src/db/repositories/project-location-structure-project-command.repository.ts +++ b/src/db/repositories/project-location-structure-project-command.repository.ts @@ -26,8 +26,7 @@ import { consumers } from "../schema/consumers.js"; import { floors } from "../schema/floors.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; -import { applyProjectHistoryTransition } from "./project-history.persistence.js"; -import { appendProjectRevision } from "./project-revision.persistence.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class ProjectLocationStructureProjectCommandRepository implements ProjectLocationStructureProjectCommandStore @@ -35,29 +34,12 @@ export class ProjectLocationStructureProjectCommandRepository constructor(private readonly database: AppDatabase) {} execute(input: ExecuteProjectLocationStructureCommandInput) { - return this.database.transaction((tx) => { - const inverse = this.applyCommand( - tx, - input.projectId, - input.command - ); - const revision = appendProjectRevision(tx, { - projectId: input.projectId, - expectedRevision: input.expectedRevision, - source: input.source, - description: input.description, - actorId: input.actorId, - forward: input.command, - inverse, - }); - applyProjectHistoryTransition(tx, { - projectId: input.projectId, - source: input.source, - recordedChangeSetId: revision.changeSetId, - targetChangeSetId: input.historyTargetChangeSetId, - }); - return { revision, inverse }; - }); + return executeProjectCommandTransaction( + this.database, + input, + (tx) => + this.applyCommand(tx, input.projectId, input.command) + ); } private applyCommand(