570 lines
16 KiB
TypeScript
570 lines
16 KiB
TypeScript
import crypto from "node:crypto";
|
|
import {
|
|
distributionBoardSupplyTypes,
|
|
type DistributionBoardSupplyType,
|
|
} from "../../shared/constants/distribution-board.js";
|
|
import {
|
|
defaultMainSwitchComponent,
|
|
defaultSurgeProtectiveDeviceComponent,
|
|
} from "../../shared/constants/distribution-board-component.js";
|
|
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
|
|
import type {
|
|
DistributionBoardComponentPlacement,
|
|
DistributionBoardComponentRole,
|
|
} from "../../shared/constants/distribution-board-component.js";
|
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
|
|
|
export const distributionBoardInsertCommandType =
|
|
"distribution-board.insert" as const;
|
|
export const distributionBoardDeleteCommandType =
|
|
"distribution-board.delete" as const;
|
|
export const legacyDistributionBoardStructureCommandSchemaVersion = 1 as const;
|
|
export const previousDistributionBoardStructureCommandSchemaVersion = 2 as const;
|
|
export const distributionBoardStructureCommandSchemaVersion = 3 as const;
|
|
|
|
const legacyDefaultCircuitSectionDefinitions = [
|
|
{
|
|
key: "lighting",
|
|
displayName: "Lighting",
|
|
prefix: "-1F",
|
|
sortOrder: 10,
|
|
},
|
|
{
|
|
key: "single_phase",
|
|
displayName: "Single-phase circuits",
|
|
prefix: "-2F",
|
|
sortOrder: 20,
|
|
},
|
|
{
|
|
key: "three_phase",
|
|
displayName: "Three-phase circuits",
|
|
prefix: "-3F",
|
|
sortOrder: 30,
|
|
},
|
|
{
|
|
key: "unassigned",
|
|
displayName: "Unassigned",
|
|
prefix: "-UF",
|
|
sortOrder: 90,
|
|
},
|
|
] as const;
|
|
|
|
export const defaultCircuitSectionDefinitions = [
|
|
{
|
|
key: "lighting",
|
|
displayName: "Beleuchtung 1",
|
|
prefix: "-1F1.",
|
|
sortOrder: 10,
|
|
category: "lighting",
|
|
groupNumber: 1,
|
|
},
|
|
{
|
|
key: "single_phase",
|
|
displayName: "1-phasig 1",
|
|
prefix: "-2F1.",
|
|
sortOrder: 20,
|
|
category: "single_phase",
|
|
groupNumber: 1,
|
|
},
|
|
{
|
|
key: "three_phase",
|
|
displayName: "3-phasig 1",
|
|
prefix: "-3F1.",
|
|
sortOrder: 30,
|
|
category: "three_phase",
|
|
groupNumber: 1,
|
|
},
|
|
] as const;
|
|
|
|
interface LegacyDistributionBoardStructureSnapshot {
|
|
distributionBoard: {
|
|
id: string;
|
|
projectId: string;
|
|
name: string;
|
|
};
|
|
circuitList: DistributionBoardStructureSnapshot["circuitList"];
|
|
sections: LegacyCircuitSectionSnapshot[];
|
|
}
|
|
|
|
interface PreviousDistributionBoardStructureSnapshot {
|
|
distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"];
|
|
circuitList: DistributionBoardStructureSnapshot["circuitList"];
|
|
sections: LegacyCircuitSectionSnapshot[];
|
|
}
|
|
|
|
interface LegacyCircuitSectionSnapshot {
|
|
id: string;
|
|
circuitListId: string;
|
|
key: string;
|
|
displayName: string;
|
|
prefix: string;
|
|
sortOrder: number;
|
|
}
|
|
|
|
export interface DistributionBoardStructureSnapshot {
|
|
distributionBoard: {
|
|
id: string;
|
|
projectId: string;
|
|
name: string;
|
|
floorId: string | null;
|
|
supplyType: DistributionBoardSupplyType | null;
|
|
};
|
|
circuitList: {
|
|
id: string;
|
|
projectId: string;
|
|
distributionBoardId: string;
|
|
name: string;
|
|
};
|
|
sections: Array<{
|
|
id: string;
|
|
circuitListId: string;
|
|
key: string;
|
|
displayName: string;
|
|
prefix: string;
|
|
sortOrder: number;
|
|
category: CircuitGroupCategory;
|
|
groupNumber: number;
|
|
}>;
|
|
components: Array<{
|
|
id: string;
|
|
circuitListId: string;
|
|
sectionId: null;
|
|
equipmentIdentifier: string;
|
|
name: string;
|
|
role: DistributionBoardComponentRole;
|
|
placement: DistributionBoardComponentPlacement;
|
|
sortOrder: number;
|
|
}>;
|
|
}
|
|
|
|
export interface NormalizedDistributionBoardStructureSnapshot {
|
|
distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"];
|
|
circuitList: DistributionBoardStructureSnapshot["circuitList"];
|
|
sections: Array<
|
|
LegacyCircuitSectionSnapshot & {
|
|
category: CircuitGroupCategory | null;
|
|
groupNumber: number | null;
|
|
}
|
|
>;
|
|
components: DistributionBoardStructureSnapshot["components"];
|
|
}
|
|
|
|
interface DistributionBoardStructureCommandPayload<
|
|
TStructure = DistributionBoardStructureSnapshot,
|
|
> {
|
|
structure: TStructure;
|
|
}
|
|
|
|
interface CurrentDistributionBoardStructureProjectCommand
|
|
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
|
|
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
|
|
type:
|
|
| typeof distributionBoardInsertCommandType
|
|
| typeof distributionBoardDeleteCommandType;
|
|
}
|
|
|
|
interface LegacyDistributionBoardStructureProjectCommand
|
|
extends SerializedProjectCommand<
|
|
DistributionBoardStructureCommandPayload<LegacyDistributionBoardStructureSnapshot>
|
|
> {
|
|
schemaVersion: typeof legacyDistributionBoardStructureCommandSchemaVersion;
|
|
type:
|
|
| typeof distributionBoardInsertCommandType
|
|
| typeof distributionBoardDeleteCommandType;
|
|
}
|
|
|
|
interface PreviousDistributionBoardStructureProjectCommand
|
|
extends SerializedProjectCommand<
|
|
DistributionBoardStructureCommandPayload<PreviousDistributionBoardStructureSnapshot>
|
|
> {
|
|
schemaVersion: typeof previousDistributionBoardStructureCommandSchemaVersion;
|
|
type:
|
|
| typeof distributionBoardInsertCommandType
|
|
| typeof distributionBoardDeleteCommandType;
|
|
}
|
|
|
|
export interface DistributionBoardInsertProjectCommand
|
|
extends CurrentDistributionBoardStructureProjectCommand {
|
|
type: typeof distributionBoardInsertCommandType;
|
|
}
|
|
|
|
export interface DistributionBoardDeleteProjectCommand
|
|
extends CurrentDistributionBoardStructureProjectCommand {
|
|
type: typeof distributionBoardDeleteCommandType;
|
|
}
|
|
|
|
export type DistributionBoardStructureProjectCommand =
|
|
| DistributionBoardInsertProjectCommand
|
|
| DistributionBoardDeleteProjectCommand
|
|
| PreviousDistributionBoardStructureProjectCommand
|
|
| LegacyDistributionBoardStructureProjectCommand;
|
|
|
|
export function createDistributionBoardStructureSnapshot(
|
|
projectId: string,
|
|
name: string,
|
|
input: {
|
|
floorId?: string | null;
|
|
supplyType?: DistributionBoardSupplyType | null;
|
|
} = {}
|
|
): DistributionBoardStructureSnapshot {
|
|
const normalizedName = name.trim();
|
|
assertNonEmptyString(projectId, "projectId");
|
|
assertNonEmptyString(normalizedName, "name");
|
|
const structureId = crypto.randomUUID();
|
|
const structure: DistributionBoardStructureSnapshot = {
|
|
distributionBoard: {
|
|
id: structureId,
|
|
projectId,
|
|
name: normalizedName,
|
|
floorId: input.floorId ?? null,
|
|
supplyType: input.supplyType ?? null,
|
|
},
|
|
circuitList: {
|
|
id: structureId,
|
|
projectId,
|
|
distributionBoardId: structureId,
|
|
name: `${normalizedName} Stromkreisliste`,
|
|
},
|
|
sections: defaultCircuitSectionDefinitions.map((section) => ({
|
|
id: crypto.randomUUID(),
|
|
circuitListId: structureId,
|
|
...section,
|
|
})),
|
|
components: [
|
|
{
|
|
id: crypto.randomUUID(),
|
|
circuitListId: structureId,
|
|
sectionId: null,
|
|
...defaultMainSwitchComponent,
|
|
sortOrder: 10,
|
|
},
|
|
{
|
|
id: crypto.randomUUID(),
|
|
circuitListId: structureId,
|
|
sectionId: null,
|
|
...defaultSurgeProtectiveDeviceComponent,
|
|
sortOrder: 20,
|
|
},
|
|
],
|
|
};
|
|
assertDistributionBoardStructureSnapshot(structure);
|
|
return structure;
|
|
}
|
|
|
|
export function createDistributionBoardInsertProjectCommand(
|
|
structure: DistributionBoardStructureSnapshot
|
|
): DistributionBoardInsertProjectCommand {
|
|
const command: DistributionBoardInsertProjectCommand = {
|
|
schemaVersion: distributionBoardStructureCommandSchemaVersion,
|
|
type: distributionBoardInsertCommandType,
|
|
payload: { structure },
|
|
};
|
|
assertDistributionBoardInsertProjectCommand(command);
|
|
return command;
|
|
}
|
|
|
|
export function createDistributionBoardDeleteProjectCommand(
|
|
structure: DistributionBoardStructureSnapshot
|
|
): DistributionBoardDeleteProjectCommand {
|
|
const command: DistributionBoardDeleteProjectCommand = {
|
|
schemaVersion: distributionBoardStructureCommandSchemaVersion,
|
|
type: distributionBoardDeleteCommandType,
|
|
payload: { structure },
|
|
};
|
|
assertDistributionBoardDeleteProjectCommand(command);
|
|
return command;
|
|
}
|
|
|
|
export function assertDistributionBoardInsertProjectCommand(
|
|
command: SerializedProjectCommand<unknown>
|
|
): asserts command is DistributionBoardStructureProjectCommand {
|
|
assertDistributionBoardStructureProjectCommand(
|
|
command,
|
|
distributionBoardInsertCommandType
|
|
);
|
|
}
|
|
|
|
export function assertDistributionBoardDeleteProjectCommand(
|
|
command: SerializedProjectCommand<unknown>
|
|
): asserts command is DistributionBoardStructureProjectCommand {
|
|
assertDistributionBoardStructureProjectCommand(
|
|
command,
|
|
distributionBoardDeleteCommandType
|
|
);
|
|
}
|
|
|
|
export function normalizeDistributionBoardStructureProjectCommand(
|
|
command: DistributionBoardStructureProjectCommand
|
|
): NormalizedDistributionBoardStructureSnapshot {
|
|
if (
|
|
command.schemaVersion ===
|
|
legacyDistributionBoardStructureCommandSchemaVersion
|
|
) {
|
|
return {
|
|
distributionBoard: {
|
|
...command.payload.structure.distributionBoard,
|
|
floorId: null,
|
|
supplyType: null,
|
|
},
|
|
circuitList: command.payload.structure.circuitList,
|
|
sections: command.payload.structure.sections.map(
|
|
normalizeLegacySection
|
|
),
|
|
components: [],
|
|
};
|
|
}
|
|
if (
|
|
command.schemaVersion ===
|
|
previousDistributionBoardStructureCommandSchemaVersion
|
|
) {
|
|
return {
|
|
...command.payload.structure,
|
|
sections: command.payload.structure.sections.map(
|
|
normalizeLegacySection
|
|
),
|
|
components: [],
|
|
};
|
|
}
|
|
return command.payload.structure;
|
|
}
|
|
|
|
export function invertDistributionBoardStructureProjectCommand(
|
|
command: DistributionBoardStructureProjectCommand
|
|
): DistributionBoardStructureProjectCommand {
|
|
return {
|
|
...command,
|
|
type:
|
|
command.type === distributionBoardInsertCommandType
|
|
? distributionBoardDeleteCommandType
|
|
: distributionBoardInsertCommandType,
|
|
} as DistributionBoardStructureProjectCommand;
|
|
}
|
|
|
|
function normalizeLegacySection(
|
|
section: LegacyCircuitSectionSnapshot
|
|
): NormalizedDistributionBoardStructureSnapshot["sections"][number] {
|
|
const category =
|
|
section.key === "lighting" ||
|
|
section.key === "single_phase" ||
|
|
section.key === "three_phase"
|
|
? section.key
|
|
: null;
|
|
return {
|
|
...section,
|
|
category,
|
|
groupNumber: category === null ? null : 1,
|
|
};
|
|
}
|
|
|
|
export function assertDistributionBoardStructureSnapshot(
|
|
structure: unknown
|
|
): asserts structure is DistributionBoardStructureSnapshot {
|
|
assertDistributionBoardStructureSnapshotVersion(structure, "current");
|
|
}
|
|
|
|
function assertDistributionBoardStructureProjectCommand(
|
|
command: SerializedProjectCommand<unknown>,
|
|
expectedType:
|
|
| typeof distributionBoardInsertCommandType
|
|
| typeof distributionBoardDeleteCommandType
|
|
) {
|
|
if (
|
|
command.type !== expectedType ||
|
|
!isPlainObject(command.payload) ||
|
|
Object.keys(command.payload).length !== 1
|
|
) {
|
|
throw new Error("Unsupported distribution-board structure command.");
|
|
}
|
|
if (
|
|
command.schemaVersion ===
|
|
legacyDistributionBoardStructureCommandSchemaVersion
|
|
) {
|
|
assertDistributionBoardStructureSnapshotVersion(
|
|
command.payload.structure,
|
|
"legacy"
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
command.schemaVersion ===
|
|
previousDistributionBoardStructureCommandSchemaVersion
|
|
) {
|
|
assertDistributionBoardStructureSnapshotVersion(
|
|
command.payload.structure,
|
|
"previous"
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
command.schemaVersion !==
|
|
distributionBoardStructureCommandSchemaVersion
|
|
) {
|
|
throw new Error("Unsupported distribution-board structure command.");
|
|
}
|
|
assertDistributionBoardStructureSnapshotVersion(
|
|
command.payload.structure,
|
|
"current"
|
|
);
|
|
}
|
|
|
|
function assertDistributionBoardStructureSnapshotVersion(
|
|
structure: unknown,
|
|
version: "legacy" | "previous" | "current"
|
|
) {
|
|
const current = version === "current";
|
|
if (
|
|
!isPlainObject(structure) ||
|
|
Object.keys(structure).length !== (current ? 4 : 3)
|
|
) {
|
|
throw new Error("Distribution-board structure is invalid.");
|
|
}
|
|
const { distributionBoard, circuitList, sections, components } =
|
|
structure;
|
|
const expectedSections = current
|
|
? defaultCircuitSectionDefinitions
|
|
: legacyDefaultCircuitSectionDefinitions;
|
|
if (
|
|
!isPlainObject(distributionBoard) ||
|
|
Object.keys(distributionBoard).length !==
|
|
(version === "legacy" ? 3 : 5) ||
|
|
!isPlainObject(circuitList) ||
|
|
Object.keys(circuitList).length !== 4 ||
|
|
!Array.isArray(sections) ||
|
|
sections.length !== expectedSections.length ||
|
|
(current && (!Array.isArray(components) || components.length !== 2))
|
|
) {
|
|
throw new Error("Distribution-board structure is incomplete.");
|
|
}
|
|
for (const field of ["id", "projectId", "name"] as const) {
|
|
assertNonEmptyString(
|
|
distributionBoard[field],
|
|
`distributionBoard.${field}`
|
|
);
|
|
}
|
|
if (version !== "legacy") {
|
|
assertNullableId(distributionBoard.floorId, "distributionBoard.floorId");
|
|
assertNullableSupplyType(distributionBoard.supplyType);
|
|
}
|
|
for (const [field, value] of Object.entries(circuitList)) {
|
|
assertNonEmptyString(value, `circuitList.${field}`);
|
|
}
|
|
if (
|
|
circuitList.projectId !== distributionBoard.projectId ||
|
|
circuitList.distributionBoardId !== distributionBoard.id ||
|
|
circuitList.name !== `${distributionBoard.name} Stromkreisliste`
|
|
) {
|
|
throw new Error("Distribution board and circuit list are inconsistent.");
|
|
}
|
|
|
|
const sectionIds = new Set<string>();
|
|
for (let index = 0; index < sections.length; index += 1) {
|
|
const section = sections[index];
|
|
const expected = expectedSections[index];
|
|
if (
|
|
!isPlainObject(section) ||
|
|
Object.keys(section).length !== (current ? 8 : 6)
|
|
) {
|
|
throw new Error("Default circuit section is invalid.");
|
|
}
|
|
assertNonEmptyString(section.id, "section.id");
|
|
if (sectionIds.has(section.id)) {
|
|
throw new Error("Default circuit-section ids must be unique.");
|
|
}
|
|
sectionIds.add(section.id);
|
|
if (
|
|
section.circuitListId !== circuitList.id ||
|
|
section.key !== expected.key ||
|
|
section.displayName !== expected.displayName ||
|
|
section.prefix !== expected.prefix ||
|
|
section.sortOrder !== expected.sortOrder ||
|
|
(current &&
|
|
(!("category" in expected) ||
|
|
section.category !== expected.category ||
|
|
section.groupNumber !== expected.groupNumber))
|
|
) {
|
|
throw new Error(
|
|
"Distribution-board structure has invalid default sections."
|
|
);
|
|
}
|
|
}
|
|
|
|
if (current) {
|
|
assertDefaultComponents(components, circuitList.id);
|
|
}
|
|
}
|
|
|
|
function assertDefaultComponents(
|
|
components: unknown,
|
|
circuitListId: unknown
|
|
) {
|
|
if (!Array.isArray(components)) {
|
|
throw new Error("Default distribution-board components are invalid.");
|
|
}
|
|
const expectedComponents = [
|
|
{ ...defaultMainSwitchComponent, sortOrder: 10 },
|
|
{ ...defaultSurgeProtectiveDeviceComponent, sortOrder: 20 },
|
|
];
|
|
const componentIds = new Set<string>();
|
|
for (let index = 0; index < expectedComponents.length; index += 1) {
|
|
const component = components[index];
|
|
const expected = expectedComponents[index];
|
|
if (!isPlainObject(component) || Object.keys(component).length !== 8) {
|
|
throw new Error(
|
|
"Default distribution-board component is invalid."
|
|
);
|
|
}
|
|
assertNonEmptyString(component.id, "component.id");
|
|
if (componentIds.has(component.id)) {
|
|
throw new Error(
|
|
"Default distribution-board component ids must be unique."
|
|
);
|
|
}
|
|
componentIds.add(component.id);
|
|
if (
|
|
component.circuitListId !== circuitListId ||
|
|
component.sectionId !== null ||
|
|
component.equipmentIdentifier !== expected.equipmentIdentifier ||
|
|
component.name !== expected.name ||
|
|
component.role !== expected.role ||
|
|
component.placement !== expected.placement ||
|
|
component.sortOrder !== expected.sortOrder
|
|
) {
|
|
throw new Error(
|
|
"Distribution-board structure has invalid default components."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNullableId(value: unknown, field: string) {
|
|
if (value !== null) {
|
|
assertNonEmptyString(value, field);
|
|
}
|
|
}
|
|
|
|
function assertNullableSupplyType(
|
|
value: unknown
|
|
): asserts value is DistributionBoardSupplyType | null {
|
|
if (
|
|
value !== null &&
|
|
!distributionBoardSupplyTypes.includes(
|
|
value as DistributionBoardSupplyType
|
|
)
|
|
) {
|
|
throw new Error("distributionBoard.supplyType is invalid.");
|
|
}
|
|
}
|
|
|
|
function assertNonEmptyString(
|
|
value: unknown,
|
|
field: string
|
|
): asserts value is 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);
|
|
}
|