import { and, asc, eq, inArray } from "drizzle-orm"; import { assertDistributionBoardDeleteProjectCommand, assertDistributionBoardInsertProjectCommand, distributionBoardDeleteCommandType, distributionBoardInsertCommandType, invertDistributionBoardStructureProjectCommand, normalizeDistributionBoardStructureProjectCommand, type DistributionBoardStructureProjectCommand, type NormalizedDistributionBoardStructureSnapshot, } from "../../domain/models/distribution-board-structure-project-command.model.js"; import { assertDistributionBoardUpdateProjectCommand, createDistributionBoardUpdateProjectCommand, legacyDistributionBoardUpdateCommandSchemaVersion, type DistributionBoardUpdateField, type DistributionBoardUpdatePatch, type DistributionBoardUpdateProjectCommand, type DistributionBoardUpdateValues, } from "../../domain/models/distribution-board-project-command.model.js"; import type { DistributionBoardStructureProjectCommandStore, ExecuteDistributionBoardStructureCommandInput, } from "../../domain/ports/distribution-board-structure-project-command.store.js"; import type { AppDatabase } from "../database-context.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { floors } from "../schema/floors.js"; import { projects } from "../schema/projects.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class DistributionBoardStructureProjectCommandRepository implements DistributionBoardStructureProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteDistributionBoardStructureCommandInput) { return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) ); } executeUpdate( input: Omit< ExecuteDistributionBoardStructureCommandInput, "command" > & { command: DistributionBoardUpdateProjectCommand; } ) { assertDistributionBoardUpdateProjectCommand(input.command); return executeProjectCommandTransaction( this.database, input, (tx) => this.update(tx, input) ); } private applyCommand( database: AppDatabase, projectId: string, command: DistributionBoardStructureProjectCommand ): DistributionBoardStructureProjectCommand { const structure = normalizeDistributionBoardStructureProjectCommand(command); if (command.type === distributionBoardInsertCommandType) { this.insert(database, projectId, structure); return invertDistributionBoardStructureProjectCommand(command); } if (command.type === distributionBoardDeleteCommandType) { this.delete(database, projectId, structure); return invertDistributionBoardStructureProjectCommand(command); } throw new Error("Unsupported distribution-board structure command."); } private insert( database: AppDatabase, projectId: string, structure: NormalizedDistributionBoardStructureSnapshot ) { if ( structure.distributionBoard.projectId !== projectId || structure.circuitList.projectId !== projectId ) { throw new Error( "Distribution-board structure does not belong to project." ); } const project = database .select({ id: projects.id, enabledSupplyTypes: projects.enabledDistributionBoardSupplyTypes, }) .from(projects) .where(eq(projects.id, projectId)) .get(); if (!project) { throw new Error("Project not found."); } assertSupplyTypeEnabled( project.enabledSupplyTypes, structure.distributionBoard.supplyType ); if (structure.distributionBoard.floorId !== null) { const floor = database .select({ projectId: floors.projectId }) .from(floors) .where(eq(floors.id, structure.distributionBoard.floorId)) .get(); if (!floor || floor.projectId !== projectId) { throw new Error( "Distribution-board floor does not belong to project." ); } } const existingBoard = database .select({ id: distributionBoards.id }) .from(distributionBoards) .where(eq(distributionBoards.id, structure.distributionBoard.id)) .get(); const existingList = database .select({ id: circuitLists.id }) .from(circuitLists) .where(eq(circuitLists.id, structure.circuitList.id)) .get(); const existingSection = database .select({ id: circuitSections.id }) .from(circuitSections) .where( inArray( circuitSections.id, structure.sections.map((section) => section.id) ) ) .limit(1) .get(); const existingComponent = structure.components.length === 0 ? undefined : database .select({ id: distributionBoardComponents.id }) .from(distributionBoardComponents) .where( inArray( distributionBoardComponents.id, structure.components.map((component) => component.id) ) ) .limit(1) .get(); if ( existingBoard || existingList || existingSection || existingComponent ) { throw new Error("Distribution-board structure id already exists."); } database .insert(distributionBoards) .values(structure.distributionBoard) .run(); database.insert(circuitLists).values(structure.circuitList).run(); database .insert(circuitSections) .values(structure.sections) .run(); if (structure.components.length > 0) { database .insert(distributionBoardComponents) .values(structure.components) .run(); } } private delete( database: AppDatabase, projectId: string, structure: NormalizedDistributionBoardStructureSnapshot ) { const board = database .select() .from(distributionBoards) .where(eq(distributionBoards.id, structure.distributionBoard.id)) .get(); const list = database .select() .from(circuitLists) .where(eq(circuitLists.id, structure.circuitList.id)) .get(); const sections = database .select() .from(circuitSections) .where( eq(circuitSections.circuitListId, structure.circuitList.id) ) .orderBy( asc(circuitSections.sortOrder), asc(circuitSections.id) ) .all(); const components = database .select() .from(distributionBoardComponents) .where( eq( distributionBoardComponents.circuitListId, structure.circuitList.id ) ) .orderBy( asc(distributionBoardComponents.sortOrder), asc(distributionBoardComponents.id) ) .all(); if ( !board || !list || board.projectId !== projectId || !structureMatches(structure, { board, list, sections, components, }) ) { throw new Error( "Distribution-board structure changed before deletion." ); } const populatedCircuit = database .select({ id: circuits.id }) .from(circuits) .where(eq(circuits.circuitListId, structure.circuitList.id)) .limit(1) .get(); if (populatedCircuit) { throw new Error( "A populated distribution board cannot be removed by history." ); } const deleted = database .delete(distributionBoards) .where( and( eq( distributionBoards.id, structure.distributionBoard.id ), eq(distributionBoards.projectId, projectId) ) ) .run(); if (deleted.changes !== 1) { throw new Error("Distribution board could not be deleted."); } } private update( database: AppDatabase, input: Omit< ExecuteDistributionBoardStructureCommandInput, "command" > & { command: DistributionBoardUpdateProjectCommand; } ) { const current = database .select() .from(distributionBoards) .where( and( eq( distributionBoards.id, input.command.payload.distributionBoardId ), eq(distributionBoards.projectId, input.projectId) ) ) .get(); if (!current) { throw new Error("Distribution board does not belong to project."); } const project = database .select({ enabledSupplyTypes: projects.enabledDistributionBoardSupplyTypes, }) .from(projects) .where(eq(projects.id, input.projectId)) .get(); if (!project) { throw new Error("Project not found."); } const patch = Object.fromEntries( input.command.payload.changes.map((change) => [ change.field, change.value, ]) ) as DistributionBoardUpdatePatch; if (patch.floorId !== undefined && patch.floorId !== null) { const floor = database .select({ projectId: floors.projectId }) .from(floors) .where(eq(floors.id, patch.floorId)) .get(); if (!floor || floor.projectId !== input.projectId) { throw new Error( "Distribution-board floor does not belong to project." ); } } if (patch.supplyType !== undefined) { assertSupplyTypeEnabled( project.enabledSupplyTypes, patch.supplyType ); } const inversePatch = Object.fromEntries( input.command.payload.changes.map((change) => [ change.field, getDistributionBoardFieldValue(current, change.field), ]) ) as DistributionBoardUpdatePatch; const currentInverse = createDistributionBoardUpdateProjectCommand( current.id, inversePatch ); const inverse = input.command.schemaVersion === legacyDistributionBoardUpdateCommandSchemaVersion ? ({ ...currentInverse, schemaVersion: legacyDistributionBoardUpdateCommandSchemaVersion, } as DistributionBoardUpdateProjectCommand) : currentInverse; const updated = database .update(distributionBoards) .set(patch) .where( and( eq(distributionBoards.id, current.id), eq(distributionBoards.projectId, input.projectId) ) ) .run(); if (updated.changes !== 1) { throw new Error( "Distribution board changed before command execution." ); } return inverse; } } function assertSupplyTypeEnabled( enabledSupplyTypes: (typeof projects.$inferSelect)["enabledDistributionBoardSupplyTypes"], supplyType: (typeof distributionBoards.$inferSelect)["supplyType"] ) { if ( supplyType !== null && !enabledSupplyTypes.includes(supplyType) ) { throw new Error( `Die Netzart ${supplyType} ist in den Projekteinstellungen nicht aktiviert.` ); } } function getDistributionBoardFieldValue< TField extends DistributionBoardUpdateField, >( distributionBoard: typeof distributionBoards.$inferSelect, field: TField ): DistributionBoardUpdateValues[TField] { return distributionBoard[field] as DistributionBoardUpdateValues[TField]; } function structureMatches( expected: NormalizedDistributionBoardStructureSnapshot, actual: { board: typeof distributionBoards.$inferSelect; list: typeof circuitLists.$inferSelect; sections: Array; components: Array; } ) { const { simultaneityFactor, ...actualDistributionBoard } = actual.board; if ( simultaneityFactor !== 1 || !sameRecord( expected.distributionBoard, actualDistributionBoard ) || !sameRecord(expected.circuitList, actual.list) || expected.sections.length !== actual.sections.length || expected.components.length !== actual.components.length ) { return false; } const expectedSections = [...expected.sections].sort( (left, right) => left.sortOrder - right.sortOrder || left.id.localeCompare(right.id) ); const sectionsMatch = expectedSections.every((section, index) => sameRecord(section, actual.sections[index]) ); const expectedComponents = [...expected.components].sort( (left, right) => left.sortOrder - right.sortOrder || left.id.localeCompare(right.id) ); return ( sectionsMatch && expectedComponents.every((component, index) => sameRecord(component, actual.components[index]) ) ); } function sameRecord( expected: object, actual: object ) { const expectedEntries = Object.entries(expected); const actualRecord = actual as Record; return ( expectedEntries.length === Object.keys(actual).length && expectedEntries.every( ([key, value]) => actualRecord[key] === value ) ); }