409 lines
12 KiB
TypeScript
409 lines
12 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 distributionBoardStructureCommandSchemaVersion = 1 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;
|
|
|
|
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: DistributionBoardStructureSnapshot["sections"];
|
|
components: DistributionBoardStructureSnapshot["components"];
|
|
}
|
|
|
|
interface DistributionBoardStructureCommandPayload {
|
|
structure: DistributionBoardStructureSnapshot;
|
|
}
|
|
|
|
interface DistributionBoardStructureProjectCommandBase
|
|
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
|
|
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
|
|
type:
|
|
| typeof distributionBoardInsertCommandType
|
|
| typeof distributionBoardDeleteCommandType;
|
|
}
|
|
|
|
export interface DistributionBoardInsertProjectCommand
|
|
extends DistributionBoardStructureProjectCommandBase {
|
|
type: typeof distributionBoardInsertCommandType;
|
|
}
|
|
|
|
export interface DistributionBoardDeleteProjectCommand
|
|
extends DistributionBoardStructureProjectCommandBase {
|
|
type: typeof distributionBoardDeleteCommandType;
|
|
}
|
|
|
|
export type DistributionBoardStructureProjectCommand =
|
|
| DistributionBoardInsertProjectCommand
|
|
| DistributionBoardDeleteProjectCommand;
|
|
|
|
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 {
|
|
return command.payload.structure;
|
|
}
|
|
|
|
export function invertDistributionBoardStructureProjectCommand(
|
|
command: DistributionBoardStructureProjectCommand
|
|
): DistributionBoardStructureProjectCommand {
|
|
return {
|
|
...command,
|
|
type:
|
|
command.type === distributionBoardInsertCommandType
|
|
? distributionBoardDeleteCommandType
|
|
: distributionBoardInsertCommandType,
|
|
} as DistributionBoardStructureProjectCommand;
|
|
}
|
|
|
|
export function assertDistributionBoardStructureSnapshot(
|
|
structure: unknown
|
|
): asserts structure is DistributionBoardStructureSnapshot {
|
|
assertDistributionBoardStructureSnapshotVersion(structure);
|
|
}
|
|
|
|
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 !==
|
|
distributionBoardStructureCommandSchemaVersion
|
|
) {
|
|
throw new Error("Unsupported distribution-board structure command.");
|
|
}
|
|
assertDistributionBoardStructureSnapshotVersion(command.payload.structure);
|
|
}
|
|
|
|
function assertDistributionBoardStructureSnapshotVersion(
|
|
structure: unknown
|
|
) {
|
|
if (
|
|
!isPlainObject(structure) ||
|
|
Object.keys(structure).length !== 4
|
|
) {
|
|
throw new Error("Distribution-board structure is invalid.");
|
|
}
|
|
const { distributionBoard, circuitList, sections, components } =
|
|
structure;
|
|
const expectedSections = defaultCircuitSectionDefinitions;
|
|
if (
|
|
!isPlainObject(distributionBoard) ||
|
|
Object.keys(distributionBoard).length !== 5 ||
|
|
!isPlainObject(circuitList) ||
|
|
Object.keys(circuitList).length !== 4 ||
|
|
!Array.isArray(sections) ||
|
|
sections.length !== expectedSections.length ||
|
|
!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}`
|
|
);
|
|
}
|
|
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 !== 8
|
|
) {
|
|
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 ||
|
|
section.category !== expected.category ||
|
|
section.groupNumber !== expected.groupNumber
|
|
) {
|
|
throw new Error(
|
|
"Distribution-board structure has invalid default sections."
|
|
);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|