import { eq } from "drizzle-orm"; import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js"; import { assertExternalObjectNewRowProjectCommand, createExternalObjectNewRowProjectCommand, externalObjectAssignToNewRowCommandType, externalObjectDeleteCreatedRowCommandType, invertExternalObjectNewRowProjectCommand, type ExternalObjectNewRowProjectCommand, } from "../../domain/models/external-object-new-row-project-command.model.js"; import type { ExternalObjectNewRowProjectCommandStore } from "../../domain/ports/external-object-new-row-project-command.store.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 { 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 { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js"; import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; import { applyExternalObjectLinks, assertExternalObjectsCompatibleWithRow, loadExpectedExternalObjectTransitions, snapshotsEqual, } from "./external-object-assignment.persistence.js"; export class ExternalObjectNewRowProjectCommandRepository implements ExternalObjectNewRowProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: Parameters[0]) { assertExternalObjectNewRowProjectCommand(input.command); if ( input.source === "user" && input.command.type === externalObjectDeleteCreatedRowCommandType ) { throw new Error("Created external rows may only be removed through project history."); } return executeProjectCommandTransactionWithAppliedForward(this.database, input, (tx) => { if (input.command.type === externalObjectAssignToNewRowCommandType) { this.insert(tx, input.projectId, input.command); } else { this.remove(tx, input.projectId, input.command); } const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( tx, input.source, [input.command.payload.row.circuitId], input.command.payload.circuitDisplayNames ); const forward = createExternalObjectNewRowProjectCommand( input.command.type, { ...input.command.payload, circuitDisplayNames } ); return { forward, inverse: invertExternalObjectNewRowProjectCommand(forward), }; }); } private insert( database: AppDatabase, projectId: string, command: ExternalObjectNewRowProjectCommand ) { const row = command.payload.row; const context = this.loadCircuitContext(database, projectId, row.circuitId); if (!isElectricalPhaseType(row.phaseType)) { throw new Error("External device-row phase type is invalid."); } if (row.category !== context.category) { throw new Error("External device-row category does not match target circuit group."); } assertCircuitDeviceRowReferencesInProject(database, projectId, row); 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(circuitDeviceRows).values(toCircuitDeviceRowInsertValues(row)).run(); this.updateCircuitState(database, projectId, row.circuitId); applyExternalObjectLinks( database, objects, "External object changed during new-row assignment." ); } private remove( database: AppDatabase, projectId: string, command: ExternalObjectNewRowProjectCommand ) { const expectedRow = command.payload.row; this.loadCircuitContext(database, projectId, expectedRow.circuitId); const currentRow = database.select().from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, expectedRow.id)).get(); if (!currentRow || !snapshotsEqual(toCircuitDeviceRowSnapshot(currentRow), expectedRow)) { throw new Error("Created external device row changed before history removal."); } const objects = this.loadExpectedObjects(database, projectId, command); const linkedObjects = database.select({ id: externalModelObjects.id }) .from(externalModelObjects) .where(eq(externalModelObjects.circuitDeviceRowId, expectedRow.id)).all(); if ( linkedObjects.length !== objects.length || linkedObjects.some(({ id }) => !objects.some((object) => object.expected.id === id)) ) { throw new Error("Created external device row has different object links."); } applyExternalObjectLinks( database, objects, "External object changed during new-row assignment." ); const deleted = database.delete(circuitDeviceRows) .where(eq(circuitDeviceRows.id, expectedRow.id)).run(); if (deleted.changes !== 1) { throw new Error("Created external device row changed during history removal."); } this.updateCircuitState(database, projectId, expectedRow.circuitId); } private loadCircuitContext(database: AppDatabase, projectId: string, circuitId: string) { const context = database.select({ projectId: circuitLists.projectId, distributionBoardId: circuitLists.distributionBoardId, category: circuitSections.category, }).from(circuits) .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) .innerJoin(circuitSections, eq(circuitSections.id, circuits.sectionId)) .where(eq(circuits.id, circuitId)).get(); if (!context || context.projectId !== projectId || context.category === null) { throw new Error("Target circuit does not belong to the project."); } return { projectId: context.projectId, distributionBoardId: context.distributionBoardId, category: context.category, }; } private loadExpectedObjects( database: AppDatabase, projectId: string, command: ExternalObjectNewRowProjectCommand ) { return loadExpectedExternalObjectTransitions( database, projectId, command.payload.objects, "External object changed before new-row assignment." ); } private updateCircuitState(database: AppDatabase, projectId: string, circuitId: string) { const remaining = database.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows) .where(eq(circuitDeviceRows.circuitId, circuitId)).limit(1).get(); const result = database.update(circuits).set({ isReserve: remaining ? 0 : 1 }) .where(eq(circuits.id, circuitId)).run(); if (result.changes !== 1) throw new Error("Target circuit changed during row assignment."); updateDerivedCircuitVoltage(database, projectId, circuitId); } }