import { and, eq, inArray } from "drizzle-orm"; import { assertCircuitDeviceRowQuantity, } from "../../domain/calculations/circuit-device-row-quantity.js"; import { assertExternalObjectRowAssignmentProjectCommand, invertExternalObjectRowAssignmentProjectCommand, } from "../../domain/models/external-object-row-assignment-project-command.model.js"; import type { ExternalObjectRowAssignmentProjectCommandStore } from "../../domain/ports/external-object-row-assignment-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 { externalModelObjects } from "../schema/external-model-objects.js"; import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { applyExternalObjectLinks, assertExternalObjectsCompatibleWithRow, loadExpectedExternalObjectTransitions, snapshotsEqual, toExternalObjectSnapshot, } from "./external-object-assignment.persistence.js"; export class ExternalObjectRowAssignmentProjectCommandRepository implements ExternalObjectRowAssignmentProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: Parameters[0]) { assertExternalObjectRowAssignmentProjectCommand(input.command); return executeProjectCommandTransaction(this.database, input, (tx) => { this.apply(tx, input.projectId, input.command); return invertExternalObjectRowAssignmentProjectCommand(input.command); }); } private apply( database: AppDatabase, projectId: string, command: Parameters[0]["command"] ) { const rowTransitions = new Map( command.payload.rows.map((transition) => [transition.expected.id, transition]) ); const objectTransitions = new Map( command.payload.objects.map((transition) => [transition.expected.id, transition]) ); const rowIds = [...rowTransitions.keys()]; const currentRows = database.select().from(circuitDeviceRows) .where(inArray(circuitDeviceRows.id, rowIds)).all(); if (currentRows.length !== rowIds.length) { throw new Error("External object assignment target no longer exists."); } for (const row of currentRows) { const expected = rowTransitions.get(row.id)!.expected; if (!snapshotsEqual(toCircuitDeviceRowSnapshot(row), expected)) { throw new Error("Circuit device row changed before external object assignment."); } } loadExpectedExternalObjectTransitions( database, projectId, command.payload.objects, "External object changed before row assignment." ); const rowContexts = database .select({ rowId: circuitDeviceRows.id, projectId: circuitLists.projectId, distributionBoardId: circuitLists.distributionBoardId, category: circuitSections.category, }) .from(circuitDeviceRows) .innerJoin(circuits, eq(circuits.id, circuitDeviceRows.circuitId)) .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) .innerJoin(circuitSections, eq(circuitSections.id, circuits.sectionId)) .where(inArray(circuitDeviceRows.id, rowIds)) .all(); if ( rowContexts.length !== rowIds.length || rowContexts.some((context) => context.projectId !== projectId || context.category === null ) ) { throw new Error("Circuit device row belongs to another project."); } const contextByRowId = new Map(rowContexts.map((context) => [context.rowId, context])); const allProjectObjects = database.select().from(externalModelObjects) .where(eq(externalModelObjects.projectId, projectId)).all() .map(toExternalObjectSnapshot); const targetObjects = allProjectObjects.map((object) => objectTransitions.get(object.id)?.target ?? object ); const confirmedConflicts = new Set(command.payload.confirmedConflictObjectIds); for (const transition of command.payload.rows) { const linkedObjects = targetObjects.filter( (object) => object.circuitDeviceRowId === transition.target.id ); const assignedObjects = command.payload.objects .map(({ target }) => target) .filter((object) => object.circuitDeviceRowId === transition.target.id); const context = contextByRowId.get(transition.target.id)!; assertExternalObjectsCompatibleWithRow({ database, projectId, row: transition.target, distributionBoardId: context.distributionBoardId, category: context.category!, assignedObjects, allLinkedObjects: linkedObjects, confirmedConflictObjectIds: confirmedConflicts, }); assertCircuitDeviceRowQuantity({ quantity: transition.target.quantity, manualQuantity: transition.target.manualQuantity ?? transition.target.quantity, externalObjects: linkedObjects.map((object) => ({ effectiveQuantity: object.planningValues.effectiveQuantity, })), }); } for (const transition of command.payload.rows) { const result = database.update(circuitDeviceRows) .set({ quantity: transition.target.quantity }) .where(and( eq(circuitDeviceRows.id, transition.expected.id), eq(circuitDeviceRows.quantity, transition.expected.quantity), eq(circuitDeviceRows.manualQuantity, transition.expected.manualQuantity ?? transition.expected.quantity) )).run(); if (result.changes !== 1) throw new Error("Circuit device row changed during assignment."); } applyExternalObjectLinks( database, command.payload.objects, "External object changed during assignment." ); } }