Add component structure commands

This commit is contained in:
2026-07-30 19:38:48 +02:00
parent 9d4d4082fe
commit 604604f056
15 changed files with 1058 additions and 5 deletions
@@ -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({