import type { SerializedProjectCommand } from "./project-command.model.js"; export const projectDeviceUpdateCommandType = "project-device.update" as const; export const projectDeviceUpdateCommandSchemaVersion = 1 as const; export interface ProjectDeviceUpdateValues { name: string; displayName: string; phaseType: "single_phase" | "three_phase"; connectionKind: string | null; costGroup: string | null; category: string | null; quantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi: number | null; remark: string | null; voltageV: number | null; } export type ProjectDeviceUpdateField = keyof ProjectDeviceUpdateValues; export type ProjectDeviceUpdatePatch = Partial; export const projectDeviceUpdateFields = [ "name", "displayName", "phaseType", "connectionKind", "costGroup", "category", "quantity", "powerPerUnit", "simultaneityFactor", "cosPhi", "remark", "voltageV", ] as const satisfies readonly ProjectDeviceUpdateField[]; export interface ProjectDeviceUpdateFieldChange< TField extends ProjectDeviceUpdateField = ProjectDeviceUpdateField, > { field: TField; value: ProjectDeviceUpdateValues[TField]; } export interface ProjectDeviceUpdateCommandPayload { projectDeviceId: string; changes: ProjectDeviceUpdateFieldChange[]; } export interface ProjectDeviceUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectDeviceUpdateCommandSchemaVersion; type: typeof projectDeviceUpdateCommandType; } export function createProjectDeviceUpdateProjectCommand( projectDeviceId: string, patch: ProjectDeviceUpdatePatch ): ProjectDeviceUpdateProjectCommand { const command: ProjectDeviceUpdateProjectCommand = { schemaVersion: projectDeviceUpdateCommandSchemaVersion, type: projectDeviceUpdateCommandType, payload: { projectDeviceId, changes: Object.entries(patch).map(([field, value]) => ({ field: field as ProjectDeviceUpdateField, value, })) as ProjectDeviceUpdateFieldChange[], }, }; assertProjectDeviceUpdateProjectCommand(command); return command; } export function assertProjectDeviceUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is ProjectDeviceUpdateProjectCommand { if ( command.schemaVersion !== projectDeviceUpdateCommandSchemaVersion || command.type !== projectDeviceUpdateCommandType ) { throw new Error("Unsupported project-device update command."); } if (!isPlainObject(command.payload)) { throw new Error("Project-device update payload must be an object."); } const { projectDeviceId, changes } = command.payload; if ( typeof projectDeviceId !== "string" || !projectDeviceId.trim() ) { throw new Error( "Project-device update command requires a project device id." ); } if (!Array.isArray(changes) || changes.length === 0) { throw new Error( "Project-device update command requires at least one change." ); } const seenFields = new Set(); for (const change of changes) { if (!isPlainObject(change) || typeof change.field !== "string") { throw new Error( "Project-device update command contains an invalid change." ); } if ( !isProjectDeviceUpdateField(change.field) || seenFields.has(change.field) ) { throw new Error( "Project-device update command contains an invalid or duplicate field." ); } assertProjectDeviceUpdateFieldValue( change.field, change.value ); seenFields.add(change.field); } } function assertProjectDeviceUpdateFieldValue( field: ProjectDeviceUpdateField, value: unknown ) { if (field === "name" || field === "displayName") { if (typeof value !== "string" || !value.trim()) { throw new Error(`${field} must be a non-empty string.`); } return; } if (field === "phaseType") { if (value !== "single_phase" && value !== "three_phase") { throw new Error("phaseType is invalid."); } return; } if ( field === "quantity" || field === "powerPerUnit" || field === "simultaneityFactor" ) { if ( typeof value !== "number" || !Number.isFinite(value) || value < 0 || (field === "simultaneityFactor" && value > 1) ) { throw new Error(`${field} is outside its allowed range.`); } return; } if (field === "cosPhi") { if ( value !== null && (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) ) { throw new Error("cosPhi must be between zero and one or null."); } return; } if (field === "voltageV") { if ( value !== null && (typeof value !== "number" || !Number.isFinite(value) || value <= 0) ) { throw new Error("voltageV must be positive or null."); } return; } if (value !== null && typeof value !== "string") { throw new Error(`${field} must be a string or null.`); } } function isProjectDeviceUpdateField( value: string ): value is ProjectDeviceUpdateField { return projectDeviceUpdateFields.includes( value as ProjectDeviceUpdateField ); } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }