import { and, eq, inArray } from "drizzle-orm"; import { assertCircuitDeviceRowMoveProjectCommand, assertCircuitDeviceRowMoveWithNewCircuitProjectCommand, circuitDeviceRowMoveCommandType, circuitDeviceRowMoveWithNewCircuitCommandType, createCircuitDeviceRowMoveProjectCommand, createCircuitDeviceRowMoveWithNewCircuitProjectCommand, type CircuitDeviceRowMoveAssignment, type CircuitDeviceRowMoveHistoryProjectCommand, } from "../../domain/models/circuit-device-row-move-project-command.model.js"; import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js"; import type { CircuitDeviceRowMoveProjectCommandStore, ExecuteCircuitDeviceRowMoveCommandInput, } from "../../domain/ports/circuit-device-row-move-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 { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; interface PersistedMoveRow { id: string; circuitId: string; sortOrder: number; phaseType: string | null; } export class CircuitDeviceRowMoveProjectCommandRepository implements CircuitDeviceRowMoveProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteCircuitDeviceRowMoveCommandInput) { this.assertSupportedCommand(input.command); return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) ); } private assertSupportedCommand( command: CircuitDeviceRowMoveHistoryProjectCommand ) { if (command.type === circuitDeviceRowMoveCommandType) { assertCircuitDeviceRowMoveProjectCommand(command); return; } if ( command.type === circuitDeviceRowMoveWithNewCircuitCommandType ) { assertCircuitDeviceRowMoveWithNewCircuitProjectCommand( command ); return; } throw new Error("Unsupported circuit device-row move command."); } private applyCommand( database: AppDatabase, projectId: string, command: CircuitDeviceRowMoveHistoryProjectCommand ): CircuitDeviceRowMoveHistoryProjectCommand { if (command.type === circuitDeviceRowMoveCommandType) { const rowsById = this.loadExpectedRows( database, command.payload.moves ); const circuitIds = this.collectCircuitIds( command.payload.moves ); this.assertExistingCircuitsInOneList( database, projectId, circuitIds ); const inverse = createCircuitDeviceRowMoveProjectCommand( this.reverseMoves(command.payload.moves, rowsById) ); this.applyMoves(database, command.payload.moves); this.updateReserveStates(database, circuitIds); this.updateVoltages(database, projectId, circuitIds); return inverse; } assertCircuitDeviceRowMoveWithNewCircuitProjectCommand( command ); const { targetCircuit, targetCircuitAction, moves } = command.payload; const rowsById = this.loadExpectedRows(database, moves); const appliedTargetCircuit = { ...targetCircuit, voltage: resolveCircuitVoltage( database, projectId, targetCircuit.sectionId, moves.map((move) => rowsById.get(move.rowId)?.phaseType) ), }; this.assertCircuitLocation(database, projectId, appliedTargetCircuit); if (targetCircuitAction === "create") { const sourceCircuitIds = [ ...new Set(moves.map((move) => move.expectedCircuitId)), ]; this.assertExistingCircuitsInOneList( database, projectId, sourceCircuitIds, targetCircuit.circuitListId ); this.assertTargetCircuitAvailable(database, appliedTargetCircuit); this.insertTargetCircuit(database, appliedTargetCircuit); const inverse = createCircuitDeviceRowMoveWithNewCircuitProjectCommand( "delete", appliedTargetCircuit, this.reverseMoves(moves, rowsById) ); this.applyMoves(database, moves); this.updateReserveStates(database, [ ...sourceCircuitIds, targetCircuit.id, ]); this.updateVoltages(database, projectId, [ ...sourceCircuitIds, targetCircuit.id, ]); return inverse; } this.assertTargetCircuitUnchanged( database, appliedTargetCircuit, moves.map((move) => move.rowId) ); const destinationCircuitIds = [ ...new Set(moves.map((move) => move.targetCircuitId)), ]; this.assertExistingCircuitsInOneList( database, projectId, destinationCircuitIds, targetCircuit.circuitListId ); const inverse = createCircuitDeviceRowMoveWithNewCircuitProjectCommand( "create", appliedTargetCircuit, this.reverseMoves(moves, rowsById) ); this.applyMoves(database, moves); this.updateReserveStates(database, [ targetCircuit.id, ...destinationCircuitIds, ]); this.updateVoltages( database, projectId, destinationCircuitIds ); const deleted = database .delete(circuits) .where( and( eq(circuits.id, targetCircuit.id), eq( circuits.circuitListId, targetCircuit.circuitListId ), eq(circuits.isReserve, 1) ) ) .run(); if (deleted.changes !== 1) { throw new Error( "Created target circuit changed before deletion." ); } return inverse; } private updateVoltages( database: AppDatabase, projectId: string, circuitIds: string[] ) { for (const circuitId of new Set(circuitIds)) { updateDerivedCircuitVoltage(database, projectId, circuitId); } } private loadExpectedRows( database: AppDatabase, moves: CircuitDeviceRowMoveAssignment[] ) { const rowIds = moves.map((move) => move.rowId); const persistedRows = database .select({ id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId, sortOrder: circuitDeviceRows.sortOrder, phaseType: circuitDeviceRows.phaseType, }) .from(circuitDeviceRows) .where(inArray(circuitDeviceRows.id, rowIds)) .all(); if (persistedRows.length !== rowIds.length) { throw new Error( "One or more circuit device rows do not exist." ); } const rowsById = new Map( persistedRows.map((row) => [row.id, row]) ); for (const move of moves) { const row = rowsById.get(move.rowId); if ( !row || row.circuitId !== move.expectedCircuitId || row.sortOrder !== move.expectedSortOrder ) { throw new Error( "Circuit device row changed before move execution." ); } } return rowsById; } private collectCircuitIds( moves: CircuitDeviceRowMoveAssignment[] ) { return [ ...new Set( moves.flatMap((move) => [ move.expectedCircuitId, move.targetCircuitId, ]) ), ]; } private assertExistingCircuitsInOneList( database: AppDatabase, projectId: string, circuitIds: string[], expectedCircuitListId?: string ) { const participatingCircuits = database .select({ id: circuits.id, circuitListId: circuits.circuitListId, projectId: circuitLists.projectId, }) .from(circuits) .innerJoin( circuitLists, eq(circuitLists.id, circuits.circuitListId) ) .where(inArray(circuits.id, circuitIds)) .all(); if (participatingCircuits.length !== circuitIds.length) { throw new Error( "One or more move circuits do not exist." ); } if ( participatingCircuits.some( (circuit) => circuit.projectId !== projectId ) ) { throw new Error("Move circuit does not belong to project."); } const circuitListIds = new Set( participatingCircuits.map( (circuit) => circuit.circuitListId ) ); if ( circuitListIds.size !== 1 || (expectedCircuitListId !== undefined && !circuitListIds.has(expectedCircuitListId)) ) { throw new Error( "All moved rows and targets must belong to one circuit list." ); } } 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."); } } private assertTargetCircuitAvailable( database: AppDatabase, snapshot: CircuitSnapshot ) { const existingCircuit = database .select({ id: circuits.id }) .from(circuits) .where(eq(circuits.id, snapshot.id)) .get(); if (existingCircuit) { throw new Error("Move target 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." ); } } private insertTargetCircuit( database: AppDatabase, snapshot: CircuitSnapshot ) { database .insert(circuits) .values({ id: snapshot.id, circuitListId: snapshot.circuitListId, sectionId: snapshot.sectionId, equipmentIdentifier: snapshot.equipmentIdentifier, displayName: snapshot.displayName, sortOrder: snapshot.sortOrder, 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: 1, remark: snapshot.remark, }) .run(); if (snapshot.protectionDevice) { database .insert(circuitProtectionDevices) .values(snapshot.protectionDevice) .run(); } } private assertTargetCircuitUnchanged( database: AppDatabase, snapshot: CircuitSnapshot, expectedRowIds: string[] ) { const current = database .select() .from(circuits) .where(eq(circuits.id, snapshot.id)) .get(); if ( !current || current.circuitListId !== snapshot.circuitListId || current.sectionId !== snapshot.sectionId || current.equipmentIdentifier !== snapshot.equipmentIdentifier || current.displayName !== snapshot.displayName || current.sortOrder !== snapshot.sortOrder || current.cableType !== snapshot.cableType || current.cableCrossSection !== snapshot.cableCrossSection || current.cableLength !== snapshot.cableLength || current.rcdAssignment !== snapshot.rcdAssignment || current.terminalDesignation !== snapshot.terminalDesignation || current.voltage !== snapshot.voltage || current.controlRequirement !== snapshot.controlRequirement || current.status !== snapshot.status || current.remark !== snapshot.remark || Boolean(current.isReserve) ) { throw new Error( "Created target circuit changed before command execution." ); } const protectionDevice = database .select() .from(circuitProtectionDevices) .where(eq(circuitProtectionDevices.circuitId, snapshot.id)) .get(); if ( snapshot.protectionDevice === undefined ? protectionDevice !== undefined : snapshot.protectionDevice === null ? protectionDevice !== undefined : !protectionDevice || Object.entries(snapshot.protectionDevice).some( ([key, value]) => (protectionDevice as Record)[key] !== value ) ) { throw new Error( "Created target circuit protection changed before command execution." ); } const currentRowIds = database .select({ id: circuitDeviceRows.id }) .from(circuitDeviceRows) .where(eq(circuitDeviceRows.circuitId, snapshot.id)) .all() .map((row) => row.id); const expectedRowIdSet = new Set(expectedRowIds); if ( currentRowIds.length !== expectedRowIds.length || currentRowIds.some((id) => !expectedRowIdSet.has(id)) ) { throw new Error( "Created target circuit rows changed before command execution." ); } } private reverseMoves( moves: CircuitDeviceRowMoveAssignment[], rowsById: Map ) { return moves.map((move) => { const current = rowsById.get(move.rowId)!; return { rowId: move.rowId, expectedCircuitId: move.targetCircuitId, expectedSortOrder: move.targetSortOrder, targetCircuitId: current.circuitId, targetSortOrder: current.sortOrder, }; }); } private applyMoves( database: AppDatabase, moves: CircuitDeviceRowMoveAssignment[] ) { for (const move of moves) { const result = database .update(circuitDeviceRows) .set({ circuitId: move.targetCircuitId, sortOrder: move.targetSortOrder, }) .where( and( eq(circuitDeviceRows.id, move.rowId), eq( circuitDeviceRows.circuitId, move.expectedCircuitId ), eq( circuitDeviceRows.sortOrder, move.expectedSortOrder ) ) ) .run(); if (result.changes !== 1) { throw new Error( "Circuit device row changed during move execution." ); } } } private updateReserveStates( database: AppDatabase, circuitIds: string[] ) { for (const circuitId of new Set(circuitIds)) { const remainingRow = database .select({ id: circuitDeviceRows.id }) .from(circuitDeviceRows) .where(eq(circuitDeviceRows.circuitId, circuitId)) .limit(1) .get(); const updated = database .update(circuits) .set({ isReserve: remainingRow ? 0 : 1 }) .where(eq(circuits.id, circuitId)) .run(); if (updated.changes !== 1) { throw new Error( "Circuit changed during device-row move execution." ); } } } }