forked from jappel/leistungsbilanz-ts
323 lines
10 KiB
TypeScript
323 lines
10 KiB
TypeScript
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 distributionBoardComponentUpdateCommandType =
|
|
"distribution-board-component.update" 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;
|
|
}
|
|
|
|
interface DistributionBoardComponentUpdatePayload {
|
|
expected: DistributionBoardComponentSnapshot;
|
|
target: 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 interface DistributionBoardComponentUpdateProjectCommand
|
|
extends SerializedProjectCommand<DistributionBoardComponentUpdatePayload> {
|
|
schemaVersion: typeof distributionBoardComponentStructureCommandSchemaVersion;
|
|
type: typeof distributionBoardComponentUpdateCommandType;
|
|
}
|
|
|
|
export type DistributionBoardComponentStructureProjectCommand =
|
|
| DistributionBoardComponentInsertProjectCommand
|
|
| DistributionBoardComponentDeleteProjectCommand
|
|
| DistributionBoardComponentUpdateProjectCommand;
|
|
|
|
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 createDistributionBoardComponentUpdateProjectCommand(
|
|
expected: DistributionBoardComponentSnapshot,
|
|
target: DistributionBoardComponentSnapshot
|
|
): DistributionBoardComponentUpdateProjectCommand {
|
|
const command: DistributionBoardComponentUpdateProjectCommand = {
|
|
schemaVersion:
|
|
distributionBoardComponentStructureCommandSchemaVersion,
|
|
type: distributionBoardComponentUpdateCommandType,
|
|
payload: { expected, target },
|
|
};
|
|
assertDistributionBoardComponentUpdateProjectCommand(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
|
|
);
|
|
}
|
|
|
|
export function assertDistributionBoardComponentUpdateProjectCommand(
|
|
command: SerializedProjectCommand<unknown>
|
|
): asserts command is DistributionBoardComponentUpdateProjectCommand {
|
|
if (
|
|
command.schemaVersion !==
|
|
distributionBoardComponentStructureCommandSchemaVersion ||
|
|
command.type !== distributionBoardComponentUpdateCommandType ||
|
|
!isPlainObject(command.payload) ||
|
|
Object.keys(command.payload).length !== 2
|
|
) {
|
|
throw new Error(
|
|
"Unsupported distribution-board component update command."
|
|
);
|
|
}
|
|
assertDistributionBoardComponentSnapshot(command.payload.expected);
|
|
assertDistributionBoardComponentSnapshot(command.payload.target);
|
|
assertSameComponentIdentity(
|
|
command.payload.expected,
|
|
command.payload.target
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function assertSameComponentIdentity(
|
|
expected: DistributionBoardComponentSnapshot,
|
|
target: DistributionBoardComponentSnapshot
|
|
) {
|
|
for (const field of [
|
|
"id",
|
|
"circuitListId",
|
|
"sectionId",
|
|
"role",
|
|
"placement",
|
|
] as const) {
|
|
if (expected.component[field] !== target.component[field]) {
|
|
throw new Error(
|
|
"Distribution-board component updates cannot change ownership or role."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|