Centralize update command transactions

This commit is contained in:
2026-07-26 11:24:19 +02:00
parent 0f306d9a90
commit e4d22caf09
6 changed files with 175 additions and 195 deletions
+3 -2
View File
@@ -21,8 +21,9 @@ It must support circuits, device rows, project devices, drag-and-drop restructur
`src/frontend/components/circuit-grid-*.ts` modules. `src/frontend/components/circuit-grid-*.ts` modules.
- Critical multi-write commands use injected transaction repositories with real - Critical multi-write commands use injected transaction repositories with real
SQLite commit/rollback tests. SQLite commit/rollback tests.
- Compatible multi-write command stores for structure, device-row moves, - Compatible command stores for Circuit and ProjectDevice field updates,
section reorder/renumber and project-device row synchronization share project settings, structure, device-row moves, section reorder/renumber and
project-device row synchronization share
`src/db/repositories/project-command-transaction.persistence.ts` for the `src/db/repositories/project-command-transaction.persistence.ts` for the
atomic domain-write, revision and history transition boundary. atomic domain-write, revision and history transition boundary.
- General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard - General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard
+5 -4
View File
@@ -84,10 +84,11 @@ absteigend paginierte Revisions-Timeline ist ohne Befehls-Payloads über
`GET /api/projects/:projectId/history/revisions` lesbar. Ein zentraler `GET /api/projects/:projectId/history/revisions` lesbar. Ein zentraler
Dispatcher führt die nachfolgend beschriebenen typisierten Kommandos über Dispatcher führt die nachfolgend beschriebenen typisierten Kommandos über
öffentliche Command-, Undo- und Redo-Endpunkte aus. öffentliche Command-, Undo- und Redo-Endpunkte aus.
Die kompatiblen Multi-Write-Stores für Circuit-/CircuitDeviceRow-Struktur, Die kompatiblen Command-Stores für Circuit- und ProjectDevice-Feldänderungen,
Gerätezeilen-Moves, Abschnittssortierung/-umnummerierung, Projekteinstellungen, Circuit-/CircuitDeviceRow-Struktur, Gerätezeilen-Moves,
DistributionBoard-Struktur, ProjectDevice-Struktur und -Zeilensynchronisierung Abschnittssortierung/-umnummerierung, DistributionBoard-Struktur,
sowie Geschoss/Raum verwenden dabei ProjectDevice-Struktur und -Zeilensynchronisierung sowie Geschoss/Raum verwenden
dabei
`project-command-transaction.persistence.ts` als gemeinsame äußere `project-command-transaction.persistence.ts` als gemeinsame äußere
Transaktionsgrenze. Sie führt Fachänderung, Revisions-Append und Transaktionsgrenze. Sie führt Fachänderung, Revisions-Append und
History-Transition in fester Reihenfolge innerhalb derselben SQLite-Transaktion History-Transition in fester Reihenfolge innerhalb derselben SQLite-Transaktion
+4 -4
View File
@@ -346,10 +346,10 @@ Implemented foundation:
- obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and - obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and
Project repository writes are removed; integration fixtures live under Project repository writes are removed; integration fixtures live under
`tests/support` instead of production repositories `tests/support` instead of production repositories
- compatible structure, device-row move, section reorder/renumber, - compatible Circuit/ProjectDevice update, project-settings, structure,
ProjectDevice row-sync and project-location stores share one tested device-row move, section reorder/renumber, ProjectDevice row-sync and
transaction wrapper for domain mutation, revision append and history project-location stores share one tested transaction wrapper for domain
transition mutation, revision append and history transition
- the legacy consumer UI and application read/write endpoints are removed after verified data cutover - 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 - retained legacy rows are accessible only through explicit database upgrade tooling
- project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields - project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields
@@ -18,8 +18,7 @@ import {
toCircuitPatchValues, toCircuitPatchValues,
type CircuitPatchPersistenceInput, type CircuitPatchPersistenceInput,
} from "./circuit.persistence.js"; } from "./circuit.persistence.js";
import { applyProjectHistoryTransition } from "./project-history.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { appendProjectRevision } from "./project-revision.persistence.js";
type CircuitRow = typeof circuits.$inferSelect; type CircuitRow = typeof circuits.$inferSelect;
@@ -31,7 +30,17 @@ export class CircuitProjectCommandRepository
executeUpdate(input: ExecuteCircuitUpdateCommandInput) { executeUpdate(input: ExecuteCircuitUpdateCommandInput) {
assertCircuitUpdateProjectCommand(input.command); assertCircuitUpdateProjectCommand(input.command);
return this.database.transaction((tx) => { return executeProjectCommandTransaction(
this.database,
input,
(tx) => this.applyCommand(tx, input)
);
}
private applyCommand(
tx: AppDatabase,
input: ExecuteCircuitUpdateCommandInput
) {
const current = tx const current = tx
.select() .select()
.from(circuits) .from(circuits)
@@ -88,24 +97,7 @@ export class CircuitProjectCommandRepository
throw new Error("Circuit changed before command execution."); throw new Error("Circuit changed before command execution.");
} }
const revision = appendProjectRevision(tx, { return inverse;
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 };
});
} }
private assertSectionInCircuitList( private assertSectionInCircuitList(
@@ -12,8 +12,7 @@ import type {
} from "../../domain/ports/project-device-project-command.store.js"; } from "../../domain/ports/project-device-project-command.store.js";
import type { AppDatabase } from "../database-context.js"; import type { AppDatabase } from "../database-context.js";
import { projectDevices } from "../schema/project-devices.js"; import { projectDevices } from "../schema/project-devices.js";
import { applyProjectHistoryTransition } from "./project-history.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { appendProjectRevision } from "./project-revision.persistence.js";
type ProjectDeviceRow = typeof projectDevices.$inferSelect; type ProjectDeviceRow = typeof projectDevices.$inferSelect;
@@ -25,7 +24,17 @@ export class ProjectDeviceProjectCommandRepository
executeUpdate(input: ExecuteProjectDeviceUpdateCommandInput) { executeUpdate(input: ExecuteProjectDeviceUpdateCommandInput) {
assertProjectDeviceUpdateProjectCommand(input.command); assertProjectDeviceUpdateProjectCommand(input.command);
return this.database.transaction((tx) => { return executeProjectCommandTransaction(
this.database,
input,
(tx) => this.applyCommand(tx, input)
);
}
private applyCommand(
tx: AppDatabase,
input: ExecuteProjectDeviceUpdateCommandInput
) {
const current = tx const current = tx
.select() .select()
.from(projectDevices) .from(projectDevices)
@@ -78,23 +87,7 @@ export class ProjectDeviceProjectCommandRepository
); );
} }
const revision = appendProjectRevision(tx, { return inverse;
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 };
});
} }
} }
@@ -9,8 +9,7 @@ import type {
} from "../../domain/ports/project-settings-project-command.store.js"; } from "../../domain/ports/project-settings-project-command.store.js";
import type { AppDatabase } from "../database-context.js"; import type { AppDatabase } from "../database-context.js";
import { projects } from "../schema/projects.js"; import { projects } from "../schema/projects.js";
import { applyProjectHistoryTransition } from "./project-history.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { appendProjectRevision } from "./project-revision.persistence.js";
export class ProjectSettingsProjectCommandRepository export class ProjectSettingsProjectCommandRepository
implements ProjectSettingsProjectCommandStore implements ProjectSettingsProjectCommandStore
@@ -19,7 +18,17 @@ export class ProjectSettingsProjectCommandRepository
executeUpdate(input: ExecuteProjectSettingsUpdateCommandInput) { executeUpdate(input: ExecuteProjectSettingsUpdateCommandInput) {
assertProjectSettingsUpdateProjectCommand(input.command); assertProjectSettingsUpdateProjectCommand(input.command);
return this.database.transaction((tx) => { return executeProjectCommandTransaction(
this.database,
input,
(tx) => this.applyCommand(tx, input)
);
}
private applyCommand(
tx: AppDatabase,
input: ExecuteProjectSettingsUpdateCommandInput
) {
const current = tx const current = tx
.select() .select()
.from(projects) .from(projects)
@@ -50,22 +59,6 @@ export class ProjectSettingsProjectCommandRepository
throw new Error("Project changed before settings update."); throw new Error("Project changed before settings update.");
} }
const revision = appendProjectRevision(tx, { return inverse;
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 };
});
} }
} }