import { and, eq, inArray, isNull } from "drizzle-orm"; import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; import type { AppDatabase } from "../database-context.js"; import { externalModelObjects } from "../schema/external-model-objects.js"; import { externalRoomMappings } from "../schema/external-room-mappings.js"; export interface ExternalObjectLinkTransition { expected: ExternalModelObjectSnapshot; target: ExternalModelObjectSnapshot; } export function loadExpectedExternalObjectTransitions( database: AppDatabase, projectId: string, transitions: ExternalObjectLinkTransition[], changedMessage: string ) { const byId = new Map( transitions.map((transition) => [transition.expected.id, transition]) ); const current = database.select().from(externalModelObjects) .where(inArray(externalModelObjects.id, [...byId.keys()])).all(); if (current.length !== byId.size) { throw new Error("One or more external objects no longer exist."); } for (const object of current) { if ( object.projectId !== projectId || !snapshotsEqual(toExternalObjectSnapshot(object), byId.get(object.id)!.expected) ) { throw new Error(changedMessage); } } return transitions; } export function assertExternalObjectsCompatibleWithRow(input: { database: AppDatabase; projectId: string; row: CircuitDeviceRowSnapshot; distributionBoardId: string; category: CircuitGroupCategory; assignedObjects: ExternalModelObjectSnapshot[]; allLinkedObjects: ExternalModelObjectSnapshot[]; confirmedConflictObjectIds: ReadonlySet; }) { const mappings = input.database.select().from(externalRoomMappings) .where(eq(externalRoomMappings.projectId, input.projectId)).all(); const roomIdByMappingId = new Map(mappings.map((mapping) => [mapping.id, mapping.roomId])); const markers = new Set( input.allLinkedObjects.map((object) => object.acceptedSourceValues.selectionMarker.trim() ) ); if (markers.size > 1) { throw new Error("External objects with different selection markers require separate rows."); } for (const object of input.assignedObjects) { if (object.distributionBoardId !== input.distributionBoardId) { throw new Error("External object distribution does not match target circuit."); } if (object.planningValues.category !== input.category) { throw new Error("External object category does not match target circuit group."); } const roomId = object.externalRoomMappingId === null ? null : roomIdByMappingId.get(object.externalRoomMappingId) ?? null; if (roomId !== input.row.roomId) { throw new Error("External object room does not match target device row."); } if ( !matchesRowPlanningValues(input.row, object) && !input.confirmedConflictObjectIds.has(object.id) ) { throw new Error("External object planning values require explicit conflict confirmation."); } } } export function applyExternalObjectLinks( database: AppDatabase, transitions: ExternalObjectLinkTransition[], changedMessage: string ) { for (const { expected, target } of transitions) { const updated = database.update(externalModelObjects) .set({ circuitDeviceRowId: target.circuitDeviceRowId }) .where(and( eq(externalModelObjects.id, expected.id), expected.circuitDeviceRowId === null ? isNull(externalModelObjects.circuitDeviceRowId) : eq(externalModelObjects.circuitDeviceRowId, expected.circuitDeviceRowId) )).run(); if (updated.changes !== 1) throw new Error(changedMessage); } } export function toExternalObjectSnapshot( object: typeof externalModelObjects.$inferSelect ): ExternalModelObjectSnapshot { return { ...object }; } export function snapshotsEqual(left: unknown, right: unknown) { return canonicalJson(left) === canonicalJson(right); } function matchesRowPlanningValues( row: CircuitDeviceRowSnapshot, object: ExternalModelObjectSnapshot ) { const planning = object.planningValues; return ( row.displayName === (planning.displayName ?? row.displayName) && row.linkedProjectDeviceId === object.linkedProjectDeviceId && row.category === planning.category && row.connectionKind === planning.connectionKind && (planning.powerPerUnitW === null || row.powerPerUnit === planning.powerPerUnitW / 1000) && row.simultaneityFactor === planning.simultaneityFactor && row.cosPhi === planning.cosPhi && row.costGroup === planning.costGroup && row.remark === planning.remark ); } function canonicalJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; if (value !== null && typeof value === "object") { const record = value as Record; return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}` ).join(",")}}`; } return JSON.stringify(value); }