Add external object new circuit command
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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<string>;
|
||||
}) {
|
||||
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<string, unknown>;
|
||||
return `{${Object.keys(record).sort().map((key) =>
|
||||
`${JSON.stringify(key)}:${canonicalJson(record[key])}`
|
||||
).join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
Reference in New Issue
Block a user