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.
|
||||
Stored schema-version 1 and 2 setup commands remain executable with their four
|
||||
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.update`; both values are snapshot/export fields and one
|
||||
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.
|
||||
Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier
|
||||
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
|
||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||
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,
|
||||
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
|
||||
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden
|
||||
Snapshot-Schema 7; Command- und UI-Integration folgen in den nächsten
|
||||
abgegrenzten Arbeitspaketen.
|
||||
Snapshot-Schema 7. Persistente Insert/Delete-Commands für veränderliche
|
||||
Verteilerkomponenten sind integriert; weitere Command- und UI-Schritte
|
||||
folgen in abgegrenzten Arbeitspaketen.
|
||||
|
||||
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
||||
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
||||
|
||||
@@ -20,7 +20,10 @@ requirements and intended sequencing, not proof of implementation.
|
||||
mapping.
|
||||
- [x] Phase C1: versioned new-board command with three groups and fixed header
|
||||
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 E: editor projection and editing.
|
||||
- [ ] Phase F: documentation and full GUI verification.
|
||||
|
||||
@@ -593,8 +593,9 @@ Acceptance:
|
||||
|
||||
### C. Persistent Commands and Board Defaults
|
||||
|
||||
Status: In progress. New-board defaults C1 are complete; component and group
|
||||
management commands C2 are pending.
|
||||
Status: In progress. New-board defaults C1 and component insert/delete commands
|
||||
C2a1 are complete. Component update/reorder and group management remain
|
||||
pending.
|
||||
|
||||
- extend `distribution-board.insert` with fixed components and three groups
|
||||
- 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
|
||||
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:
|
||||
|
||||
- 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,
|
||||
distributionBoardUpdateCommandType,
|
||||
} from "../models/distribution-board-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardComponentDeleteProjectCommand,
|
||||
assertDistributionBoardComponentInsertProjectCommand,
|
||||
distributionBoardComponentDeleteCommandType,
|
||||
distributionBoardComponentInsertCommandType,
|
||||
} from "../models/distribution-board-component-structure-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardDeleteProjectCommand,
|
||||
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 { CircuitStructureProjectCommandStore } from "../ports/circuit-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 {
|
||||
ExecuteProjectHistoryCommandInput,
|
||||
ExecutedProjectCommand,
|
||||
@@ -135,6 +142,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
private readonly projectDeviceRowSyncStore: ProjectDeviceRowSyncProjectCommandStore,
|
||||
private readonly projectSettingsStore: ProjectSettingsProjectCommandStore,
|
||||
private readonly projectStateRestoreStore: ProjectStateRestoreCommandStore,
|
||||
private readonly distributionBoardComponentStructureStore: DistributionBoardComponentStructureProjectCommandStore,
|
||||
private readonly historyStore: ProjectHistoryStore
|
||||
) {}
|
||||
|
||||
@@ -296,6 +304,24 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
command: input.command,
|
||||
}).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: {
|
||||
assertProjectFloorInsertProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
|
||||
@@ -33,6 +33,8 @@ const commandTypeLabels: Record<string, string> = {
|
||||
"distribution-board.insert": "Verteilung angelegt",
|
||||
"distribution-board.update": "Verteilung bearbeitet",
|
||||
"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.delete": "Geschoss entfernt",
|
||||
"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 { CircuitStructureProjectCommandRepository } from "../../db/repositories/circuit-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 { ProjectLocationStructureProjectCommandRepository } from "../../db/repositories/project-location-structure-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);
|
||||
export const distributionBoardStructureProjectCommandStore =
|
||||
new DistributionBoardStructureProjectCommandRepository(db);
|
||||
export const distributionBoardComponentStructureProjectCommandStore =
|
||||
new DistributionBoardComponentStructureProjectCommandRepository(db);
|
||||
export const projectLocationStructureProjectCommandStore =
|
||||
new ProjectLocationStructureProjectCommandRepository(db);
|
||||
export const circuitSectionReorderProjectCommandStore =
|
||||
@@ -59,5 +62,6 @@ export const projectCommandService = new ProjectCommandService(
|
||||
projectDeviceRowSyncProjectCommandStore,
|
||||
projectSettingsProjectCommandStore,
|
||||
projectStateRestoreCommandStore,
|
||||
distributionBoardComponentStructureProjectCommandStore,
|
||||
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,
|
||||
} from "../src/shared/constants/distribution-board-component.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", () => {
|
||||
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 { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.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 { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-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 { circuitSections } from "../src/db/schema/circuit-sections.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 { projectDevices } from "../src/db/schema/project-devices.js";
|
||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||
@@ -49,6 +51,7 @@ import {
|
||||
createDistributionBoardInsertProjectCommand,
|
||||
createDistributionBoardStructureSnapshot,
|
||||
} 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 {
|
||||
createProjectFloorInsertProjectCommand,
|
||||
createProjectFloorSnapshot,
|
||||
@@ -122,6 +125,9 @@ function createService(context: DatabaseContext) {
|
||||
new ProjectDeviceRowSyncProjectCommandRepository(context.db),
|
||||
new ProjectSettingsProjectCommandRepository(context.db),
|
||||
new ProjectStateRestoreCommandRepository(context.db),
|
||||
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||
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", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { CircuitSectionReorderProjectCommandRepository } from "../src/db/reposit
|
||||
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.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 { 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";
|
||||
@@ -191,6 +192,9 @@ function createService(context: DatabaseContext) {
|
||||
new ProjectDeviceRowSyncProjectCommandRepository(context.db),
|
||||
new ProjectSettingsProjectCommandRepository(context.db),
|
||||
new ProjectStateRestoreCommandRepository(context.db),
|
||||
new DistributionBoardComponentStructureProjectCommandRepository(
|
||||
context.db
|
||||
),
|
||||
new ProjectHistoryRepository(context.db)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,6 +115,22 @@ describe("project version history presentation", () => {
|
||||
),
|
||||
"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("automatic"),
|
||||
|
||||
Reference in New Issue
Block a user