import { distributionBoardInfoFieldMaxLengths, distributionBoardInfoFields, distributionBoardSupplyTypes, type DistributionBoardInfoField, type DistributionBoardSupplyType, } from "../../shared/constants/distribution-board.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const distributionBoardUpdateCommandType = "distribution-board.update" as const; // Version 2 adds logical feeding references. Version-1 commands remain // executable and can only change the fields they originally knew. export const distributionBoardUpdateCommandSchemaVersion = 2 as const; const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const; export interface DistributionBoardUpdateValues extends Record { floorId: string | null; supplyType: DistributionBoardSupplyType | null; simultaneityFactor: number; feedingDistributionBoardId: string | null; feedingCircuitId: string | null; } export type DistributionBoardUpdateField = keyof DistributionBoardUpdateValues; export type DistributionBoardUpdatePatch = Partial; export interface DistributionBoardUpdateFieldChange< TField extends DistributionBoardUpdateField = DistributionBoardUpdateField, > { field: TField; value: DistributionBoardUpdateValues[TField]; } export interface DistributionBoardUpdateCommandPayload { distributionBoardId: string; changes: DistributionBoardUpdateFieldChange[]; } export interface DistributionBoardUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: | typeof legacyDistributionBoardUpdateCommandSchemaVersion | typeof distributionBoardUpdateCommandSchemaVersion; type: typeof distributionBoardUpdateCommandType; } export function createDistributionBoardUpdateProjectCommand( distributionBoardId: string, patch: DistributionBoardUpdatePatch, schemaVersion: DistributionBoardUpdateProjectCommand["schemaVersion"] = distributionBoardUpdateCommandSchemaVersion ): DistributionBoardUpdateProjectCommand { const command: DistributionBoardUpdateProjectCommand = { schemaVersion, type: distributionBoardUpdateCommandType, payload: { distributionBoardId, changes: Object.entries(patch).map(([field, value]) => ({ field: field as DistributionBoardUpdateField, value, })) as DistributionBoardUpdateFieldChange[], }, }; assertDistributionBoardUpdateProjectCommand(command); return command; } export function assertDistributionBoardUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardUpdateProjectCommand { if ( (command.schemaVersion !== legacyDistributionBoardUpdateCommandSchemaVersion && command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) || command.type !== distributionBoardUpdateCommandType || !isPlainObject(command.payload) ) { throw new Error("Unsupported distribution-board update command."); } const { distributionBoardId, changes } = command.payload; if ( typeof distributionBoardId !== "string" || !distributionBoardId.trim() || !Array.isArray(changes) || changes.length === 0 ) { throw new Error( "Distribution-board update command requires an id and changes." ); } const seen = new Set(); for (const change of changes) { if ( !isPlainObject(change) || typeof change.field !== "string" || (change.field !== "floorId" && change.field !== "supplyType" && change.field !== "simultaneityFactor" && change.field !== "feedingDistributionBoardId" && change.field !== "feedingCircuitId" && !isInfoField(change.field)) || (command.schemaVersion === legacyDistributionBoardUpdateCommandSchemaVersion && (change.field === "feedingDistributionBoardId" || change.field === "feedingCircuitId")) || seen.has(change.field) ) { throw new Error( "Distribution-board update contains an invalid or duplicate field." ); } if (isInfoField(change.field)) { assertInfoFieldValue(change.field, change.value); } else if (change.field === "floorId") { if ( change.value !== null && (typeof change.value !== "string" || !change.value.trim() || change.value !== change.value.trim()) ) { throw new Error("floorId must be a non-empty string or null."); } } else if (change.field === "supplyType") { if ( change.value !== null && !distributionBoardSupplyTypes.includes( change.value as DistributionBoardSupplyType ) ) { throw new Error("supplyType is invalid."); } } else if ( change.field === "feedingDistributionBoardId" || change.field === "feedingCircuitId" ) { if ( change.value !== null && (typeof change.value !== "string" || !change.value.trim()) ) { throw new Error(`${change.field} must be a non-empty string or null.`); } } else if ( typeof change.value !== "number" || !Number.isFinite(change.value) || change.value < 0 || change.value > 1 ) { throw new Error( "simultaneityFactor must be between zero and one." ); } seen.add(change.field); } } function isInfoField(field: string): field is DistributionBoardInfoField { return (distributionBoardInfoFields as readonly string[]).includes(field); } function assertInfoFieldValue( field: DistributionBoardInfoField, value: unknown ) { if (value === null) { return; } if ( typeof value !== "string" || !value.trim() || value !== value.trim() || value.length > distributionBoardInfoFieldMaxLengths[field] ) { throw new Error( `${field} must be a trimmed, non-empty string of at most ${distributionBoardInfoFieldMaxLengths[field]} characters or null.` ); } } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }