import { circuitGroupCategories, type CircuitGroupCategory, } from "../../shared/constants/circuit-group.js"; import { formatCircuitGroupPrefix, parseGroupedEquipmentIdentifier, } from "../services/circuit-group-numbering.js"; import type { CircuitGroupRenumberPlan } from "../services/circuit-group-renumbering.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const circuitGroupRenumberCommandType = "circuit-group.renumber" as const; export const circuitGroupRenumberCommandSchemaVersion = 1 as const; export interface CircuitGroupRenumberCommandPayload extends CircuitGroupRenumberPlan { circuitListId: string; } export interface CircuitGroupRenumberProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitGroupRenumberCommandSchemaVersion; type: typeof circuitGroupRenumberCommandType; } export function createCircuitGroupRenumberProjectCommand( circuitListId: string, plan: CircuitGroupRenumberPlan ): CircuitGroupRenumberProjectCommand { const command: CircuitGroupRenumberProjectCommand = { schemaVersion: circuitGroupRenumberCommandSchemaVersion, type: circuitGroupRenumberCommandType, payload: { circuitListId, groups: plan.groups }, }; assertCircuitGroupRenumberProjectCommand(command); return command; } export function assertCircuitGroupRenumberProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitGroupRenumberProjectCommand { if ( command.schemaVersion !== circuitGroupRenumberCommandSchemaVersion || command.type !== circuitGroupRenumberCommandType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 2 || typeof command.payload.circuitListId !== "string" || !command.payload.circuitListId.trim() || !Array.isArray(command.payload.groups) || command.payload.groups.length === 0 ) { throw new Error("Unsupported circuit-group renumber command."); } const groupIds = new Set(); const entityIds = new Set(); const targetGroupKeys = new Set(); const targetIdentifiers = new Set(); let hasChange = false; for (const group of command.payload.groups) { if ( !isPlainObject(group) || Object.keys(group).length !== 8 || typeof group.groupId !== "string" || !group.groupId.trim() || groupIds.has(group.groupId) || !circuitGroupCategories.includes( group.category as CircuitGroupCategory ) || !positiveInteger(group.expectedGroupNumber) || !positiveInteger(group.targetGroupNumber) || group.expectedPrefix !== formatCircuitGroupPrefix( group.category as CircuitGroupCategory, group.expectedGroupNumber as number ) || group.targetPrefix !== formatCircuitGroupPrefix( group.category as CircuitGroupCategory, group.targetGroupNumber as number ) || !Array.isArray(group.circuits) || !Array.isArray(group.components) ) { throw new Error("Circuit-group renumber assignment is invalid."); } const targetGroupKey = `${group.category}:${group.targetGroupNumber}`; if (targetGroupKeys.has(targetGroupKey)) { throw new Error( "Circuit-group renumber contains duplicate target group numbers." ); } hasChange ||= group.expectedGroupNumber !== group.targetGroupNumber; groupIds.add(group.groupId); targetGroupKeys.add(targetGroupKey); for (const circuit of group.circuits) { assertEntityAssignment( circuit, "circuitId", "circuit", group.category as CircuitGroupCategory, group.expectedGroupNumber as number, group.targetGroupNumber as number, entityIds, targetIdentifiers ); } for (const component of group.components) { assertEntityAssignment( component, "componentId", "component", group.category as CircuitGroupCategory, group.expectedGroupNumber as number, group.targetGroupNumber as number, entityIds, targetIdentifiers ); } } if (!hasChange) { throw new Error( "Circuit-group renumber must change at least one group number." ); } } function assertEntityAssignment( value: unknown, idField: "circuitId" | "componentId", kind: "circuit" | "component", category: CircuitGroupCategory, expectedGroupNumber: number, targetGroupNumber: number, entityIds: Set, targetIdentifiers: Set ) { if ( !isPlainObject(value) || Object.keys(value).length !== 3 || typeof value[idField] !== "string" || !(value[idField] as string).trim() || typeof value.expectedEquipmentIdentifier !== "string" || typeof value.targetEquipmentIdentifier !== "string" || entityIds.has(value[idField] as string) || targetIdentifiers.has(value.targetEquipmentIdentifier) ) { throw new Error(`Circuit-group renumber ${kind} assignment is invalid.`); } const expected = parseGroupedEquipmentIdentifier( value.expectedEquipmentIdentifier ); const target = parseGroupedEquipmentIdentifier( value.targetEquipmentIdentifier ); if ( !expected || !target || expected.kind !== target.kind || expected.category !== category || target.category !== category || expected.groupNumber !== expectedGroupNumber || target.groupNumber !== targetGroupNumber || (kind === "circuit" && (expected.kind !== "circuit" || expected.circuitNumber !== target.circuitNumber)) || (kind === "component" && expected.kind === "circuit") ) { throw new Error( `Circuit-group renumber ${kind} identifiers are incompatible.` ); } entityIds.add(value[idField] as string); targetIdentifiers.add(value.targetEquipmentIdentifier); } function positiveInteger(value: unknown): value is number { return Number.isInteger(value) && (value as number) > 0; } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }