Add component structure commands
This commit is contained in:
@@ -301,6 +301,12 @@ inverse removes only the same unchanged and still-empty structure; the POST
|
|||||||
route requires `expectedRevision` and returns the updated history state.
|
route requires `expectedRevision` and returns the updated history state.
|
||||||
Stored schema-version 1 and 2 setup commands remain executable with their four
|
Stored schema-version 1 and 2 setup commands remain executable with their four
|
||||||
legacy sections and without invented components.
|
legacy sections and without invented components.
|
||||||
|
Mutable group-protection and auxiliary distribution-board components use
|
||||||
|
`distribution-board-component.insert` and
|
||||||
|
`distribution-board-component.delete`. Their complete snapshots include the
|
||||||
|
optional one-to-one protection-device state, and deletion rejects stale data.
|
||||||
|
Fixed main-switch and surge-protection header roles are excluded from these
|
||||||
|
general component commands.
|
||||||
Distribution-board floor assignment and a project-enabled supply type use
|
Distribution-board floor assignment and a project-enabled supply type use
|
||||||
`distribution-board.update`; both values are snapshot/export fields and one
|
`distribution-board.update`; both values are snapshot/export fields and one
|
||||||
persistent undo step.
|
persistent undo step.
|
||||||
|
|||||||
@@ -285,6 +285,16 @@ nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
|
|||||||
Freigabe der Netzart in den Projekteinstellungen.
|
Freigabe der Netzart in den Projekteinstellungen.
|
||||||
Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier
|
Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier
|
||||||
Legacy-Abschnitten und ohne nachträglich erfundene Komponenten ausführbar.
|
Legacy-Abschnitten und ohne nachträglich erfundene Komponenten ausführbar.
|
||||||
|
`distribution-board-component.insert` und
|
||||||
|
`distribution-board-component.delete` versionieren die Anlage und Entfernung
|
||||||
|
veränderlicher Gruppen-Schutzgeräte und zusätzlicher Verteilergeräte. Der
|
||||||
|
vollständige Snapshot enthält die stabile Komponenten-UUID und, für
|
||||||
|
Gruppen-Schutzgeräte, die getrennte 1:1-Schutzgerätekonfiguration. Eigentum an
|
||||||
|
Projekt, Stromkreisliste und Gruppe sowie der unveränderte Löschzustand werden
|
||||||
|
innerhalb derselben Transaktion geprüft. Die festen Kopfkomponenten
|
||||||
|
Hauptschalter und Überspannungsableiter sind von diesen allgemeinen Commands
|
||||||
|
ausgeschlossen. API- und Editorintegration folgen in den nächsten
|
||||||
|
Arbeitspaketen.
|
||||||
`distribution-board.update` versioniert Etage, Netzart und den
|
`distribution-board.update` versioniert Etage, Netzart und den
|
||||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||||
@@ -355,8 +365,9 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
|
|||||||
Ein triggergeführtes Register erzwingt bereits eine normalisierte,
|
Ein triggergeführtes Register erzwingt bereits eine normalisierte,
|
||||||
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
|
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
|
||||||
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden
|
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden
|
||||||
Snapshot-Schema 7; Command- und UI-Integration folgen in den nächsten
|
Snapshot-Schema 7. Persistente Insert/Delete-Commands für veränderliche
|
||||||
abgegrenzten Arbeitspaketen.
|
Verteilerkomponenten sind integriert; weitere Command- und UI-Schritte
|
||||||
|
folgen in abgegrenzten Arbeitspaketen.
|
||||||
|
|
||||||
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
||||||
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
mapping.
|
mapping.
|
||||||
- [x] Phase C1: versioned new-board command with three groups and fixed header
|
- [x] Phase C1: versioned new-board command with three groups and fixed header
|
||||||
components.
|
components.
|
||||||
- [ ] Phase C2: component and group CRUD/reorder commands.
|
- [x] Phase C2a1: persistent insert/delete commands for mutable group
|
||||||
|
protection and auxiliary components.
|
||||||
|
- [ ] Phase C2a2: component update and reorder commands.
|
||||||
|
- [ ] Phase C2b: group CRUD and reorder commands.
|
||||||
- [ ] Phase D: group numbering, moves and destructive operations.
|
- [ ] Phase D: group numbering, moves and destructive operations.
|
||||||
- [ ] Phase E: editor projection and editing.
|
- [ ] Phase E: editor projection and editing.
|
||||||
- [ ] Phase F: documentation and full GUI verification.
|
- [ ] Phase F: documentation and full GUI verification.
|
||||||
|
|||||||
@@ -593,8 +593,9 @@ Acceptance:
|
|||||||
|
|
||||||
### C. Persistent Commands and Board Defaults
|
### C. Persistent Commands and Board Defaults
|
||||||
|
|
||||||
Status: In progress. New-board defaults C1 are complete; component and group
|
Status: In progress. New-board defaults C1 and component insert/delete commands
|
||||||
management commands C2 are pending.
|
C2a1 are complete. Component update/reorder and group management remain
|
||||||
|
pending.
|
||||||
|
|
||||||
- extend `distribution-board.insert` with fixed components and three groups
|
- extend `distribution-board.insert` with fixed components and three groups
|
||||||
- implement component and group CRUD commands
|
- implement component and group CRUD commands
|
||||||
@@ -612,6 +613,19 @@ Implemented in C1:
|
|||||||
- stored schema versions 1 and 2 retain four legacy sections and receive no
|
- stored schema versions 1 and 2 retain four legacy sections and receive no
|
||||||
invented components
|
invented components
|
||||||
|
|
||||||
|
Implemented in C2a1:
|
||||||
|
|
||||||
|
- `distribution-board-component.insert` and
|
||||||
|
`distribution-board-component.delete` persist complete component snapshots
|
||||||
|
with their optional one-to-one protection-device configuration
|
||||||
|
- mutable group protection components validate their group ownership and
|
||||||
|
protection-device combination before writing
|
||||||
|
- auxiliary components remain footer entries without protection-device data
|
||||||
|
- fixed header roles are excluded from these general CRUD commands
|
||||||
|
- deletion requires an exact unchanged snapshot; Undo/Redo restores the same
|
||||||
|
component UUID and a late revision/history failure rolls back the complete
|
||||||
|
write
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
- new boards contain the agreed fixed structure
|
- new boards contain the agreed fixed structure
|
||||||
|
|||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
assertDistributionBoardComponentDeleteProjectCommand,
|
||||||
|
assertDistributionBoardComponentInsertProjectCommand,
|
||||||
|
createDistributionBoardComponentDeleteProjectCommand,
|
||||||
|
createDistributionBoardComponentInsertProjectCommand,
|
||||||
|
distributionBoardComponentDeleteCommandType,
|
||||||
|
distributionBoardComponentInsertCommandType,
|
||||||
|
type DistributionBoardComponentSnapshot,
|
||||||
|
type DistributionBoardComponentStructureProjectCommand,
|
||||||
|
} from "../../domain/models/distribution-board-component-structure-project-command.model.js";
|
||||||
|
import type {
|
||||||
|
DistributionBoardComponentStructureProjectCommandStore,
|
||||||
|
ExecuteDistributionBoardComponentStructureCommandInput,
|
||||||
|
} from "../../domain/ports/distribution-board-component-structure-project-command.store.js";
|
||||||
|
import type { AppDatabase } from "../database-context.js";
|
||||||
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
|
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
|
||||||
|
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||||
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
|
|
||||||
|
export class DistributionBoardComponentStructureProjectCommandRepository
|
||||||
|
implements DistributionBoardComponentStructureProjectCommandStore
|
||||||
|
{
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
|
execute(
|
||||||
|
input: ExecuteDistributionBoardComponentStructureCommandInput
|
||||||
|
) {
|
||||||
|
return executeProjectCommandTransaction(
|
||||||
|
this.database,
|
||||||
|
input,
|
||||||
|
(tx) =>
|
||||||
|
this.applyCommand(
|
||||||
|
tx,
|
||||||
|
input.projectId,
|
||||||
|
input.command
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyCommand(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
command: DistributionBoardComponentStructureProjectCommand
|
||||||
|
): DistributionBoardComponentStructureProjectCommand {
|
||||||
|
if (command.type === distributionBoardComponentInsertCommandType) {
|
||||||
|
assertDistributionBoardComponentInsertProjectCommand(command);
|
||||||
|
this.insert(database, projectId, command.payload.snapshot);
|
||||||
|
return createDistributionBoardComponentDeleteProjectCommand(
|
||||||
|
command.payload.snapshot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (command.type === distributionBoardComponentDeleteCommandType) {
|
||||||
|
assertDistributionBoardComponentDeleteProjectCommand(command);
|
||||||
|
this.delete(database, projectId, command.payload.snapshot);
|
||||||
|
return createDistributionBoardComponentInsertProjectCommand(
|
||||||
|
command.payload.snapshot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
"Unsupported distribution-board component structure command."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private insert(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
snapshot: DistributionBoardComponentSnapshot
|
||||||
|
) {
|
||||||
|
this.assertOwnership(database, projectId, snapshot);
|
||||||
|
const existing = database
|
||||||
|
.select({ id: distributionBoardComponents.id })
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(eq(distributionBoardComponents.id, snapshot.component.id))
|
||||||
|
.get();
|
||||||
|
if (existing) {
|
||||||
|
throw new Error("Distribution-board component id already exists.");
|
||||||
|
}
|
||||||
|
database
|
||||||
|
.insert(distributionBoardComponents)
|
||||||
|
.values(snapshot.component)
|
||||||
|
.run();
|
||||||
|
if (snapshot.protectionDevice !== null) {
|
||||||
|
database
|
||||||
|
.insert(distributionBoardComponentProtectionDevices)
|
||||||
|
.values(snapshot.protectionDevice)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private delete(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
snapshot: DistributionBoardComponentSnapshot
|
||||||
|
) {
|
||||||
|
this.assertOwnership(database, projectId, snapshot);
|
||||||
|
const component = database
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(eq(distributionBoardComponents.id, snapshot.component.id))
|
||||||
|
.get();
|
||||||
|
const protection = database
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponentProtectionDevices)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
distributionBoardComponentProtectionDevices.componentId,
|
||||||
|
snapshot.component.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (
|
||||||
|
!component ||
|
||||||
|
!sameRecord(snapshot.component, component) ||
|
||||||
|
!sameNullableRecord(snapshot.protectionDevice, protection)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component changed before deletion."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const deleted = database
|
||||||
|
.delete(distributionBoardComponents)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(distributionBoardComponents.id, component.id),
|
||||||
|
eq(
|
||||||
|
distributionBoardComponents.circuitListId,
|
||||||
|
snapshot.component.circuitListId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
if (deleted.changes !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component could not be deleted."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertOwnership(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
snapshot: DistributionBoardComponentSnapshot
|
||||||
|
) {
|
||||||
|
const list = database
|
||||||
|
.select({ projectId: circuitLists.projectId })
|
||||||
|
.from(circuitLists)
|
||||||
|
.where(eq(circuitLists.id, snapshot.component.circuitListId))
|
||||||
|
.get();
|
||||||
|
if (!list || list.projectId !== projectId) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component circuit list does not belong to project."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.component.sectionId !== null) {
|
||||||
|
const section = database
|
||||||
|
.select({ circuitListId: circuitSections.circuitListId })
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.id, snapshot.component.sectionId))
|
||||||
|
.get();
|
||||||
|
if (
|
||||||
|
!section ||
|
||||||
|
section.circuitListId !== snapshot.component.circuitListId
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component section does not belong to circuit list."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameNullableRecord(
|
||||||
|
expected: object | null,
|
||||||
|
actual: object | undefined
|
||||||
|
) {
|
||||||
|
return expected === null
|
||||||
|
? actual === undefined
|
||||||
|
: actual !== undefined && sameRecord(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameRecord(expected: object, actual: object) {
|
||||||
|
const expectedEntries = Object.entries(expected);
|
||||||
|
const actualRecord = actual as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
expectedEntries.length === Object.keys(actual).length &&
|
||||||
|
expectedEntries.every(
|
||||||
|
([key, value]) => actualRecord[key] === value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import { distributionBoardComponentPlacements } from "../../shared/constants/distribution-board-component.js";
|
||||||
|
import type { DistributionBoardComponentPlacement } from "../../shared/constants/distribution-board-component.js";
|
||||||
|
import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js";
|
||||||
|
import type {
|
||||||
|
BreakerTripCharacteristic,
|
||||||
|
FuseUtilizationCategory,
|
||||||
|
ProtectionDeviceType,
|
||||||
|
RcdType,
|
||||||
|
} from "../../shared/constants/protection-device.js";
|
||||||
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
|
||||||
|
export const distributionBoardComponentInsertCommandType =
|
||||||
|
"distribution-board-component.insert" as const;
|
||||||
|
export const distributionBoardComponentDeleteCommandType =
|
||||||
|
"distribution-board-component.delete" as const;
|
||||||
|
export const distributionBoardComponentStructureCommandSchemaVersion =
|
||||||
|
1 as const;
|
||||||
|
|
||||||
|
type MutableDistributionBoardComponentRole =
|
||||||
|
| "group_upstream_protection"
|
||||||
|
| "group_residual_current_protection"
|
||||||
|
| "auxiliary";
|
||||||
|
|
||||||
|
export interface DistributionBoardComponentSnapshot {
|
||||||
|
component: {
|
||||||
|
id: string;
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string | null;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
name: string;
|
||||||
|
role: MutableDistributionBoardComponentRole;
|
||||||
|
placement: DistributionBoardComponentPlacement;
|
||||||
|
sortOrder: number;
|
||||||
|
};
|
||||||
|
protectionDevice: {
|
||||||
|
componentId: string;
|
||||||
|
type: ProtectionDeviceType;
|
||||||
|
ratedCurrentA: number;
|
||||||
|
fuseUtilizationCategory: FuseUtilizationCategory | null;
|
||||||
|
tripCharacteristic: BreakerTripCharacteristic | null;
|
||||||
|
rcdType: RcdType | null;
|
||||||
|
ratedResidualCurrentMa: number | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DistributionBoardComponentStructurePayload {
|
||||||
|
snapshot: DistributionBoardComponentSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributionBoardComponentInsertProjectCommand
|
||||||
|
extends SerializedProjectCommand<DistributionBoardComponentStructurePayload> {
|
||||||
|
schemaVersion: typeof distributionBoardComponentStructureCommandSchemaVersion;
|
||||||
|
type: typeof distributionBoardComponentInsertCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributionBoardComponentDeleteProjectCommand
|
||||||
|
extends SerializedProjectCommand<DistributionBoardComponentStructurePayload> {
|
||||||
|
schemaVersion: typeof distributionBoardComponentStructureCommandSchemaVersion;
|
||||||
|
type: typeof distributionBoardComponentDeleteCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DistributionBoardComponentStructureProjectCommand =
|
||||||
|
| DistributionBoardComponentInsertProjectCommand
|
||||||
|
| DistributionBoardComponentDeleteProjectCommand;
|
||||||
|
|
||||||
|
export function createDistributionBoardComponentInsertProjectCommand(
|
||||||
|
snapshot: DistributionBoardComponentSnapshot
|
||||||
|
): DistributionBoardComponentInsertProjectCommand {
|
||||||
|
const command: DistributionBoardComponentInsertProjectCommand = {
|
||||||
|
schemaVersion:
|
||||||
|
distributionBoardComponentStructureCommandSchemaVersion,
|
||||||
|
type: distributionBoardComponentInsertCommandType,
|
||||||
|
payload: { snapshot },
|
||||||
|
};
|
||||||
|
assertDistributionBoardComponentInsertProjectCommand(command);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDistributionBoardComponentDeleteProjectCommand(
|
||||||
|
snapshot: DistributionBoardComponentSnapshot
|
||||||
|
): DistributionBoardComponentDeleteProjectCommand {
|
||||||
|
const command: DistributionBoardComponentDeleteProjectCommand = {
|
||||||
|
schemaVersion:
|
||||||
|
distributionBoardComponentStructureCommandSchemaVersion,
|
||||||
|
type: distributionBoardComponentDeleteCommandType,
|
||||||
|
payload: { snapshot },
|
||||||
|
};
|
||||||
|
assertDistributionBoardComponentDeleteProjectCommand(command);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertDistributionBoardComponentInsertProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>
|
||||||
|
): asserts command is DistributionBoardComponentInsertProjectCommand {
|
||||||
|
assertStructureCommand(
|
||||||
|
command,
|
||||||
|
distributionBoardComponentInsertCommandType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertDistributionBoardComponentDeleteProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>
|
||||||
|
): asserts command is DistributionBoardComponentDeleteProjectCommand {
|
||||||
|
assertStructureCommand(
|
||||||
|
command,
|
||||||
|
distributionBoardComponentDeleteCommandType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertStructureCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>,
|
||||||
|
type:
|
||||||
|
| typeof distributionBoardComponentInsertCommandType
|
||||||
|
| typeof distributionBoardComponentDeleteCommandType
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
command.schemaVersion !==
|
||||||
|
distributionBoardComponentStructureCommandSchemaVersion ||
|
||||||
|
command.type !== type ||
|
||||||
|
!isPlainObject(command.payload) ||
|
||||||
|
Object.keys(command.payload).length !== 1
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Unsupported distribution-board component structure command."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertDistributionBoardComponentSnapshot(command.payload.snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertDistributionBoardComponentSnapshot(
|
||||||
|
value: unknown
|
||||||
|
): asserts value is DistributionBoardComponentSnapshot {
|
||||||
|
if (
|
||||||
|
!isPlainObject(value) ||
|
||||||
|
Object.keys(value).length !== 2 ||
|
||||||
|
!isPlainObject(value.component)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component snapshot is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const component = value.component;
|
||||||
|
if (Object.keys(component).length !== 8) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component snapshot is incomplete."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const field of [
|
||||||
|
"id",
|
||||||
|
"circuitListId",
|
||||||
|
"equipmentIdentifier",
|
||||||
|
"name",
|
||||||
|
] as const) {
|
||||||
|
assertNonEmptyString(component[field], `component.${field}`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
component.role !== "group_upstream_protection" &&
|
||||||
|
component.role !== "group_residual_current_protection" &&
|
||||||
|
component.role !== "auxiliary"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Fixed distribution-board components cannot use component CRUD commands."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!distributionBoardComponentPlacements.includes(
|
||||||
|
component.placement as DistributionBoardComponentPlacement
|
||||||
|
) ||
|
||||||
|
!Number.isFinite(component.sortOrder)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component placement or sort order is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const groupComponent = component.role !== "auxiliary";
|
||||||
|
if (
|
||||||
|
groupComponent !==
|
||||||
|
(component.placement === "group" &&
|
||||||
|
typeof component.sectionId === "string" &&
|
||||||
|
Boolean(component.sectionId.trim()))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Group protection components require group placement and a section."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!groupComponent &&
|
||||||
|
(component.placement !== "footer" ||
|
||||||
|
component.sectionId !== null)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Auxiliary components require footer placement without a section."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupComponent) {
|
||||||
|
if (!isPlainObject(value.protectionDevice)) {
|
||||||
|
throw new Error(
|
||||||
|
"Group protection components require protection data."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertProtectionDevice(
|
||||||
|
value.protectionDevice,
|
||||||
|
component.id as string
|
||||||
|
);
|
||||||
|
} else if (value.protectionDevice !== null) {
|
||||||
|
throw new Error(
|
||||||
|
"Auxiliary components cannot contain protection data."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertProtectionDevice(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
componentId: string
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
Object.keys(value).length !== 7 ||
|
||||||
|
value.componentId !== componentId
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component protection snapshot is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = protectionDeviceConfigurationSchema.safeParse({
|
||||||
|
type: value.type,
|
||||||
|
ratedCurrentA: value.ratedCurrentA,
|
||||||
|
...(value.fuseUtilizationCategory === null
|
||||||
|
? {}
|
||||||
|
: { fuseUtilizationCategory: value.fuseUtilizationCategory }),
|
||||||
|
...(value.tripCharacteristic === null
|
||||||
|
? {}
|
||||||
|
: { tripCharacteristic: value.tripCharacteristic }),
|
||||||
|
...(value.rcdType === null ? {} : { rcdType: value.rcdType }),
|
||||||
|
...(value.ratedResidualCurrentMa === null
|
||||||
|
? {}
|
||||||
|
: { ratedResidualCurrentMa: value.ratedResidualCurrentMa }),
|
||||||
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(
|
||||||
|
"Distribution-board component protection configuration is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNonEmptyString(value: unknown, field: string) {
|
||||||
|
if (typeof value !== "string" || !value.trim()) {
|
||||||
|
throw new Error(`${field} must be a non-empty string.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { DistributionBoardComponentStructureProjectCommand } from "../models/distribution-board-component-structure-project-command.model.js";
|
||||||
|
import type {
|
||||||
|
AppendedProjectRevision,
|
||||||
|
ProjectRevisionSource,
|
||||||
|
} from "./project-revision.store.js";
|
||||||
|
|
||||||
|
export interface ExecuteDistributionBoardComponentStructureCommandInput {
|
||||||
|
projectId: string;
|
||||||
|
expectedRevision: number;
|
||||||
|
source: ProjectRevisionSource;
|
||||||
|
description?: string;
|
||||||
|
actorId?: string;
|
||||||
|
historyTargetChangeSetId?: string;
|
||||||
|
command: DistributionBoardComponentStructureProjectCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributionBoardComponentStructureProjectCommandStore {
|
||||||
|
execute(
|
||||||
|
input: ExecuteDistributionBoardComponentStructureCommandInput
|
||||||
|
): {
|
||||||
|
revision: AppendedProjectRevision;
|
||||||
|
inverse: DistributionBoardComponentStructureProjectCommand;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -41,6 +41,12 @@ import {
|
|||||||
assertDistributionBoardUpdateProjectCommand,
|
assertDistributionBoardUpdateProjectCommand,
|
||||||
distributionBoardUpdateCommandType,
|
distributionBoardUpdateCommandType,
|
||||||
} from "../models/distribution-board-project-command.model.js";
|
} from "../models/distribution-board-project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertDistributionBoardComponentDeleteProjectCommand,
|
||||||
|
assertDistributionBoardComponentInsertProjectCommand,
|
||||||
|
distributionBoardComponentDeleteCommandType,
|
||||||
|
distributionBoardComponentInsertCommandType,
|
||||||
|
} from "../models/distribution-board-component-structure-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
assertDistributionBoardDeleteProjectCommand,
|
assertDistributionBoardDeleteProjectCommand,
|
||||||
assertDistributionBoardInsertProjectCommand,
|
assertDistributionBoardInsertProjectCommand,
|
||||||
@@ -91,6 +97,7 @@ import type { CircuitSectionReorderProjectCommandStore } from "../ports/circuit-
|
|||||||
import type { CircuitSectionRenumberProjectCommandStore } from "../ports/circuit-section-renumber-project-command.store.js";
|
import type { CircuitSectionRenumberProjectCommandStore } from "../ports/circuit-section-renumber-project-command.store.js";
|
||||||
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
|
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
|
||||||
import type { DistributionBoardStructureProjectCommandStore } from "../ports/distribution-board-structure-project-command.store.js";
|
import type { DistributionBoardStructureProjectCommandStore } from "../ports/distribution-board-structure-project-command.store.js";
|
||||||
|
import type { DistributionBoardComponentStructureProjectCommandStore } from "../ports/distribution-board-component-structure-project-command.store.js";
|
||||||
import type {
|
import type {
|
||||||
ExecuteProjectHistoryCommandInput,
|
ExecuteProjectHistoryCommandInput,
|
||||||
ExecutedProjectCommand,
|
ExecutedProjectCommand,
|
||||||
@@ -135,6 +142,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||||||
private readonly projectDeviceRowSyncStore: ProjectDeviceRowSyncProjectCommandStore,
|
private readonly projectDeviceRowSyncStore: ProjectDeviceRowSyncProjectCommandStore,
|
||||||
private readonly projectSettingsStore: ProjectSettingsProjectCommandStore,
|
private readonly projectSettingsStore: ProjectSettingsProjectCommandStore,
|
||||||
private readonly projectStateRestoreStore: ProjectStateRestoreCommandStore,
|
private readonly projectStateRestoreStore: ProjectStateRestoreCommandStore,
|
||||||
|
private readonly distributionBoardComponentStructureStore: DistributionBoardComponentStructureProjectCommandStore,
|
||||||
private readonly historyStore: ProjectHistoryStore
|
private readonly historyStore: ProjectHistoryStore
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -296,6 +304,24 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||||||
command: input.command,
|
command: input.command,
|
||||||
}).revision;
|
}).revision;
|
||||||
}
|
}
|
||||||
|
case distributionBoardComponentInsertCommandType: {
|
||||||
|
assertDistributionBoardComponentInsertProjectCommand(
|
||||||
|
input.command
|
||||||
|
);
|
||||||
|
return this.distributionBoardComponentStructureStore.execute({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
|
case distributionBoardComponentDeleteCommandType: {
|
||||||
|
assertDistributionBoardComponentDeleteProjectCommand(
|
||||||
|
input.command
|
||||||
|
);
|
||||||
|
return this.distributionBoardComponentStructureStore.execute({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
case projectFloorInsertCommandType: {
|
case projectFloorInsertCommandType: {
|
||||||
assertProjectFloorInsertProjectCommand(input.command);
|
assertProjectFloorInsertProjectCommand(input.command);
|
||||||
return this.projectLocationStructureStore.execute({
|
return this.projectLocationStructureStore.execute({
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const commandTypeLabels: Record<string, string> = {
|
|||||||
"distribution-board.insert": "Verteilung angelegt",
|
"distribution-board.insert": "Verteilung angelegt",
|
||||||
"distribution-board.update": "Verteilung bearbeitet",
|
"distribution-board.update": "Verteilung bearbeitet",
|
||||||
"distribution-board.delete": "Verteilung entfernt",
|
"distribution-board.delete": "Verteilung entfernt",
|
||||||
|
"distribution-board-component.insert": "Verteilergerät angelegt",
|
||||||
|
"distribution-board-component.delete": "Verteilergerät entfernt",
|
||||||
"project-floor.insert": "Geschoss angelegt",
|
"project-floor.insert": "Geschoss angelegt",
|
||||||
"project-floor.delete": "Geschoss entfernt",
|
"project-floor.delete": "Geschoss entfernt",
|
||||||
"project-room.insert": "Raum angelegt",
|
"project-room.insert": "Raum angelegt",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { CircuitSectionReorderProjectCommandRepository } from "../../db/reposito
|
|||||||
import { CircuitSectionRenumberProjectCommandRepository } from "../../db/repositories/circuit-section-renumber-project-command.repository.js";
|
import { CircuitSectionRenumberProjectCommandRepository } from "../../db/repositories/circuit-section-renumber-project-command.repository.js";
|
||||||
import { CircuitStructureProjectCommandRepository } from "../../db/repositories/circuit-structure-project-command.repository.js";
|
import { CircuitStructureProjectCommandRepository } from "../../db/repositories/circuit-structure-project-command.repository.js";
|
||||||
import { DistributionBoardStructureProjectCommandRepository } from "../../db/repositories/distribution-board-structure-project-command.repository.js";
|
import { DistributionBoardStructureProjectCommandRepository } from "../../db/repositories/distribution-board-structure-project-command.repository.js";
|
||||||
|
import { DistributionBoardComponentStructureProjectCommandRepository } from "../../db/repositories/distribution-board-component-structure-project-command.repository.js";
|
||||||
import { ProjectHistoryRepository } from "../../db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../../db/repositories/project-history.repository.js";
|
||||||
import { ProjectLocationStructureProjectCommandRepository } from "../../db/repositories/project-location-structure-project-command.repository.js";
|
import { ProjectLocationStructureProjectCommandRepository } from "../../db/repositories/project-location-structure-project-command.repository.js";
|
||||||
import { ProjectDeviceProjectCommandRepository } from "../../db/repositories/project-device-project-command.repository.js";
|
import { ProjectDeviceProjectCommandRepository } from "../../db/repositories/project-device-project-command.repository.js";
|
||||||
@@ -27,6 +28,8 @@ export const circuitStructureProjectCommandStore =
|
|||||||
new CircuitStructureProjectCommandRepository(db);
|
new CircuitStructureProjectCommandRepository(db);
|
||||||
export const distributionBoardStructureProjectCommandStore =
|
export const distributionBoardStructureProjectCommandStore =
|
||||||
new DistributionBoardStructureProjectCommandRepository(db);
|
new DistributionBoardStructureProjectCommandRepository(db);
|
||||||
|
export const distributionBoardComponentStructureProjectCommandStore =
|
||||||
|
new DistributionBoardComponentStructureProjectCommandRepository(db);
|
||||||
export const projectLocationStructureProjectCommandStore =
|
export const projectLocationStructureProjectCommandStore =
|
||||||
new ProjectLocationStructureProjectCommandRepository(db);
|
new ProjectLocationStructureProjectCommandRepository(db);
|
||||||
export const circuitSectionReorderProjectCommandStore =
|
export const circuitSectionReorderProjectCommandStore =
|
||||||
@@ -59,5 +62,6 @@ export const projectCommandService = new ProjectCommandService(
|
|||||||
projectDeviceRowSyncProjectCommandStore,
|
projectDeviceRowSyncProjectCommandStore,
|
||||||
projectSettingsProjectCommandStore,
|
projectSettingsProjectCommandStore,
|
||||||
projectStateRestoreCommandStore,
|
projectStateRestoreCommandStore,
|
||||||
|
distributionBoardComponentStructureProjectCommandStore,
|
||||||
projectHistoryStore
|
projectHistoryStore
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,433 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
|
import {
|
||||||
|
createDatabaseContext,
|
||||||
|
type DatabaseContext,
|
||||||
|
} from "../src/db/database-context.js";
|
||||||
|
import { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-structure-project-command.repository.js";
|
||||||
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
|
import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
||||||
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
|
import { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js";
|
||||||
|
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js";
|
||||||
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
|
import {
|
||||||
|
createDistributionBoardComponentInsertProjectCommand,
|
||||||
|
type DistributionBoardComponentSnapshot,
|
||||||
|
} from "../src/domain/models/distribution-board-component-structure-project-command.model.js";
|
||||||
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||||
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
|
|
||||||
|
function createTestDatabase(): DatabaseContext {
|
||||||
|
const context = createDatabaseContext(":memory:");
|
||||||
|
migrate(context.db, {
|
||||||
|
migrationsFolder: path.resolve("src", "db", "migrations"),
|
||||||
|
});
|
||||||
|
context.db
|
||||||
|
.insert(projects)
|
||||||
|
.values([
|
||||||
|
{ id: "project-1", name: "Projekt" },
|
||||||
|
{ id: "project-2", name: "Fremdprojekt" },
|
||||||
|
])
|
||||||
|
.run();
|
||||||
|
new DistributionBoardFixtureRepository(
|
||||||
|
context.db
|
||||||
|
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getListAndSection(context: DatabaseContext) {
|
||||||
|
const list = context.db.select().from(circuitLists).get();
|
||||||
|
assert.ok(list);
|
||||||
|
const section = context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.circuitListId, list.id))
|
||||||
|
.get();
|
||||||
|
assert.ok(section);
|
||||||
|
return { list, section };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("distribution-board component structure project command", () => {
|
||||||
|
it("inserts an auxiliary component and preserves it through undo and redo", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const { list } = getListAndSection(context);
|
||||||
|
const snapshot: DistributionBoardComponentSnapshot = {
|
||||||
|
component: {
|
||||||
|
id: "component-aux",
|
||||||
|
circuitListId: list.id,
|
||||||
|
sectionId: null,
|
||||||
|
equipmentIdentifier: "-K1",
|
||||||
|
name: "KNX Schaltaktor",
|
||||||
|
role: "auxiliary",
|
||||||
|
placement: "footer",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
protectionDevice: null,
|
||||||
|
};
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
const history = new ProjectHistoryRepository(context.db);
|
||||||
|
const command =
|
||||||
|
createDistributionBoardComponentInsertProjectCommand(snapshot);
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(eq(distributionBoardComponents.id, "component-aux"))
|
||||||
|
.get(),
|
||||||
|
snapshot.component
|
||||||
|
);
|
||||||
|
|
||||||
|
const undo = history.getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undo);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undo.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(eq(distributionBoardComponents.id, "component-aux"))
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const redo = history.getNextCommand("project-1", "redo");
|
||||||
|
assert.ok(redo);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 2,
|
||||||
|
source: "redo",
|
||||||
|
historyTargetChangeSetId: redo.changeSetId,
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
assert.ok(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(eq(distributionBoardComponents.id, "component-aux"))
|
||||||
|
.get()
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores and restores a group RCD with its protection data atomically", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const { list, section } = getListAndSection(context);
|
||||||
|
const snapshot: DistributionBoardComponentSnapshot = {
|
||||||
|
component: {
|
||||||
|
id: "component-rcd",
|
||||||
|
circuitListId: list.id,
|
||||||
|
sectionId: section.id,
|
||||||
|
equipmentIdentifier: "-1Q1.0",
|
||||||
|
name: "Gruppen-FI",
|
||||||
|
role: "group_residual_current_protection",
|
||||||
|
placement: "group",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
protectionDevice: {
|
||||||
|
componentId: "component-rcd",
|
||||||
|
type: "FI",
|
||||||
|
ratedCurrentA: 40,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command:
|
||||||
|
createDistributionBoardComponentInsertProjectCommand(
|
||||||
|
snapshot
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponentProtectionDevices)
|
||||||
|
.get(),
|
||||||
|
snapshot.protectionDevice
|
||||||
|
);
|
||||||
|
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: new ProjectHistoryRepository(
|
||||||
|
context.db
|
||||||
|
).getNextCommand("project-1", "undo")?.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponentProtectionDevices)
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects fixed roles, foreign sections, duplicate BMKs and stale revisions", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const { list } = getListAndSection(context);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createDistributionBoardComponentInsertProjectCommand({
|
||||||
|
component: {
|
||||||
|
id: "fixed",
|
||||||
|
circuitListId: list.id,
|
||||||
|
sectionId: null,
|
||||||
|
equipmentIdentifier: "-Q1",
|
||||||
|
name: "Hauptschalter",
|
||||||
|
role: "main_switch",
|
||||||
|
placement: "header",
|
||||||
|
sortOrder: 10,
|
||||||
|
} as never,
|
||||||
|
protectionDevice: null,
|
||||||
|
}),
|
||||||
|
/Fixed distribution-board components/
|
||||||
|
);
|
||||||
|
const snapshot: DistributionBoardComponentSnapshot = {
|
||||||
|
component: {
|
||||||
|
id: "duplicate",
|
||||||
|
circuitListId: list.id,
|
||||||
|
sectionId: null,
|
||||||
|
equipmentIdentifier: " -q0 ",
|
||||||
|
name: "Doppelt",
|
||||||
|
role: "auxiliary",
|
||||||
|
placement: "footer",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
protectionDevice: null,
|
||||||
|
};
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
const foreignBoard = new DistributionBoardFixtureRepository(
|
||||||
|
context.db
|
||||||
|
).createWithCircuitListAndDefaultSections(
|
||||||
|
"project-2",
|
||||||
|
"UV fremd"
|
||||||
|
);
|
||||||
|
const foreignSection = context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(
|
||||||
|
eq(circuitSections.circuitListId, foreignBoard.id)
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
assert.ok(foreignSection);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command:
|
||||||
|
createDistributionBoardComponentInsertProjectCommand({
|
||||||
|
component: {
|
||||||
|
id: "foreign-section",
|
||||||
|
circuitListId: list.id,
|
||||||
|
sectionId: foreignSection.id,
|
||||||
|
equipmentIdentifier: "-1Q2.0",
|
||||||
|
name: "Fremder Gruppen-FI",
|
||||||
|
role: "group_residual_current_protection",
|
||||||
|
placement: "group",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
protectionDevice: {
|
||||||
|
componentId: "foreign-section",
|
||||||
|
type: "FI",
|
||||||
|
ratedCurrentA: 40,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
/section does not belong/
|
||||||
|
);
|
||||||
|
const staleSnapshot: DistributionBoardComponentSnapshot = {
|
||||||
|
component: {
|
||||||
|
...snapshot.component,
|
||||||
|
id: "stale",
|
||||||
|
equipmentIdentifier: "-K-stale",
|
||||||
|
},
|
||||||
|
protectionDevice: null,
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command:
|
||||||
|
createDistributionBoardComponentInsertProjectCommand(
|
||||||
|
staleSnapshot
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
ProjectRevisionConflictError
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command:
|
||||||
|
createDistributionBoardComponentInsertProjectCommand(
|
||||||
|
snapshot
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/UNIQUE constraint failed/
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
context.db.select().from(projectRevisions).all().length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects changed deletes and rolls back late history failures", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const { list } = getListAndSection(context);
|
||||||
|
const snapshot: DistributionBoardComponentSnapshot = {
|
||||||
|
component: {
|
||||||
|
id: "component-change",
|
||||||
|
circuitListId: list.id,
|
||||||
|
sectionId: null,
|
||||||
|
equipmentIdentifier: "-K2",
|
||||||
|
name: "Aktor",
|
||||||
|
role: "auxiliary",
|
||||||
|
placement: "footer",
|
||||||
|
sortOrder: 20,
|
||||||
|
},
|
||||||
|
protectionDevice: null,
|
||||||
|
};
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command:
|
||||||
|
createDistributionBoardComponentInsertProjectCommand(
|
||||||
|
snapshot
|
||||||
|
),
|
||||||
|
});
|
||||||
|
context.db
|
||||||
|
.update(distributionBoardComponents)
|
||||||
|
.set({ name: "Direkt geändert" })
|
||||||
|
.where(eq(distributionBoardComponents.id, snapshot.component.id))
|
||||||
|
.run();
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: new ProjectHistoryRepository(
|
||||||
|
context.db
|
||||||
|
).getNextCommand("project-1", "undo")?.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
}),
|
||||||
|
/changed before deletion/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const rollback = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const { list } = getListAndSection(rollback);
|
||||||
|
rollback.sqlite.exec(`
|
||||||
|
CREATE TRIGGER fail_component_history
|
||||||
|
BEFORE INSERT ON project_history_stack_entries
|
||||||
|
BEGIN
|
||||||
|
SELECT RAISE(ABORT, 'forced component history failure');
|
||||||
|
END;
|
||||||
|
`);
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||||
|
rollback.db
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command:
|
||||||
|
createDistributionBoardComponentInsertProjectCommand({
|
||||||
|
component: {
|
||||||
|
id: "component-rollback",
|
||||||
|
circuitListId: list.id,
|
||||||
|
sectionId: null,
|
||||||
|
equipmentIdentifier: "-K3",
|
||||||
|
name: "Aktor",
|
||||||
|
role: "auxiliary",
|
||||||
|
placement: "footer",
|
||||||
|
sortOrder: 30,
|
||||||
|
},
|
||||||
|
protectionDevice: null,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
/forced component history failure/
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
rollback.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
distributionBoardComponents.id,
|
||||||
|
"component-rollback"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rollback.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
distributionBoardComponentRoles,
|
distributionBoardComponentRoles,
|
||||||
} from "../src/shared/constants/distribution-board-component.js";
|
} from "../src/shared/constants/distribution-board-component.js";
|
||||||
import { circuitGroupCategories } from "../src/shared/constants/circuit-group.js";
|
import { circuitGroupCategories } from "../src/shared/constants/circuit-group.js";
|
||||||
|
import "./distribution-board-component-structure-project-command.repository.test.js";
|
||||||
|
|
||||||
describe("distribution board component catalog", () => {
|
describe("distribution board component catalog", () => {
|
||||||
it("defines the agreed component roles and placement zones", () => {
|
it("defines the agreed component roles and placement zones", () => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/reposi
|
|||||||
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
||||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
||||||
|
import { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-structure-project-command.repository.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
||||||
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
||||||
@@ -26,6 +27,7 @@ import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/
|
|||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
|
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js";
|
||||||
import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
||||||
import { projectDevices } from "../src/db/schema/project-devices.js";
|
import { projectDevices } from "../src/db/schema/project-devices.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
@@ -49,6 +51,7 @@ import {
|
|||||||
createDistributionBoardInsertProjectCommand,
|
createDistributionBoardInsertProjectCommand,
|
||||||
createDistributionBoardStructureSnapshot,
|
createDistributionBoardStructureSnapshot,
|
||||||
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
||||||
|
import { createDistributionBoardComponentInsertProjectCommand } from "../src/domain/models/distribution-board-component-structure-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
createProjectFloorInsertProjectCommand,
|
createProjectFloorInsertProjectCommand,
|
||||||
createProjectFloorSnapshot,
|
createProjectFloorSnapshot,
|
||||||
@@ -122,6 +125,9 @@ function createService(context: DatabaseContext) {
|
|||||||
new ProjectDeviceRowSyncProjectCommandRepository(context.db),
|
new ProjectDeviceRowSyncProjectCommandRepository(context.db),
|
||||||
new ProjectSettingsProjectCommandRepository(context.db),
|
new ProjectSettingsProjectCommandRepository(context.db),
|
||||||
new ProjectStateRestoreCommandRepository(context.db),
|
new ProjectStateRestoreCommandRepository(context.db),
|
||||||
|
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
),
|
||||||
new ProjectHistoryRepository(context.db)
|
new ProjectHistoryRepository(context.db)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1059,6 +1065,62 @@ describe("project command service", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dispatches distribution-board component insertion and deletion", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const service = createService(context);
|
||||||
|
const created = service.executeUser({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
command:
|
||||||
|
createDistributionBoardComponentInsertProjectCommand({
|
||||||
|
component: {
|
||||||
|
id: "component-service",
|
||||||
|
circuitListId: context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.get()!.circuitListId,
|
||||||
|
sectionId: null,
|
||||||
|
equipmentIdentifier: "-K10",
|
||||||
|
name: "KNX Aktor",
|
||||||
|
role: "auxiliary",
|
||||||
|
placement: "footer",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
protectionDevice: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(created.history.currentRevision, 1);
|
||||||
|
assert.ok(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(
|
||||||
|
eq(distributionBoardComponents.id, "component-service")
|
||||||
|
)
|
||||||
|
.get()
|
||||||
|
);
|
||||||
|
|
||||||
|
const undone = createService(context).undo({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
});
|
||||||
|
assert.equal(undone.history.currentRevision, 2);
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(
|
||||||
|
eq(distributionBoardComponents.id, "component-service")
|
||||||
|
)
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("dispatches floor and room setup with their persisted inverses", () => {
|
it("dispatches floor and room setup with their persisted inverses", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { CircuitSectionReorderProjectCommandRepository } from "../src/db/reposit
|
|||||||
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
||||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
||||||
|
import { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-structure-project-command.repository.js";
|
||||||
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
||||||
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
||||||
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
||||||
@@ -191,6 +192,9 @@ function createService(context: DatabaseContext) {
|
|||||||
new ProjectDeviceRowSyncProjectCommandRepository(context.db),
|
new ProjectDeviceRowSyncProjectCommandRepository(context.db),
|
||||||
new ProjectSettingsProjectCommandRepository(context.db),
|
new ProjectSettingsProjectCommandRepository(context.db),
|
||||||
new ProjectStateRestoreCommandRepository(context.db),
|
new ProjectStateRestoreCommandRepository(context.db),
|
||||||
|
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
),
|
||||||
new ProjectHistoryRepository(context.db)
|
new ProjectHistoryRepository(context.db)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,22 @@ describe("project version history presentation", () => {
|
|||||||
),
|
),
|
||||||
"Projekteinstellungen bearbeitet"
|
"Projekteinstellungen bearbeitet"
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
getProjectRevisionDescription(
|
||||||
|
revision(5, {
|
||||||
|
commandType: "distribution-board-component.insert",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
"Verteilergerät angelegt"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
getProjectRevisionDescription(
|
||||||
|
revision(6, {
|
||||||
|
commandType: "distribution-board-component.delete",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
"Verteilergerät entfernt"
|
||||||
|
);
|
||||||
assert.equal(getProjectSnapshotKindLabel("named"), "Benannt");
|
assert.equal(getProjectSnapshotKindLabel("named"), "Benannt");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
getProjectSnapshotKindLabel("automatic"),
|
getProjectSnapshotKindLabel("automatic"),
|
||||||
|
|||||||
Reference in New Issue
Block a user