From c1be85e408b8e7ea60e2d0a925e2af804673f2e6 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 26 Jul 2026 12:04:50 +0200 Subject: [PATCH] Decouple runtime domain services --- AGENTS.md | 2 + docs/current-architecture.md | 10 ++- docs/spec/07-implementation-phases-todo.md | 2 + .../services/circuit-numbering.service.ts | 31 +++++--- .../services/project-device-sync.service.ts | 71 ++++++++++++++----- .../composition/circuit-numbering-service.ts | 7 +- .../project-device-sync-service.ts | 7 +- tests/circuit-numbering.service.test.ts | 4 +- tests/project-device-sync.service.test.ts | 4 +- 9 files changed, 103 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e6dddce..e14e3f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,8 @@ It must support circuits, device rows, project devices, drag-and-drop restructur 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. +- Runtime domain services receive narrow reader/store dependencies explicitly; + concrete SQLite repositories are instantiated only under `src/server/composition`. - 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 ec7cc80..7acf29f 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -47,6 +47,8 @@ liegt über einen Host-Mount außerhalb des Containers. - `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe - `src/server/index.ts` und `src/server/routes/` – API-Komposition - `src/domain/services/` – fachliche Command- und Synchronisierungsregeln +- `src/server/composition/` – Verdrahtung fachlicher Services mit konkreten + SQLite-Repositories - `src/db/repositories/` – Abfragen, Persistenzmapper und Transaktionsadapter - `src/db/schema/` und `src/db/migrations/` – SQLite-Schema und Migrationen @@ -66,8 +68,9 @@ Der Editor besitzt keinen sitzungslokalen Undo-/Redo-Stapel mehr. Beim initialen Laden und nach jedem Tree-Reload liest er den persistenten History-Status und gleicht dessen Revision mit `currentRevision` des Trees ab. Das Datenmodell besitzt einen projektbezogenen Revisionszähler sowie getrennte Revision-/Change-Set- -Tabellen. Ein getestetes Repository kann diese Historienmetadaten optimistisch -und atomar fortschreiben. Vorwärts- und Rückwärtskommandos besitzen einen +Tabellen. Eine direkt getestete Persistence-Funktion schreibt diese +Historienmetadaten innerhalb der zentralen Command-Transaktion optimistisch und +atomar fort. Vorwärts- und Rückwärtskommandos besitzen einen versionierten, JSON-sicheren Umschlag; Typ und Payload können dadurch nach einem Neustart verlustfrei rekonstruiert werden. Alle aktuell unterstützten Circuit-, Gerätezeilen-, Projektgeräte-, Projektstruktur- und @@ -96,6 +99,9 @@ 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. +`ProjectDeviceSyncService` und `CircuitNumberingService` kennen nur schmale, +fachlich benannte Reader-Interfaces. Ihre SQLite-Repositories werden +ausschließlich in `src/server/composition/` erzeugt und injiziert. `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 36641b6..ef50e0b 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -352,6 +352,8 @@ Implemented foundation: CircuitDeviceRow override metadata - low-level revision append persistence is tested directly; the unused standalone runtime revision repository and store interface are removed +- runtime project-device synchronization and circuit-numbering services depend + on narrow domain readers; server composition injects the SQLite repositories - 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/domain/services/circuit-numbering.service.ts b/src/domain/services/circuit-numbering.service.ts index c883d7e..5802d55 100644 --- a/src/domain/services/circuit-numbering.service.ts +++ b/src/domain/services/circuit-numbering.service.ts @@ -1,5 +1,19 @@ -import { CircuitRepository } from "../../db/repositories/circuit.repository.js"; -import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js"; +export interface CircuitNumberingSectionReader { + findById( + sectionId: string + ): Promise<{ prefix: string } | null>; +} + +export interface CircuitNumberingCircuitReader { + listBySection( + sectionId: string + ): Promise>; +} + +export interface CircuitNumberingDependencies { + sectionRepository: CircuitNumberingSectionReader; + circuitRepository: CircuitNumberingCircuitReader; +} function parseSuffix(equipmentIdentifier: string, prefix: string): number | null { if (!equipmentIdentifier.startsWith(prefix)) { @@ -13,15 +27,12 @@ function parseSuffix(equipmentIdentifier: string, prefix: string): number | null } export class CircuitNumberingService { - private readonly sectionRepository: Pick; - private readonly circuitRepository: Pick; + private readonly sectionRepository: CircuitNumberingSectionReader; + private readonly circuitRepository: CircuitNumberingCircuitReader; - constructor(deps?: { - sectionRepository?: Pick; - circuitRepository?: Pick; - }) { - this.sectionRepository = deps?.sectionRepository ?? new CircuitSectionRepository(); - this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository(); + constructor(deps: CircuitNumberingDependencies) { + this.sectionRepository = deps.sectionRepository; + this.circuitRepository = deps.circuitRepository; } async getNextIdentifier(sectionId: string) { diff --git a/src/domain/services/project-device-sync.service.ts b/src/domain/services/project-device-sync.service.ts index aac9db1..574ecb6 100644 --- a/src/domain/services/project-device-sync.service.ts +++ b/src/domain/services/project-device-sync.service.ts @@ -1,5 +1,3 @@ -import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js"; -import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js"; import { createProjectDeviceRowSyncProjectCommand, projectDeviceSyncRowSnapshotFields, @@ -20,15 +18,44 @@ export { serializeOverriddenFields, } from "./project-device-overrides.js"; -type ProjectDevice = NonNullable>>; -type LinkedRow = Awaited>[number]; +export type ProjectDeviceSyncSource = { + id: string; +} & Pick; -type SyncDependencies = { - projectDeviceRepository: Pick; - deviceRowRepository: Pick; +export type LinkedProjectDeviceRow = ProjectDeviceSyncRowSnapshot & { + id: string; + circuitId: string; + equipmentIdentifier: string; + circuitDisplayName: string | null; + circuitListId: string; + circuitListName: string; + distributionBoardId: string; + distributionBoardName: string; }; -function sourceValue(projectDevice: ProjectDevice, field: ProjectDeviceSyncField) { +export interface ProjectDeviceSyncSourceReader { + findById( + projectId: string, + projectDeviceId: string + ): Promise; +} + +export interface LinkedProjectDeviceRowReader { + listLinkedByProjectDevice( + projectId: string, + projectDeviceId: string + ): Promise; +} + +export interface ProjectDeviceSyncDependencies { + projectDeviceRepository: ProjectDeviceSyncSourceReader; + deviceRowRepository: LinkedProjectDeviceRowReader; +} + +function sourceValue( + projectDevice: ProjectDeviceSyncSource, + field: ProjectDeviceSyncField +) { return projectDevice[field]; } @@ -36,7 +63,10 @@ function valuesEqual(left: unknown, right: unknown) { return (left ?? null) === (right ?? null); } -function buildDifferences(projectDevice: ProjectDevice, row: LinkedRow) { +function buildDifferences( + projectDevice: ProjectDeviceSyncSource, + row: LinkedProjectDeviceRow +) { return projectDeviceSyncFields .filter((field) => !valuesEqual(row[field], sourceValue(projectDevice, field))) .map((field) => ({ @@ -48,12 +78,12 @@ function buildDifferences(projectDevice: ProjectDevice, row: LinkedRow) { } export class ProjectDeviceSyncService { - private readonly projectDeviceRepository: SyncDependencies["projectDeviceRepository"]; - private readonly deviceRowRepository: SyncDependencies["deviceRowRepository"]; + private readonly projectDeviceRepository: ProjectDeviceSyncSourceReader; + private readonly deviceRowRepository: LinkedProjectDeviceRowReader; - constructor(deps?: Partial) { - this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository(); - this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository(); + constructor(deps: ProjectDeviceSyncDependencies) { + this.projectDeviceRepository = deps.projectDeviceRepository; + this.deviceRowRepository = deps.deviceRowRepository; } async getPreview(projectId: string, projectDeviceId: string) { @@ -154,10 +184,15 @@ export class ProjectDeviceSyncService { ); } - private resolveSelectedRows(linkedRows: LinkedRow[], rowIds: string[]) { + private resolveSelectedRows( + linkedRows: LinkedProjectDeviceRow[], + rowIds: string[] + ) { const uniqueRowIds = [...new Set(rowIds)]; const byId = new Map(linkedRows.map((row) => [row.id, row])); - const selectedRows = uniqueRowIds.map((rowId) => byId.get(rowId)).filter(Boolean) as LinkedRow[]; + const selectedRows = uniqueRowIds + .map((rowId) => byId.get(rowId)) + .filter(Boolean) as LinkedProjectDeviceRow[]; if (selectedRows.length !== uniqueRowIds.length) { throw new Error("One or more rows are not linked to this project device."); } @@ -166,7 +201,9 @@ export class ProjectDeviceSyncService { } -function toSyncSnapshot(row: LinkedRow): ProjectDeviceSyncRowSnapshot { +function toSyncSnapshot( + row: LinkedProjectDeviceRow +): ProjectDeviceSyncRowSnapshot { return { linkedProjectDeviceId: row.linkedProjectDeviceId, name: row.name, diff --git a/src/server/composition/circuit-numbering-service.ts b/src/server/composition/circuit-numbering-service.ts index 0d3d6c5..265c16b 100644 --- a/src/server/composition/circuit-numbering-service.ts +++ b/src/server/composition/circuit-numbering-service.ts @@ -1,3 +1,8 @@ +import { CircuitRepository } from "../../db/repositories/circuit.repository.js"; +import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js"; import { CircuitNumberingService } from "../../domain/services/circuit-numbering.service.js"; -export const circuitNumberingService = new CircuitNumberingService(); +export const circuitNumberingService = new CircuitNumberingService({ + sectionRepository: new CircuitSectionRepository(), + circuitRepository: new CircuitRepository(), +}); diff --git a/src/server/composition/project-device-sync-service.ts b/src/server/composition/project-device-sync-service.ts index b5c53a9..26f67d7 100644 --- a/src/server/composition/project-device-sync-service.ts +++ b/src/server/composition/project-device-sync-service.ts @@ -1,3 +1,8 @@ +import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js"; +import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js"; import { ProjectDeviceSyncService } from "../../domain/services/project-device-sync.service.js"; -export const projectDeviceSyncService = new ProjectDeviceSyncService(); +export const projectDeviceSyncService = new ProjectDeviceSyncService({ + projectDeviceRepository: new ProjectDeviceRepository(), + deviceRowRepository: new CircuitDeviceRowRepository(), +}); diff --git a/tests/circuit-numbering.service.test.ts b/tests/circuit-numbering.service.test.ts index 487c148..28afa6b 100644 --- a/tests/circuit-numbering.service.test.ts +++ b/tests/circuit-numbering.service.test.ts @@ -7,7 +7,7 @@ describe("circuit numbering service", () => { const service = new CircuitNumberingService({ sectionRepository: { async findById() { - return { id: "s1", prefix: "-2F" } as never; + return { prefix: "-2F" }; }, }, circuitRepository: { @@ -18,7 +18,7 @@ describe("circuit numbering service", () => { { equipmentIdentifier: "-2F5" }, { equipmentIdentifier: "-2FX" }, { equipmentIdentifier: "-1F9" }, - ] as never[]; + ]; }, }, }); diff --git a/tests/project-device-sync.service.test.ts b/tests/project-device-sync.service.test.ts index 9ca6703..e5a2191 100644 --- a/tests/project-device-sync.service.test.ts +++ b/tests/project-device-sync.service.test.ts @@ -59,12 +59,12 @@ function createService() { const service = new ProjectDeviceSyncService({ projectDeviceRepository: { async findById() { - return projectDevice() as never; + return projectDevice(); }, }, deviceRowRepository: { async listLinkedByProjectDevice() { - return rows as never; + return rows; }, }, });