import { circuitGroupCategories, type CircuitGroupCategory, } from "../../shared/constants/circuit-group.js"; import { formatCircuitGroupPrefix } from "../services/circuit-group-numbering.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const circuitGroupInsertCommandType = "circuit-group.insert" as const; export const circuitGroupDeleteCommandType = "circuit-group.delete" as const; export const circuitGroupUpdateCommandType = "circuit-group.update" as const; export const circuitGroupStructureCommandSchemaVersion = 1 as const; export interface CircuitGroupSnapshot { id: string; circuitListId: string; key: string; displayName: string; prefix: string; sortOrder: number; category: CircuitGroupCategory; groupNumber: number; } interface SnapshotPayload { snapshot: CircuitGroupSnapshot; } interface UpdatePayload { expected: CircuitGroupSnapshot; target: CircuitGroupSnapshot; } export interface CircuitGroupInsertProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitGroupStructureCommandSchemaVersion; type: typeof circuitGroupInsertCommandType; } export interface CircuitGroupDeleteProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitGroupStructureCommandSchemaVersion; type: typeof circuitGroupDeleteCommandType; } export interface CircuitGroupUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitGroupStructureCommandSchemaVersion; type: typeof circuitGroupUpdateCommandType; } export type CircuitGroupStructureProjectCommand = | CircuitGroupInsertProjectCommand | CircuitGroupDeleteProjectCommand | CircuitGroupUpdateProjectCommand; export function createCircuitGroupInsertProjectCommand( snapshot: CircuitGroupSnapshot ): CircuitGroupInsertProjectCommand { const command: CircuitGroupInsertProjectCommand = { schemaVersion: circuitGroupStructureCommandSchemaVersion, type: circuitGroupInsertCommandType, payload: { snapshot }, }; assertCircuitGroupInsertProjectCommand(command); return command; } export function createCircuitGroupDeleteProjectCommand( snapshot: CircuitGroupSnapshot ): CircuitGroupDeleteProjectCommand { const command: CircuitGroupDeleteProjectCommand = { schemaVersion: circuitGroupStructureCommandSchemaVersion, type: circuitGroupDeleteCommandType, payload: { snapshot }, }; assertCircuitGroupDeleteProjectCommand(command); return command; } export function createCircuitGroupUpdateProjectCommand( expected: CircuitGroupSnapshot, target: CircuitGroupSnapshot ): CircuitGroupUpdateProjectCommand { const command: CircuitGroupUpdateProjectCommand = { schemaVersion: circuitGroupStructureCommandSchemaVersion, type: circuitGroupUpdateCommandType, payload: { expected, target }, }; assertCircuitGroupUpdateProjectCommand(command); return command; } export function assertCircuitGroupInsertProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitGroupInsertProjectCommand { assertSnapshotCommand(command, circuitGroupInsertCommandType); } export function assertCircuitGroupDeleteProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitGroupDeleteProjectCommand { assertSnapshotCommand(command, circuitGroupDeleteCommandType); } export function assertCircuitGroupUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitGroupUpdateProjectCommand { if ( command.schemaVersion !== circuitGroupStructureCommandSchemaVersion || command.type !== circuitGroupUpdateCommandType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 2 ) { throw new Error("Unsupported circuit-group update command."); } assertCircuitGroupSnapshot(command.payload.expected); assertCircuitGroupSnapshot(command.payload.target); for (const field of [ "id", "circuitListId", "key", "prefix", "sortOrder", "category", "groupNumber", ] as const) { if (command.payload.expected[field] !== command.payload.target[field]) { throw new Error( "Circuit-group updates can only change the display name." ); } } } function assertSnapshotCommand( command: SerializedProjectCommand, type: | typeof circuitGroupInsertCommandType | typeof circuitGroupDeleteCommandType ) { if ( command.schemaVersion !== circuitGroupStructureCommandSchemaVersion || command.type !== type || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 1 ) { throw new Error("Unsupported circuit-group structure command."); } assertCircuitGroupSnapshot(command.payload.snapshot); } export function assertCircuitGroupSnapshot( value: unknown ): asserts value is CircuitGroupSnapshot { if (!isPlainObject(value) || Object.keys(value).length !== 8) { throw new Error("Circuit-group snapshot is invalid."); } for (const field of [ "id", "circuitListId", "key", "displayName", "prefix", ] as const) { if (typeof value[field] !== "string" || !value[field].trim()) { throw new Error(`${field} must be a non-empty string.`); } } if ( !circuitGroupCategories.includes( value.category as CircuitGroupCategory ) || !Number.isInteger(value.groupNumber) || (value.groupNumber as number) < 1 || !Number.isFinite(value.sortOrder) ) { throw new Error("Circuit-group category, number or order is invalid."); } if ( value.prefix !== formatCircuitGroupPrefix( value.category as CircuitGroupCategory, value.groupNumber as number ) ) { throw new Error("Circuit-group prefix does not match its category and number."); } } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }