Decouple runtime domain services
This commit is contained in:
@@ -28,6 +28,8 @@ It must support circuits, device rows, project devices, drag-and-drop restructur
|
|||||||
forward-command variant preserves derived CircuitDeviceRow override metadata.
|
forward-command variant preserves derived CircuitDeviceRow override metadata.
|
||||||
- Low-level `appendProjectRevision` persistence is adapter-internal and tested
|
- Low-level `appendProjectRevision` persistence is adapter-internal and tested
|
||||||
directly; do not reintroduce a standalone runtime revision repository.
|
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
|
- General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard
|
||||||
repositories expose only active reads. Runtime writes belong in typed command
|
repositories expose only active reads. Runtime writes belong in typed command
|
||||||
repositories; direct integration fixtures belong under `tests/support`.
|
repositories; direct integration fixtures belong under `tests/support`.
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ liegt über einen Host-Mount außerhalb des Containers.
|
|||||||
- `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe
|
- `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe
|
||||||
- `src/server/index.ts` und `src/server/routes/` – API-Komposition
|
- `src/server/index.ts` und `src/server/routes/` – API-Komposition
|
||||||
- `src/domain/services/` – fachliche Command- und Synchronisierungsregeln
|
- `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/repositories/` – Abfragen, Persistenzmapper und Transaktionsadapter
|
||||||
- `src/db/schema/` und `src/db/migrations/` – SQLite-Schema und Migrationen
|
- `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
|
Laden und nach jedem Tree-Reload liest er den persistenten History-Status und
|
||||||
gleicht dessen Revision mit `currentRevision` des Trees ab. Das Datenmodell
|
gleicht dessen Revision mit `currentRevision` des Trees ab. Das Datenmodell
|
||||||
besitzt einen projektbezogenen Revisionszähler sowie getrennte Revision-/Change-Set-
|
besitzt einen projektbezogenen Revisionszähler sowie getrennte Revision-/Change-Set-
|
||||||
Tabellen. Ein getestetes Repository kann diese Historienmetadaten optimistisch
|
Tabellen. Eine direkt getestete Persistence-Funktion schreibt diese
|
||||||
und atomar fortschreiben. Vorwärts- und Rückwärtskommandos besitzen einen
|
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
|
versionierten, JSON-sicheren Umschlag; Typ und Payload können dadurch nach
|
||||||
einem Neustart verlustfrei rekonstruiert werden. Alle aktuell unterstützten
|
einem Neustart verlustfrei rekonstruiert werden. Alle aktuell unterstützten
|
||||||
Circuit-, Gerätezeilen-, Projektgeräte-, Projektstruktur- und
|
Circuit-, Gerätezeilen-, Projektgeräte-, Projektstruktur- und
|
||||||
@@ -96,6 +99,9 @@ Store.
|
|||||||
Die Low-Level-Funktion `appendProjectRevision` bleibt ein internes Detail dieser
|
Die Low-Level-Funktion `appendProjectRevision` bleibt ein internes Detail dieser
|
||||||
Persistenzgrenze und wird direkt mit einer realen SQLite-Transaktion getestet.
|
Persistenzgrenze und wird direkt mit einer realen SQLite-Transaktion getestet.
|
||||||
Ein eigenständiges Runtime-Revisions-Repository existiert nicht.
|
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
|
`circuit-device-row.insert` und `circuit-device-row.delete` sind atomare
|
||||||
Strukturkommandos. Beim Löschen wird die vollständige Zeile im inversen
|
Strukturkommandos. Beim Löschen wird die vollständige Zeile im inversen
|
||||||
Kommando gesichert, sodass Undo dieselbe UUID und alle Fachwerte wiederherstellt.
|
Kommando gesichert, sodass Undo dieselbe UUID und alle Fachwerte wiederherstellt.
|
||||||
|
|||||||
@@ -352,6 +352,8 @@ Implemented foundation:
|
|||||||
CircuitDeviceRow override metadata
|
CircuitDeviceRow override metadata
|
||||||
- low-level revision append persistence is tested directly; the unused
|
- low-level revision append persistence is tested directly; the unused
|
||||||
standalone runtime revision repository and store interface are removed
|
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
|
- 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
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
export interface CircuitNumberingSectionReader {
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
findById(
|
||||||
|
sectionId: string
|
||||||
|
): Promise<{ prefix: string } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CircuitNumberingCircuitReader {
|
||||||
|
listBySection(
|
||||||
|
sectionId: string
|
||||||
|
): Promise<Array<{ equipmentIdentifier: string }>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CircuitNumberingDependencies {
|
||||||
|
sectionRepository: CircuitNumberingSectionReader;
|
||||||
|
circuitRepository: CircuitNumberingCircuitReader;
|
||||||
|
}
|
||||||
|
|
||||||
function parseSuffix(equipmentIdentifier: string, prefix: string): number | null {
|
function parseSuffix(equipmentIdentifier: string, prefix: string): number | null {
|
||||||
if (!equipmentIdentifier.startsWith(prefix)) {
|
if (!equipmentIdentifier.startsWith(prefix)) {
|
||||||
@@ -13,15 +27,12 @@ function parseSuffix(equipmentIdentifier: string, prefix: string): number | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CircuitNumberingService {
|
export class CircuitNumberingService {
|
||||||
private readonly sectionRepository: Pick<CircuitSectionRepository, "findById">;
|
private readonly sectionRepository: CircuitNumberingSectionReader;
|
||||||
private readonly circuitRepository: Pick<CircuitRepository, "listBySection">;
|
private readonly circuitRepository: CircuitNumberingCircuitReader;
|
||||||
|
|
||||||
constructor(deps?: {
|
constructor(deps: CircuitNumberingDependencies) {
|
||||||
sectionRepository?: Pick<CircuitSectionRepository, "findById">;
|
this.sectionRepository = deps.sectionRepository;
|
||||||
circuitRepository?: Pick<CircuitRepository, "listBySection">;
|
this.circuitRepository = deps.circuitRepository;
|
||||||
}) {
|
|
||||||
this.sectionRepository = deps?.sectionRepository ?? new CircuitSectionRepository();
|
|
||||||
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextIdentifier(sectionId: string) {
|
async getNextIdentifier(sectionId: string) {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
|
||||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
|
||||||
import {
|
import {
|
||||||
createProjectDeviceRowSyncProjectCommand,
|
createProjectDeviceRowSyncProjectCommand,
|
||||||
projectDeviceSyncRowSnapshotFields,
|
projectDeviceSyncRowSnapshotFields,
|
||||||
@@ -20,15 +18,44 @@ export {
|
|||||||
serializeOverriddenFields,
|
serializeOverriddenFields,
|
||||||
} from "./project-device-overrides.js";
|
} from "./project-device-overrides.js";
|
||||||
|
|
||||||
type ProjectDevice = NonNullable<Awaited<ReturnType<ProjectDeviceRepository["findById"]>>>;
|
export type ProjectDeviceSyncSource = {
|
||||||
type LinkedRow = Awaited<ReturnType<CircuitDeviceRowRepository["listLinkedByProjectDevice"]>>[number];
|
id: string;
|
||||||
|
} & Pick<ProjectDeviceSyncRowSnapshot, ProjectDeviceSyncField>;
|
||||||
|
|
||||||
type SyncDependencies = {
|
export type LinkedProjectDeviceRow = ProjectDeviceSyncRowSnapshot & {
|
||||||
projectDeviceRepository: Pick<ProjectDeviceRepository, "findById">;
|
id: string;
|
||||||
deviceRowRepository: Pick<CircuitDeviceRowRepository, "listLinkedByProjectDevice">;
|
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<ProjectDeviceSyncSource | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkedProjectDeviceRowReader {
|
||||||
|
listLinkedByProjectDevice(
|
||||||
|
projectId: string,
|
||||||
|
projectDeviceId: string
|
||||||
|
): Promise<LinkedProjectDeviceRow[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectDeviceSyncDependencies {
|
||||||
|
projectDeviceRepository: ProjectDeviceSyncSourceReader;
|
||||||
|
deviceRowRepository: LinkedProjectDeviceRowReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceValue(
|
||||||
|
projectDevice: ProjectDeviceSyncSource,
|
||||||
|
field: ProjectDeviceSyncField
|
||||||
|
) {
|
||||||
return projectDevice[field];
|
return projectDevice[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +63,10 @@ function valuesEqual(left: unknown, right: unknown) {
|
|||||||
return (left ?? null) === (right ?? null);
|
return (left ?? null) === (right ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDifferences(projectDevice: ProjectDevice, row: LinkedRow) {
|
function buildDifferences(
|
||||||
|
projectDevice: ProjectDeviceSyncSource,
|
||||||
|
row: LinkedProjectDeviceRow
|
||||||
|
) {
|
||||||
return projectDeviceSyncFields
|
return projectDeviceSyncFields
|
||||||
.filter((field) => !valuesEqual(row[field], sourceValue(projectDevice, field)))
|
.filter((field) => !valuesEqual(row[field], sourceValue(projectDevice, field)))
|
||||||
.map((field) => ({
|
.map((field) => ({
|
||||||
@@ -48,12 +78,12 @@ function buildDifferences(projectDevice: ProjectDevice, row: LinkedRow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectDeviceSyncService {
|
export class ProjectDeviceSyncService {
|
||||||
private readonly projectDeviceRepository: SyncDependencies["projectDeviceRepository"];
|
private readonly projectDeviceRepository: ProjectDeviceSyncSourceReader;
|
||||||
private readonly deviceRowRepository: SyncDependencies["deviceRowRepository"];
|
private readonly deviceRowRepository: LinkedProjectDeviceRowReader;
|
||||||
|
|
||||||
constructor(deps?: Partial<SyncDependencies>) {
|
constructor(deps: ProjectDeviceSyncDependencies) {
|
||||||
this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository();
|
this.projectDeviceRepository = deps.projectDeviceRepository;
|
||||||
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
|
this.deviceRowRepository = deps.deviceRowRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPreview(projectId: string, projectDeviceId: string) {
|
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 uniqueRowIds = [...new Set(rowIds)];
|
||||||
const byId = new Map(linkedRows.map((row) => [row.id, row]));
|
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) {
|
if (selectedRows.length !== uniqueRowIds.length) {
|
||||||
throw new Error("One or more rows are not linked to this project device.");
|
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 {
|
return {
|
||||||
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
|||||||
@@ -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";
|
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(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -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";
|
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(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe("circuit numbering service", () => {
|
|||||||
const service = new CircuitNumberingService({
|
const service = new CircuitNumberingService({
|
||||||
sectionRepository: {
|
sectionRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
return { id: "s1", prefix: "-2F" } as never;
|
return { prefix: "-2F" };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
@@ -18,7 +18,7 @@ describe("circuit numbering service", () => {
|
|||||||
{ equipmentIdentifier: "-2F5" },
|
{ equipmentIdentifier: "-2F5" },
|
||||||
{ equipmentIdentifier: "-2FX" },
|
{ equipmentIdentifier: "-2FX" },
|
||||||
{ equipmentIdentifier: "-1F9" },
|
{ equipmentIdentifier: "-1F9" },
|
||||||
] as never[];
|
];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,12 +59,12 @@ function createService() {
|
|||||||
const service = new ProjectDeviceSyncService({
|
const service = new ProjectDeviceSyncService({
|
||||||
projectDeviceRepository: {
|
projectDeviceRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
return projectDevice() as never;
|
return projectDevice();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
deviceRowRepository: {
|
deviceRowRepository: {
|
||||||
async listLinkedByProjectDevice() {
|
async listLinkedByProjectDevice() {
|
||||||
return rows as never;
|
return rows;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user