import { and, eq } from "drizzle-orm"; import { assertProjectFloorDeleteProjectCommand, assertProjectFloorInsertProjectCommand, assertProjectFloorUpdateProjectCommand, assertProjectRoomDeleteProjectCommand, assertProjectRoomInsertProjectCommand, assertProjectRoomUpdateProjectCommand, createProjectFloorDeleteProjectCommand, createProjectFloorInsertProjectCommand, createProjectFloorUpdateProjectCommand, createProjectRoomDeleteProjectCommand, createProjectRoomInsertProjectCommand, createProjectRoomUpdateProjectCommand, projectFloorDeleteCommandType, projectFloorInsertCommandType, projectFloorUpdateCommandType, projectRoomDeleteCommandType, projectRoomInsertCommandType, projectRoomUpdateCommandType, type ProjectFloorSnapshot, type ProjectLocationStructureProjectCommand, type ProjectRoomSnapshot, } from "../../domain/models/project-location-structure-project-command.model.js"; import type { ExecuteProjectLocationStructureCommandInput, ProjectLocationStructureProjectCommandStore, } from "../../domain/ports/project-location-structure-project-command.store.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { floors } from "../schema/floors.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class ProjectLocationStructureProjectCommandRepository implements ProjectLocationStructureProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteProjectLocationStructureCommandInput) { return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) ); } private applyCommand( database: AppDatabase, projectId: string, command: ProjectLocationStructureProjectCommand ): ProjectLocationStructureProjectCommand { switch (command.type) { case projectFloorInsertCommandType: assertProjectFloorInsertProjectCommand(command); return this.insertFloor( database, projectId, command.payload.floor ); case projectFloorDeleteCommandType: assertProjectFloorDeleteProjectCommand(command); return this.deleteFloor( database, projectId, command.payload.floor ); case projectFloorUpdateCommandType: assertProjectFloorUpdateProjectCommand(command); return this.updateFloor(database, projectId, command.payload.expected, command.payload.target); case projectRoomInsertCommandType: assertProjectRoomInsertProjectCommand(command); return this.insertRoom( database, projectId, command.payload.room ); case projectRoomDeleteCommandType: assertProjectRoomDeleteProjectCommand(command); return this.deleteRoom( database, projectId, command.payload.room ); case projectRoomUpdateCommandType: assertProjectRoomUpdateProjectCommand(command); return this.updateRoom(database, projectId, command.payload.expected, command.payload.target); } } private insertFloor( database: AppDatabase, projectId: string, floor: ProjectFloorSnapshot ) { this.assertProjectExists(database, projectId); if (floor.projectId !== projectId) { throw new Error("Project-floor snapshot does not belong to project."); } const existing = database .select({ id: floors.id }) .from(floors) .where(eq(floors.id, floor.id)) .get(); if (existing) { throw new Error("Project-floor id already exists."); } database.insert(floors).values(floor).run(); return createProjectFloorDeleteProjectCommand(floor); } private deleteFloor( database: AppDatabase, projectId: string, expected: ProjectFloorSnapshot ) { const persisted = database .select() .from(floors) .where( and( eq(floors.id, expected.id), eq(floors.projectId, projectId) ) ) .get(); if (!persisted || !sameRecord(expected, persisted)) { throw new Error("Project floor changed before deletion."); } const referencedRoom = database .select({ id: rooms.id }) .from(rooms) .where(eq(rooms.floorId, expected.id)) .limit(1) .get(); if (referencedRoom) { throw new Error( "Die Etage kann nicht gelöscht werden, solange Räume zugeordnet sind." ); } const referencedDistributionBoard = database .select({ id: distributionBoards.id }) .from(distributionBoards) .where(eq(distributionBoards.floorId, expected.id)) .limit(1) .get(); if (referencedDistributionBoard) { throw new Error( "Die Etage kann nicht gelöscht werden, solange eine Verteilung zugeordnet ist." ); } const deleted = database .delete(floors) .where( and( eq(floors.id, expected.id), eq(floors.projectId, projectId) ) ) .run(); if (deleted.changes !== 1) { throw new Error("Project floor could not be deleted."); } return createProjectFloorInsertProjectCommand(expected); } private insertRoom( database: AppDatabase, projectId: string, room: ProjectRoomSnapshot ) { this.assertProjectExists(database, projectId); if (room.projectId !== projectId) { throw new Error("Project-room snapshot does not belong to project."); } if (room.floorId) { const floor = database .select({ id: floors.id }) .from(floors) .where( and( eq(floors.id, room.floorId), eq(floors.projectId, projectId) ) ) .get(); if (!floor) { throw new Error("Project room references a foreign floor."); } } const existing = database .select({ id: rooms.id }) .from(rooms) .where(eq(rooms.id, room.id)) .get(); if (existing) { throw new Error("Project-room id already exists."); } database.insert(rooms).values(room).run(); return createProjectRoomDeleteProjectCommand(room); } private updateFloor( database: AppDatabase, projectId: string, expected: ProjectFloorSnapshot, target: ProjectFloorSnapshot ) { const persisted = database .select() .from(floors) .where(and(eq(floors.id, expected.id), eq(floors.projectId, projectId))) .get(); if (!persisted || !sameRecord(expected, persisted)) { throw new Error("Project floor changed before update."); } const updated = database .update(floors) .set({ name: target.name }) .where(and(eq(floors.id, expected.id), eq(floors.projectId, projectId))) .run(); if (updated.changes !== 1) { throw new Error("Die Etage konnte nicht aktualisiert werden."); } return createProjectFloorUpdateProjectCommand(target, expected); } private updateRoom( database: AppDatabase, projectId: string, expected: ProjectRoomSnapshot, target: ProjectRoomSnapshot ) { const persisted = database .select() .from(rooms) .where(and(eq(rooms.id, expected.id), eq(rooms.projectId, projectId))) .get(); if (!persisted || !sameRecord(expected, persisted)) { throw new Error("Project room changed before update."); } if (target.floorId) { const targetFloor = database .select({ id: floors.id }) .from(floors) .where(and(eq(floors.id, target.floorId), eq(floors.projectId, projectId))) .get(); if (!targetFloor) { throw new Error("Project room references a foreign floor."); } } const updated = database .update(rooms) .set({ floorId: target.floorId, roomNumber: target.roomNumber, roomName: target.roomName, }) .where(and(eq(rooms.id, expected.id), eq(rooms.projectId, projectId))) .run(); if (updated.changes !== 1) { throw new Error("Der Raum konnte nicht aktualisiert werden."); } return createProjectRoomUpdateProjectCommand(target, expected); } private deleteRoom( database: AppDatabase, projectId: string, expected: ProjectRoomSnapshot ) { const persisted = database .select() .from(rooms) .where( and( eq(rooms.id, expected.id), eq(rooms.projectId, projectId) ) ) .get(); if (!persisted || !sameRecord(expected, persisted)) { throw new Error("Project room changed before deletion."); } const referencedDeviceRow = database .select({ id: circuitDeviceRows.id }) .from(circuitDeviceRows) .where(eq(circuitDeviceRows.roomId, expected.id)) .limit(1) .get(); if (referencedDeviceRow) { throw new Error( "Der Raum kann nicht gelöscht werden, solange er in einem Stromkreis verwendet wird." ); } const deleted = database .delete(rooms) .where( and( eq(rooms.id, expected.id), eq(rooms.projectId, projectId) ) ) .run(); if (deleted.changes !== 1) { throw new Error("Project room could not be deleted."); } return createProjectRoomInsertProjectCommand(expected); } private assertProjectExists( database: AppDatabase, projectId: string ) { const project = database .select({ id: projects.id }) .from(projects) .where(eq(projects.id, projectId)) .get(); if (!project) { throw new Error("Project does not exist."); } } } 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) ); }