import { and, eq, inArray } from "drizzle-orm"; import { assertProjectDeviceRowSyncProjectCommand, createProjectDeviceRowSyncProjectCommand, invertProjectDeviceRowSyncOperation, projectDeviceSyncRowSnapshotFields, type ProjectDeviceSyncRowSnapshot, } from "../../domain/models/project-device-row-sync-project-command.model.js"; import type { ExecuteProjectDeviceRowSyncCommandInput, ProjectDeviceRowSyncProjectCommandStore, } from "../../domain/ports/project-device-row-sync-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 { circuits } from "../schema/circuits.js"; import { projectDevices } from "../schema/project-devices.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect; export class ProjectDeviceRowSyncProjectCommandRepository implements ProjectDeviceRowSyncProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteProjectDeviceRowSyncCommandInput) { assertProjectDeviceRowSyncProjectCommand(input.command); return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand(tx, input) ); } private applyCommand( tx: AppDatabase, input: ExecuteProjectDeviceRowSyncCommandInput ) { const projectDevice = tx .select({ id: projectDevices.id }) .from(projectDevices) .where( and( eq( projectDevices.id, input.command.payload.projectDeviceId ), eq(projectDevices.projectId, input.projectId) ) ) .get(); if (!projectDevice) { throw new Error( "Project device does not belong to project." ); } const rowIds = input.command.payload.rows.map( (row) => row.rowId ); const persistedRows = tx .select({ row: circuitDeviceRows, projectId: circuitLists.projectId, }) .from(circuitDeviceRows) .innerJoin( circuits, eq(circuits.id, circuitDeviceRows.circuitId) ) .innerJoin( circuitLists, eq(circuitLists.id, circuits.circuitListId) ) .where(inArray(circuitDeviceRows.id, rowIds)) .all(); if ( persistedRows.length !== rowIds.length || persistedRows.some( (persisted) => persisted.projectId !== input.projectId ) ) { throw new Error( "One or more sync rows do not belong to project." ); } const persistedById = new Map( persistedRows.map((persisted) => [ persisted.row.id, persisted.row, ]) ); for (const assignment of input.command.payload.rows) { const persisted = persistedById.get(assignment.rowId); if ( !persisted || !snapshotMatchesRow(assignment.expected, persisted) ) { throw new Error( "Project-device row changed before sync execution." ); } } const inverse = createProjectDeviceRowSyncProjectCommand( projectDevice.id, invertProjectDeviceRowSyncOperation( input.command.payload.operation ), input.command.payload.rows.map((row) => ({ rowId: row.rowId, expected: row.target, target: row.expected, })) ); for (const assignment of input.command.payload.rows) { const updated = tx .update(circuitDeviceRows) .set(assignment.target) .where(eq(circuitDeviceRows.id, assignment.rowId)) .run(); if (updated.changes !== 1) { throw new Error( "Project-device row changed during sync execution." ); } } const affectedCircuitIds = new Set( input.command.payload.rows .filter( (assignment) => assignment.expected.phaseType !== assignment.target.phaseType ) .map( (assignment) => persistedById.get(assignment.rowId)!.circuitId ) ); for (const circuitId of affectedCircuitIds) { updateDerivedCircuitVoltage( tx, input.projectId, circuitId ); } return inverse; } } function snapshotMatchesRow( snapshot: ProjectDeviceSyncRowSnapshot, row: CircuitDeviceRow ) { return projectDeviceSyncRowSnapshotFields.every( (field) => snapshot[field] === row[field] ); }