import { and, asc, eq } from "drizzle-orm"; import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js"; import { assertExternalObjectNewCircuitProjectCommand, externalObjectAssignToNewCircuitCommandType, externalObjectDeleteCreatedCircuitCommandType, invertExternalObjectNewCircuitProjectCommand, type ExternalObjectNewCircuitProjectCommand, } from "../../domain/models/external-object-new-circuit-project-command.model.js"; import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js"; import type { ExternalObjectNewCircuitProjectCommandStore } from "../../domain/ports/external-object-new-circuit-project-command.store.js"; import { parseGroupedEquipmentIdentifier } from "../../domain/services/circuit-group-numbering.js"; import { isElectricalPhaseType } from "../../domain/services/project-voltage.service.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { externalModelObjects } from "../schema/external-model-objects.js"; import { assertCircuitDeviceRowReferencesInProject, toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; import { applyExternalObjectLinks, assertExternalObjectsCompatibleWithRow, loadExpectedExternalObjectTransitions, snapshotsEqual, } from "./external-object-assignment.persistence.js"; export class ExternalObjectNewCircuitProjectCommandRepository implements ExternalObjectNewCircuitProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: Parameters[0]) { assertExternalObjectNewCircuitProjectCommand(input.command); if ( input.source === "user" && input.command.type === externalObjectDeleteCreatedCircuitCommandType ) { throw new Error("Created external circuits may only be removed through project history."); } return executeProjectCommandTransaction(this.database, input, (tx) => { if (input.command.type === externalObjectAssignToNewCircuitCommandType) { this.insert(tx, input.projectId, input.command); } else { this.remove(tx, input.projectId, input.command); } return invertExternalObjectNewCircuitProjectCommand(input.command); }); } private insert( database: AppDatabase, projectId: string, command: ExternalObjectNewCircuitProjectCommand ) { const circuit = command.payload.circuit; const row = circuit.deviceRows[0]!; const context = this.loadSectionContext( database, projectId, circuit.circuitListId, circuit.sectionId ); const parsedIdentifier = parseGroupedEquipmentIdentifier(circuit.equipmentIdentifier); if ( !parsedIdentifier || parsedIdentifier.kind !== "circuit" || parsedIdentifier.category !== context.category || parsedIdentifier.groupNumber !== context.groupNumber ) { throw new Error("External circuit equipment identifier does not match target group."); } if (row.category !== context.category || !isElectricalPhaseType(row.phaseType)) { throw new Error("External device row does not match target circuit group."); } assertCircuitDeviceRowReferencesInProject(database, projectId, row); const voltage = resolveCircuitVoltage(database, projectId, circuit.sectionId, [row.phaseType]); if (circuit.voltage !== voltage) { throw new Error("External circuit voltage must match the project phase voltage."); } if (database.select({ id: circuits.id }).from(circuits) .where(eq(circuits.id, circuit.id)).get()) { throw new Error("External circuit id already exists."); } if (database.select({ id: circuits.id }).from(circuits).where(and( eq(circuits.circuitListId, circuit.circuitListId), eq(circuits.equipmentIdentifier, circuit.equipmentIdentifier) )).get()) { throw new Error("Duplicate equipmentIdentifier in circuit list."); } if (database.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, row.id)).get()) { throw new Error("External device-row id already exists."); } const objects = this.loadExpectedObjects(database, projectId, command); assertExternalObjectsCompatibleWithRow({ database, projectId, row, distributionBoardId: context.distributionBoardId, category: context.category, assignedObjects: objects.map(({ target }) => target), allLinkedObjects: objects.map(({ target }) => target), confirmedConflictObjectIds: new Set(command.payload.confirmedConflictObjectIds), }); assertCircuitDeviceRowQuantity({ quantity: row.quantity, manualQuantity: 0, externalObjects: objects.map(({ target }) => ({ effectiveQuantity: target.planningValues.effectiveQuantity, })), }); database.insert(circuits).values(toCircuitValues(circuit)).run(); database.insert(circuitDeviceRows).values(toCircuitDeviceRowInsertValues(row)).run(); database.insert(circuitProtectionDevices).values(circuit.protectionDevice!).run(); applyExternalObjectLinks( database, objects, "External object changed during new-circuit assignment." ); } private remove( database: AppDatabase, projectId: string, command: ExternalObjectNewCircuitProjectCommand ) { const expectedCircuit = command.payload.circuit; this.loadSectionContext( database, projectId, expectedCircuit.circuitListId, expectedCircuit.sectionId ); const current = this.loadCircuitSnapshot(database, expectedCircuit.id); if (!current || !snapshotsEqual(current, expectedCircuit)) { throw new Error("Created external circuit changed before history removal."); } const objects = this.loadExpectedObjects(database, projectId, command); const rowId = expectedCircuit.deviceRows[0]!.id; const linkedObjects = database.select({ id: externalModelObjects.id }) .from(externalModelObjects) .where(eq(externalModelObjects.circuitDeviceRowId, rowId)).all(); if ( linkedObjects.length !== objects.length || linkedObjects.some(({ id }) => !objects.some(({ expected }) => expected.id === id)) ) { throw new Error("Created external circuit has different object links."); } applyExternalObjectLinks( database, objects, "External object changed during new-circuit assignment." ); const deleted = database.delete(circuits).where(and( eq(circuits.id, expectedCircuit.id), eq(circuits.circuitListId, expectedCircuit.circuitListId) )).run(); if (deleted.changes !== 1) { throw new Error("Created external circuit changed during history removal."); } } private loadSectionContext( database: AppDatabase, projectId: string, circuitListId: string, sectionId: string ) { const context = database.select({ projectId: circuitLists.projectId, distributionBoardId: circuitLists.distributionBoardId, category: circuitSections.category, groupNumber: circuitSections.groupNumber, }).from(circuitSections) .innerJoin(circuitLists, eq(circuitLists.id, circuitSections.circuitListId)) .where(and( eq(circuitSections.id, sectionId), eq(circuitSections.circuitListId, circuitListId) )).get(); if ( !context || context.projectId !== projectId || context.category === null || context.groupNumber === null ) { throw new Error("Target circuit group does not belong to the project."); } return { distributionBoardId: context.distributionBoardId, category: context.category, groupNumber: context.groupNumber, }; } private loadExpectedObjects( database: AppDatabase, projectId: string, command: ExternalObjectNewCircuitProjectCommand ) { return loadExpectedExternalObjectTransitions( database, projectId, command.payload.objects, "External object changed before new-circuit assignment." ); } private loadCircuitSnapshot(database: AppDatabase, circuitId: string): CircuitSnapshot | null { const circuit = database.select().from(circuits).where(eq(circuits.id, circuitId)).get(); if (!circuit) return null; const rows = database.select().from(circuitDeviceRows) .where(eq(circuitDeviceRows.circuitId, circuitId)) .orderBy(asc(circuitDeviceRows.sortOrder), asc(circuitDeviceRows.id)).all(); const protection = database.select().from(circuitProtectionDevices) .where(eq(circuitProtectionDevices.circuitId, circuitId)).get(); return { ...circuit, isReserve: Boolean(circuit.isReserve), deviceRows: rows.map(toCircuitDeviceRowSnapshot), protectionDevice: protection ?? null, }; } } function toCircuitValues(circuit: CircuitSnapshot) { const { deviceRows: _deviceRows, protectionDevice: _protectionDevice, ...values } = circuit; return { ...values, isReserve: circuit.isReserve ? 1 : 0 }; }