import { assertCircuitDeviceRowInsertProjectCommand, circuitDeviceRowInsertCommandType, circuitDeviceRowStructureCommandSchemaVersion, type CircuitDeviceRowSnapshot, } from "./circuit-device-row-structure-project-command.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; import { assertCircuitProtectionSnapshot, type CircuitProtectionSnapshot, } from "./circuit-protection-project-command.model.js"; export const circuitInsertCommandType = "circuit.insert" as const; export const circuitDeleteCommandType = "circuit.delete" as const; export const circuitStructureCommandSchemaVersion = 1 as const; export interface CircuitSnapshot { id: string; circuitListId: string; sectionId: string; equipmentIdentifier: string; displayName: string | null; sortOrder: number; cableType: string | null; cableCrossSection: string | null; cableLength: number | null; rcdAssignment: string | null; terminalDesignation: string | null; voltage: number | null; controlRequirement: string | null; status: string | null; isReserve: boolean; remark: string | null; deviceRows: CircuitDeviceRowSnapshot[]; protectionDevice?: CircuitProtectionSnapshot | null; } export interface CircuitInsertCommandPayload { circuit: CircuitSnapshot; } export interface CircuitDeleteCommandPayload { circuitId: string; expectedCircuitListId: string; } export interface CircuitInsertProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitStructureCommandSchemaVersion; type: typeof circuitInsertCommandType; } export interface CircuitDeleteProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitStructureCommandSchemaVersion; type: typeof circuitDeleteCommandType; } export type CircuitStructureProjectCommand = | CircuitInsertProjectCommand | CircuitDeleteProjectCommand; export function createCircuitInsertProjectCommand( circuit: CircuitSnapshot ): CircuitInsertProjectCommand { const command: CircuitInsertProjectCommand = { schemaVersion: circuitStructureCommandSchemaVersion, type: circuitInsertCommandType, payload: { circuit }, }; assertCircuitInsertProjectCommand(command); return command; } export function createCircuitDeleteProjectCommand( circuitId: string, expectedCircuitListId: string ): CircuitDeleteProjectCommand { const command: CircuitDeleteProjectCommand = { schemaVersion: circuitStructureCommandSchemaVersion, type: circuitDeleteCommandType, payload: { circuitId, expectedCircuitListId }, }; assertCircuitDeleteProjectCommand(command); return command; } export function assertCircuitInsertProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitInsertProjectCommand { if ( command.schemaVersion !== circuitStructureCommandSchemaVersion || command.type !== circuitInsertCommandType ) { throw new Error("Unsupported circuit insert command."); } if (!isPlainObject(command.payload)) { throw new Error("Circuit insert payload must be an object."); } const circuit = command.payload.circuit; if (!isPlainObject(circuit)) { throw new Error("Circuit insert command requires a circuit."); } assertNonEmptyString(circuit.id, "circuit.id"); assertNonEmptyString(circuit.circuitListId, "circuit.circuitListId"); assertNonEmptyString(circuit.sectionId, "circuit.sectionId"); assertNonEmptyString( circuit.equipmentIdentifier, "circuit.equipmentIdentifier" ); assertNullableString(circuit.displayName, "circuit.displayName"); assertFiniteNumber(circuit.sortOrder, "circuit.sortOrder"); for (const field of [ "cableType", "cableCrossSection", "rcdAssignment", "terminalDesignation", "controlRequirement", "status", "remark", ] as const) { assertNullableString(circuit[field], `circuit.${field}`); } assertNullableNonNegativeNumber( circuit.cableLength, "circuit.cableLength" ); if (circuit.voltage !== null) { assertFiniteNumber(circuit.voltage, "circuit.voltage"); if (circuit.voltage <= 0) { throw new Error("circuit.voltage must be positive or null."); } } if (typeof circuit.isReserve !== "boolean") { throw new Error("circuit.isReserve must be a boolean."); } if (!Array.isArray(circuit.deviceRows)) { throw new Error("circuit.deviceRows must be an array."); } if ( circuit.protectionDevice !== undefined && circuit.protectionDevice !== null ) { assertCircuitProtectionSnapshot( circuit.protectionDevice, circuit.id as string ); } if (circuit.isReserve !== (circuit.deviceRows.length === 0)) { throw new Error( "Circuit reserve state must match whether device rows exist." ); } const rowIds = new Set(); for (const row of circuit.deviceRows) { assertCircuitDeviceRowInsertProjectCommand({ schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, type: circuitDeviceRowInsertCommandType, payload: { row }, }); if (row.circuitId !== circuit.id) { throw new Error("Circuit device row belongs to a different circuit."); } if (rowIds.has(row.id)) { throw new Error("Circuit insert command contains duplicate row ids."); } rowIds.add(row.id); } } export function assertCircuitDeleteProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitDeleteProjectCommand { if ( command.schemaVersion !== circuitStructureCommandSchemaVersion || command.type !== circuitDeleteCommandType ) { throw new Error("Unsupported circuit delete command."); } if (!isPlainObject(command.payload)) { throw new Error("Circuit delete payload must be an object."); } assertNonEmptyString(command.payload.circuitId, "circuitId"); assertNonEmptyString( command.payload.expectedCircuitListId, "expectedCircuitListId" ); } function assertNonEmptyString(value: unknown, field: string) { if (typeof value !== "string" || !value.trim()) { throw new Error(`${field} must be a non-empty string.`); } } function assertNullableString(value: unknown, field: string) { if (value !== null && typeof value !== "string") { throw new Error(`${field} must be a string or null.`); } } function assertFiniteNumber( value: unknown, field: string ): asserts value is number { if (typeof value !== "number" || !Number.isFinite(value)) { throw new Error(`${field} must be a finite number.`); } } function assertNullableNonNegativeNumber(value: unknown, field: string) { if (value === null) { return; } assertFiniteNumber(value, field); if (value < 0) { throw new Error(`${field} must not be negative.`); } } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }