import { and, asc, eq, inArray } from "drizzle-orm"; import { assertDistributionBoardDeleteSubtreeProjectCommand, assertDistributionBoardInsertSubtreeProjectCommand, createDistributionBoardDeleteSubtreeProjectCommand, createDistributionBoardInsertSubtreeProjectCommand, distributionBoardDeleteSubtreeCommandType, distributionBoardInsertSubtreeCommandType, type DistributionBoardSubtreeProjectCommand, } from "../../domain/models/distribution-board-subtree-project-command.model.js"; import { assertDistributionBoardSubtreeSnapshot, type DistributionBoardSubtreeSnapshot, } from "../../domain/models/distribution-board-subtree-snapshot.model.js"; import type { DistributionBoardSubtreeProjectCommandStore, ExecuteDistributionBoardSubtreeCommandInput, } from "../../domain/ports/distribution-board-subtree-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 { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { floors } from "../schema/floors.js"; import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class DistributionBoardSubtreeProjectCommandRepository implements DistributionBoardSubtreeProjectCommandStore { constructor(private readonly database: AppDatabase) {} capture(projectId: string, distributionBoardId: string) { return this.captureFrom( this.database, projectId, distributionBoardId ); } execute(input: ExecuteDistributionBoardSubtreeCommandInput) { if (input.command.type === distributionBoardInsertSubtreeCommandType) { assertDistributionBoardInsertSubtreeProjectCommand(input.command); } else { assertDistributionBoardDeleteSubtreeProjectCommand(input.command); } return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) ); } private applyCommand( database: AppDatabase, projectId: string, command: DistributionBoardSubtreeProjectCommand ) { if (command.type === distributionBoardInsertSubtreeCommandType) { this.insert(database, projectId, command.payload.snapshot); return createDistributionBoardDeleteSubtreeProjectCommand( command.payload.snapshot ); } if (command.type === distributionBoardDeleteSubtreeCommandType) { const current = this.captureFrom( database, projectId, command.payload.snapshot.distributionBoard.id ); if (!sameSnapshot(current, command.payload.snapshot)) { throw new Error( "Distribution-board subtree changed before deletion." ); } const deleted = database .delete(distributionBoards) .where( and( eq( distributionBoards.id, current.distributionBoard.id ), eq(distributionBoards.projectId, projectId) ) ) .run(); if (deleted.changes !== 1) { throw new Error( "Distribution-board subtree could not be deleted." ); } return createDistributionBoardInsertSubtreeProjectCommand( command.payload.snapshot ); } throw new Error("Unsupported distribution-board subtree command."); } private captureFrom( database: AppDatabase, projectId: string, distributionBoardId: string ): DistributionBoardSubtreeSnapshot { const distributionBoard = database .select() .from(distributionBoards) .where( and( eq(distributionBoards.id, distributionBoardId), eq(distributionBoards.projectId, projectId) ) ) .get(); if (!distributionBoard) { throw new Error("Distribution board not found."); } const circuitList = database .select() .from(circuitLists) .where( and( eq(circuitLists.distributionBoardId, distributionBoard.id), eq(circuitLists.projectId, projectId) ) ) .get(); if (!circuitList) { throw new Error("Distribution-board circuit list not found."); } const sections = database .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, circuitList.id)) .orderBy(asc(circuitSections.sortOrder), asc(circuitSections.id)) .all(); const components = database .select() .from(distributionBoardComponents) .where(eq(distributionBoardComponents.circuitListId, circuitList.id)) .orderBy( asc(distributionBoardComponents.sortOrder), asc(distributionBoardComponents.id) ) .all() .map((component) => ({ component, protectionDevice: database .select() .from(distributionBoardComponentProtectionDevices) .where( eq( distributionBoardComponentProtectionDevices.componentId, component.id ) ) .get() ?? null, })); const circuitSnapshots = database .select() .from(circuits) .where(eq(circuits.circuitListId, circuitList.id)) .orderBy(asc(circuits.sortOrder), asc(circuits.id)) .all() .map((circuit) => { const { isReserve, ...values } = circuit; return { ...values, isReserve: Boolean(isReserve), deviceRows: database .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.circuitId, circuit.id)) .orderBy( asc(circuitDeviceRows.sortOrder), asc(circuitDeviceRows.id) ) .all(), protectionDevice: database .select() .from(circuitProtectionDevices) .where(eq(circuitProtectionDevices.circuitId, circuit.id)) .get() ?? null, }; }); const snapshot: DistributionBoardSubtreeSnapshot = { distributionBoard, circuitList, sections, components, circuits: circuitSnapshots, }; assertDistributionBoardSubtreeSnapshot(snapshot); return snapshot; } private insert( database: AppDatabase, projectId: string, snapshot: DistributionBoardSubtreeSnapshot ) { assertDistributionBoardSubtreeSnapshot(snapshot); if ( snapshot.distributionBoard.projectId !== projectId || snapshot.circuitList.projectId !== projectId ) { throw new Error( "Distribution-board subtree belongs to another project." ); } this.assertProjectReferences(database, projectId, snapshot); this.assertIdsAvailable(database, snapshot); database .insert(distributionBoards) .values(snapshot.distributionBoard) .run(); database.insert(circuitLists).values(snapshot.circuitList).run(); if (snapshot.sections.length > 0) { database.insert(circuitSections).values(snapshot.sections).run(); } for (const entry of snapshot.components) { database .insert(distributionBoardComponents) .values(entry.component) .run(); if (entry.protectionDevice !== null) { database .insert(distributionBoardComponentProtectionDevices) .values(entry.protectionDevice) .run(); } } for (const circuit of snapshot.circuits) { const { deviceRows, protectionDevice, isReserve, ...values } = circuit; database .insert(circuits) .values({ ...values, isReserve: isReserve ? 1 : 0 }) .run(); if (protectionDevice !== null && protectionDevice !== undefined) { database .insert(circuitProtectionDevices) .values(protectionDevice) .run(); } if (deviceRows.length > 0) { database.insert(circuitDeviceRows).values(deviceRows).run(); } } } private assertProjectReferences( database: AppDatabase, projectId: string, snapshot: DistributionBoardSubtreeSnapshot ) { const project = database .select({ enabledSupplyTypes: projects.enabledDistributionBoardSupplyTypes, }) .from(projects) .where(eq(projects.id, projectId)) .get(); if (!project) { throw new Error("Project not found."); } if ( snapshot.distributionBoard.supplyType !== null && !project.enabledSupplyTypes.includes( snapshot.distributionBoard.supplyType ) ) { throw new Error( "Distribution-board subtree uses a disabled supply type." ); } if (snapshot.distributionBoard.floorId !== null) { const floor = database .select({ projectId: floors.projectId }) .from(floors) .where(eq(floors.id, snapshot.distributionBoard.floorId)) .get(); if (!floor || floor.projectId !== projectId) { throw new Error( "Distribution-board floor does not belong to project." ); } } const linkedProjectDeviceIds = uniqueStrings( snapshot.circuits.flatMap((circuit) => circuit.deviceRows.flatMap((row) => row.linkedProjectDeviceId === null ? [] : [row.linkedProjectDeviceId] ) ) ); if (linkedProjectDeviceIds.length > 0) { const references = database .select({ id: projectDevices.id, projectId: projectDevices.projectId }) .from(projectDevices) .where(inArray(projectDevices.id, linkedProjectDeviceIds)) .all(); if ( references.length !== linkedProjectDeviceIds.length || references.some((reference) => reference.projectId !== projectId) ) { throw new Error( "Distribution-board project-device reference is invalid." ); } } const roomIds = uniqueStrings( snapshot.circuits.flatMap((circuit) => circuit.deviceRows.flatMap((row) => row.roomId === null ? [] : [row.roomId] ) ) ); if (roomIds.length > 0) { const references = database .select({ id: rooms.id, projectId: rooms.projectId }) .from(rooms) .where(inArray(rooms.id, roomIds)) .all(); if ( references.length !== roomIds.length || references.some((reference) => reference.projectId !== projectId) ) { throw new Error("Distribution-board room reference is invalid."); } } } private assertIdsAvailable( database: AppDatabase, snapshot: DistributionBoardSubtreeSnapshot ) { const sectionIds = snapshot.sections.map((section) => section.id); const componentIds = snapshot.components.map( ({ component }) => component.id ); const circuitIds = snapshot.circuits.map((circuit) => circuit.id); const deviceRowIds = snapshot.circuits.flatMap((circuit) => circuit.deviceRows.map((row) => row.id) ); const conflicts = [ database .select({ id: distributionBoards.id }) .from(distributionBoards) .where(eq(distributionBoards.id, snapshot.distributionBoard.id)) .get(), database .select({ id: circuitLists.id }) .from(circuitLists) .where(eq(circuitLists.id, snapshot.circuitList.id)) .get(), sectionIds.length === 0 ? undefined : database .select({ id: circuitSections.id }) .from(circuitSections) .where(inArray(circuitSections.id, sectionIds)) .limit(1) .get(), componentIds.length === 0 ? undefined : database .select({ id: distributionBoardComponents.id }) .from(distributionBoardComponents) .where(inArray(distributionBoardComponents.id, componentIds)) .limit(1) .get(), circuitIds.length === 0 ? undefined : database .select({ id: circuits.id }) .from(circuits) .where(inArray(circuits.id, circuitIds)) .limit(1) .get(), deviceRowIds.length === 0 ? undefined : database .select({ id: circuitDeviceRows.id }) .from(circuitDeviceRows) .where(inArray(circuitDeviceRows.id, deviceRowIds)) .limit(1) .get(), ]; if (conflicts.some(Boolean)) { throw new Error("Distribution-board subtree id already exists."); } } } function uniqueStrings(values: string[]) { return [...new Set(values)]; } function sameSnapshot( left: DistributionBoardSubtreeSnapshot, right: DistributionBoardSubtreeSnapshot ) { return canonicalJson(left) === canonicalJson(right); } function canonicalJson(value: unknown): string { if (Array.isArray(value)) { return `[${value.map(canonicalJson).join(",")}]`; } if (value !== null && typeof value === "object") { return `{${Object.entries(value) .sort(([left], [right]) => left.localeCompare(right)) .map( ([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}` ) .join(",")}}`; } return JSON.stringify(value) ?? "null"; }