import crypto from "node:crypto"; import { distributionBoardSupplyTypes, type DistributionBoardSupplyType, } from "../../shared/constants/distribution-board.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 distributionBoardStructureCommandSchemaVersion = 2 as const; export const defaultCircuitSectionDefinitions = [ { 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; interface LegacyDistributionBoardStructureSnapshot { distributionBoard: { id: string; projectId: string; name: string; }; circuitList: DistributionBoardStructureSnapshot["circuitList"]; sections: DistributionBoardStructureSnapshot["sections"]; } 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; }>; } interface DistributionBoardStructureCommandPayload< TStructure = DistributionBoardStructureSnapshot, > { structure: TStructure; } interface CurrentDistributionBoardStructureProjectCommand extends SerializedProjectCommand { schemaVersion: typeof distributionBoardStructureCommandSchemaVersion; type: | typeof distributionBoardInsertCommandType | typeof distributionBoardDeleteCommandType; } interface LegacyDistributionBoardStructureProjectCommand extends SerializedProjectCommand< DistributionBoardStructureCommandPayload > { schemaVersion: typeof legacyDistributionBoardStructureCommandSchemaVersion; 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 | 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, })), }; 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 ): asserts command is DistributionBoardStructureProjectCommand { assertDistributionBoardStructureProjectCommand( command, distributionBoardInsertCommandType ); } export function assertDistributionBoardDeleteProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardStructureProjectCommand { assertDistributionBoardStructureProjectCommand( command, distributionBoardDeleteCommandType ); } export function normalizeDistributionBoardStructureProjectCommand( command: DistributionBoardStructureProjectCommand ): DistributionBoardInsertProjectCommand | DistributionBoardDeleteProjectCommand { if ( command.schemaVersion === legacyDistributionBoardStructureCommandSchemaVersion ) { const structure: DistributionBoardStructureSnapshot = { ...command.payload.structure, distributionBoard: { ...command.payload.structure.distributionBoard, floorId: null, supplyType: null, }, }; return command.type === distributionBoardInsertCommandType ? createDistributionBoardInsertProjectCommand(structure) : createDistributionBoardDeleteProjectCommand(structure); } return command as | DistributionBoardInsertProjectCommand | DistributionBoardDeleteProjectCommand; } export function assertDistributionBoardStructureSnapshot( structure: unknown ): asserts structure is DistributionBoardStructureSnapshot { assertDistributionBoardStructureSnapshotVersion(structure, false); } function assertDistributionBoardStructureProjectCommand( command: SerializedProjectCommand, 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, true ); return; } if ( command.schemaVersion !== distributionBoardStructureCommandSchemaVersion ) { throw new Error("Unsupported distribution-board structure command."); } assertDistributionBoardStructureSnapshotVersion( command.payload.structure, false ); } function assertDistributionBoardStructureSnapshotVersion( structure: unknown, legacy: boolean ) { if (!isPlainObject(structure) || Object.keys(structure).length !== 3) { throw new Error("Distribution-board structure is invalid."); } const { distributionBoard, circuitList, sections } = structure; if ( !isPlainObject(distributionBoard) || Object.keys(distributionBoard).length !== (legacy ? 3 : 5) || !isPlainObject(circuitList) || Object.keys(circuitList).length !== 4 || !Array.isArray(sections) || sections.length !== defaultCircuitSectionDefinitions.length ) { throw new Error("Distribution-board structure is incomplete."); } for (const field of ["id", "projectId", "name"] as const) { assertNonEmptyString( distributionBoard[field], `distributionBoard.${field}` ); } if (!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(); for (let index = 0; index < sections.length; index += 1) { const section = sections[index]; const expected = defaultCircuitSectionDefinitions[index]; if (!isPlainObject(section) || Object.keys(section).length !== 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 ) { throw new Error( "Distribution-board structure has invalid default sections." ); } } } 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 { return value !== null && typeof value === "object" && !Array.isArray(value); }