import { and, asc, eq, inArray } from "drizzle-orm"; import { assertCircuitDeleteProjectCommand, assertCircuitInsertProjectCommand, circuitDeleteCommandType, circuitInsertCommandType, createCircuitDeleteProjectCommand, createCircuitInsertProjectCommand, type CircuitSnapshot, type CircuitStructureProjectCommand, } from "../../domain/models/circuit-structure-project-command.model.js"; import type { CircuitStructureProjectCommandStore, ExecuteCircuitStructureCommandInput, } from "../../domain/ports/circuit-structure-project-command.store.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { assertCircuitDeviceRowReferencesInProject, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class CircuitStructureProjectCommandRepository implements CircuitStructureProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteCircuitStructureCommandInput) { return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand( tx, input.projectId, input.source, input.command ) ); } private applyCommand( database: AppDatabase, projectId: string, source: ExecuteCircuitStructureCommandInput["source"], command: CircuitStructureProjectCommand ): CircuitStructureProjectCommand { if (command.type === circuitInsertCommandType) { assertCircuitInsertProjectCommand(command); return this.insert( database, projectId, source, command.payload.circuit ); } if (command.type === circuitDeleteCommandType) { assertCircuitDeleteProjectCommand(command); return this.delete( database, projectId, command.payload.circuitId, command.payload.expectedCircuitListId ); } throw new Error("Unsupported circuit structure command."); } private insert( database: AppDatabase, projectId: string, source: ExecuteCircuitStructureCommandInput["source"], snapshot: CircuitSnapshot ) { this.assertCircuitLocation(database, projectId, snapshot); const existingCircuit = database .select({ id: circuits.id }) .from(circuits) .where(eq(circuits.id, snapshot.id)) .get(); if (existingCircuit) { throw new Error("Circuit id already exists."); } const duplicateIdentifier = database .select({ id: circuits.id }) .from(circuits) .where( and( eq(circuits.circuitListId, snapshot.circuitListId), eq( circuits.equipmentIdentifier, snapshot.equipmentIdentifier ) ) ) .get(); if (duplicateIdentifier) { throw new Error( "Duplicate equipmentIdentifier in circuit list." ); } if (snapshot.deviceRows.length > 0) { const rowIds = snapshot.deviceRows.map((row) => row.id); const existingRow = database .select({ id: circuitDeviceRows.id }) .from(circuitDeviceRows) .where(inArray(circuitDeviceRows.id, rowIds)) .limit(1) .get(); if (existingRow) { throw new Error("Circuit device-row id already exists."); } for (const row of snapshot.deviceRows) { if (source === "user" && row.legacyConsumerId !== null) { throw new Error( "User commands cannot assign legacy consumer ids." ); } assertCircuitDeviceRowReferencesInProject( database, projectId, row ); } } database .insert(circuits) .values({ id: snapshot.id, circuitListId: snapshot.circuitListId, sectionId: snapshot.sectionId, equipmentIdentifier: snapshot.equipmentIdentifier, displayName: snapshot.displayName, sortOrder: snapshot.sortOrder, protectionType: snapshot.protectionType, protectionRatedCurrent: snapshot.protectionRatedCurrent, protectionCharacteristic: snapshot.protectionCharacteristic, cableType: snapshot.cableType, cableCrossSection: snapshot.cableCrossSection, cableLength: snapshot.cableLength, rcdAssignment: snapshot.rcdAssignment, terminalDesignation: snapshot.terminalDesignation, voltage: snapshot.voltage, controlRequirement: snapshot.controlRequirement, status: snapshot.status, isReserve: snapshot.isReserve ? 1 : 0, remark: snapshot.remark, }) .run(); if (snapshot.deviceRows.length > 0) { database.insert(circuitDeviceRows).values(snapshot.deviceRows).run(); } return createCircuitDeleteProjectCommand( snapshot.id, snapshot.circuitListId ); } private delete( database: AppDatabase, projectId: string, circuitId: string, expectedCircuitListId: string ) { const circuit = database .select() .from(circuits) .where(eq(circuits.id, circuitId)) .get(); if (!circuit || circuit.circuitListId !== expectedCircuitListId) { throw new Error("Circuit changed before command execution."); } const owningList = database .select({ id: circuitLists.id }) .from(circuitLists) .where( and( eq(circuitLists.id, circuit.circuitListId), eq(circuitLists.projectId, projectId) ) ) .get(); if (!owningList) { throw new Error("Circuit does not belong to project."); } const rows = database .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.circuitId, circuit.id)) .orderBy( asc(circuitDeviceRows.sortOrder), asc(circuitDeviceRows.id) ) .all(); const inverse = createCircuitInsertProjectCommand({ id: circuit.id, circuitListId: circuit.circuitListId, sectionId: circuit.sectionId, equipmentIdentifier: circuit.equipmentIdentifier, displayName: circuit.displayName, sortOrder: circuit.sortOrder, protectionType: circuit.protectionType, protectionRatedCurrent: circuit.protectionRatedCurrent, protectionCharacteristic: circuit.protectionCharacteristic, cableType: circuit.cableType, cableCrossSection: circuit.cableCrossSection, cableLength: circuit.cableLength, rcdAssignment: circuit.rcdAssignment, terminalDesignation: circuit.terminalDesignation, voltage: circuit.voltage, controlRequirement: circuit.controlRequirement, status: circuit.status, isReserve: Boolean(circuit.isReserve), remark: circuit.remark, deviceRows: rows.map(toCircuitDeviceRowSnapshot), }); const result = database .delete(circuits) .where( and( eq(circuits.id, circuitId), eq(circuits.circuitListId, expectedCircuitListId) ) ) .run(); if (result.changes !== 1) { throw new Error("Circuit could not be deleted."); } return inverse; } private assertCircuitLocation( database: AppDatabase, projectId: string, snapshot: CircuitSnapshot ) { const list = database .select({ id: circuitLists.id }) .from(circuitLists) .where( and( eq(circuitLists.id, snapshot.circuitListId), eq(circuitLists.projectId, projectId) ) ) .get(); if (!list) { throw new Error("Circuit list does not belong to project."); } const section = database .select({ id: circuitSections.id }) .from(circuitSections) .where( and( eq(circuitSections.id, snapshot.sectionId), eq( circuitSections.circuitListId, snapshot.circuitListId ) ) ) .get(); if (!section) { throw new Error("Section does not belong to circuit list."); } } }