import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; import { assertCircuitDeviceRowInsertProjectCommand, circuitDeviceRowInsertCommandType, circuitDeviceRowStructureCommandSchemaVersion, type CircuitDeviceRowSnapshot, } from "./circuit-device-row-structure-project-command.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; export const externalObjectAssignToNewRowCommandType = "external-object.assign-to-new-row" as const; export const externalObjectDeleteCreatedRowCommandType = "external-object.unassign-and-delete-created-row" as const; export const externalObjectNewRowCommandSchemaVersion = 1 as const; export interface ExternalObjectNewRowPayload { row: CircuitDeviceRowSnapshot; objects: Array<{ expected: ExternalModelObjectSnapshot; target: ExternalModelObjectSnapshot; }>; confirmedConflictObjectIds: string[]; } export interface ExternalObjectNewRowProjectCommand extends SerializedProjectCommand { schemaVersion: typeof externalObjectNewRowCommandSchemaVersion; type: | typeof externalObjectAssignToNewRowCommandType | typeof externalObjectDeleteCreatedRowCommandType; } export function createExternalObjectNewRowProjectCommand( type: ExternalObjectNewRowProjectCommand["type"], payload: ExternalObjectNewRowPayload ): ExternalObjectNewRowProjectCommand { const command: ExternalObjectNewRowProjectCommand = { schemaVersion: externalObjectNewRowCommandSchemaVersion, type, payload: { row: { ...payload.row, manualQuantity: payload.row.manualQuantity ?? payload.row.quantity, }, objects: payload.objects, confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds], }, }; assertExternalObjectNewRowProjectCommand(command); return command; } export function invertExternalObjectNewRowProjectCommand( command: ExternalObjectNewRowProjectCommand ) { return createExternalObjectNewRowProjectCommand( command.type === externalObjectAssignToNewRowCommandType ? externalObjectDeleteCreatedRowCommandType : externalObjectAssignToNewRowCommandType, { row: command.payload.row, objects: command.payload.objects.map(({ expected, target }) => ({ expected: target, target: expected, })), confirmedConflictObjectIds: command.payload.confirmedConflictObjectIds, } ); } export function assertExternalObjectNewRowProjectCommand( command: SerializedProjectCommand ): asserts command is ExternalObjectNewRowProjectCommand { if ( command.schemaVersion !== externalObjectNewRowCommandSchemaVersion || (command.type !== externalObjectAssignToNewRowCommandType && command.type !== externalObjectDeleteCreatedRowCommandType) || !isRecord(command.payload) || !Array.isArray(command.payload.objects) || command.payload.objects.length === 0 || !Array.isArray(command.payload.confirmedConflictObjectIds) ) { throw new Error("Unsupported external object new-row command."); } const row = parseRow(command.payload.row); if (row.manualQuantity !== 0) { throw new Error("An external-only device row must start with zero manual quantity."); } const objectIds = new Set(); for (const transition of command.payload.objects) { if (!isRecord(transition)) throw new Error("Invalid external object transition."); const expected = parseObject(transition.expected); const target = parseObject(transition.target); if (expected.id !== target.id || objectIds.has(expected.id)) { throw new Error("New-row assignment contains mismatched or duplicate objects."); } objectIds.add(expected.id); const assigning = command.type === externalObjectAssignToNewRowCommandType; if ( (assigning && (expected.circuitDeviceRowId !== null || target.circuitDeviceRowId !== row.id)) || (!assigning && (expected.circuitDeviceRowId !== row.id || target.circuitDeviceRowId !== null)) ) { throw new Error("External object links do not match the new-row action."); } assertOnlyAssignmentChanged(expected, target); } const confirmedIds = new Set(); for (const objectId of command.payload.confirmedConflictObjectIds) { if (typeof objectId !== "string" || !objectIds.has(objectId) || confirmedIds.has(objectId)) { throw new Error("New-row assignment contains an invalid conflict confirmation."); } confirmedIds.add(objectId); } } function parseRow(value: unknown): CircuitDeviceRowSnapshot { const envelope = { schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, type: circuitDeviceRowInsertCommandType, payload: { row: value }, }; assertCircuitDeviceRowInsertProjectCommand(envelope); const row = envelope.payload.row; if (row.manualQuantity === undefined) { throw new Error("External object new-row command requires manual quantity."); } return row; } function parseObject(value: unknown): ExternalModelObjectSnapshot { return parseExternalModelStateSnapshot({ source: null, importBatches: [], roomMappings: [], objects: [value], }).objects[0]!; } function assertOnlyAssignmentChanged( expected: ExternalModelObjectSnapshot, target: ExternalModelObjectSnapshot ) { const expectedRest = { ...expected, circuitDeviceRowId: null }; const targetRest = { ...target, circuitDeviceRowId: null }; if (canonicalJson(expectedRest) !== canonicalJson(targetRest)) { throw new Error("New-row assignment may only change the external object row link."); } } function canonicalJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; if (isRecord(value)) { return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}` ).join(",")}}`; } return JSON.stringify(value); } function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }