import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js"; import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const externalInitialImportCommandType = "external-import.apply-initial" as const; export const externalInitialImportCommandSchemaVersion = 1 as const; export interface ExternalInitialImportPayload { expected: ExternalModelStateSnapshot; target: ExternalModelStateSnapshot; } export interface ExternalInitialImportProjectCommand extends SerializedProjectCommand { schemaVersion: typeof externalInitialImportCommandSchemaVersion; type: typeof externalInitialImportCommandType; } export function createExternalInitialImportProjectCommand( expected: ExternalModelStateSnapshot, target: ExternalModelStateSnapshot ): ExternalInitialImportProjectCommand { const command: ExternalInitialImportProjectCommand = { schemaVersion: externalInitialImportCommandSchemaVersion, type: externalInitialImportCommandType, payload: { expected, target }, }; assertExternalInitialImportProjectCommand(command); return command; } export function assertExternalInitialImportProjectCommand( command: SerializedProjectCommand ): asserts command is ExternalInitialImportProjectCommand { if ( command.schemaVersion !== externalInitialImportCommandSchemaVersion || command.type !== externalInitialImportCommandType || !isRecord(command.payload) || Object.keys(command.payload).length !== 2 ) { throw new Error("Unsupported external initial import command."); } const expected = parseExternalModelStateSnapshot(command.payload.expected); const target = parseExternalModelStateSnapshot(command.payload.target); const expectedIsEmpty = isEmptyExternalModelState(expected); const targetIsEmpty = isEmptyExternalModelState(target); if (expectedIsEmpty === targetIsEmpty) { throw new Error("External initial import must transition between empty and populated state."); } assertPopulatedInitialState(expectedIsEmpty ? target : expected); } export function createEmptyExternalModelState(): ExternalModelStateSnapshot { return { source: null, importBatches: [], roomMappings: [], objects: [] }; } export function isEmptyExternalModelState(state: ExternalModelStateSnapshot) { return ( state.source === null && state.importBatches.length === 0 && state.roomMappings.length === 0 && state.objects.length === 0 ); } function assertPopulatedInitialState(state: ExternalModelStateSnapshot) { if (state.source === null || state.importBatches.length !== 1 || state.objects.length === 0) { throw new Error("External initial import requires one source, one batch and at least one object."); } const source = state.source; const batch = state.importBatches[0]; if ( batch.importKind !== "initial" || batch.projectId !== source.projectId || batch.sourceId !== source.id ) { throw new Error("External initial import batch identity is invalid."); } const mappingIds = uniqueIds(state.roomMappings, "room mapping"); const roomKeys = new Set(); for (const mapping of state.roomMappings) { if (mapping.projectId !== source.projectId || mapping.sourceId !== source.id) { throw new Error("External initial room mapping identity is invalid."); } if (roomKeys.has(mapping.normalizedSourceRoomKey)) { throw new Error("External initial import contains duplicate room keys."); } roomKeys.add(mapping.normalizedSourceRoomKey); } uniqueIds(state.objects, "object"); const ifcGuids = new Set(); for (const object of state.objects) { if ( object.projectId !== source.projectId || object.sourceId !== source.id || object.lastSeenImportBatchId !== batch.id || object.lastAcceptedImportBatchId !== batch.id ) { throw new Error("External initial object identity is invalid."); } if (ifcGuids.has(object.ifcGuid)) { throw new Error("External initial import contains duplicate IFCGUIDs."); } ifcGuids.add(object.ifcGuid); if ( object.externalRoomMappingId !== null && !mappingIds.has(object.externalRoomMappingId) ) { throw new Error("External initial object room mapping is invalid."); } if (object.circuitDeviceRowId !== null) { throw new Error("External initial import must not assign circuit device rows."); } if (object.presenceStatus !== "present") { throw new Error("External initial import objects must be present."); } } } function uniqueIds(entries: ReadonlyArray<{ id: string }>, label: string) { const ids = new Set(); for (const entry of entries) { if (ids.has(entry.id)) { throw new Error(`External initial import contains duplicate ${label} ids.`); } ids.add(entry.id); } return ids; } function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }