import { and, eq } from "drizzle-orm"; import { assertProjectFloorDeleteProjectCommand, assertProjectFloorInsertProjectCommand, assertProjectRoomDeleteProjectCommand, assertProjectRoomInsertProjectCommand, createProjectFloorDeleteProjectCommand, createProjectFloorInsertProjectCommand, createProjectRoomDeleteProjectCommand, createProjectRoomInsertProjectCommand, projectFloorDeleteCommandType, projectFloorInsertCommandType, projectRoomDeleteCommandType, projectRoomInsertCommandType, 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 { consumers } from "../schema/consumers.js"; import { floors } from "../schema/floors.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; import { applyProjectHistoryTransition } from "./project-history.persistence.js"; import { appendProjectRevision } from "./project-revision.persistence.js"; export class ProjectLocationStructureProjectCommandRepository implements ProjectLocationStructureProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteProjectLocationStructureCommandInput) { return this.database.transaction((tx) => { const inverse = this.applyCommand( tx, input.projectId, input.command ); const revision = appendProjectRevision(tx, { projectId: input.projectId, expectedRevision: input.expectedRevision, source: input.source, description: input.description, actorId: input.actorId, forward: input.command, inverse, }); applyProjectHistoryTransition(tx, { projectId: input.projectId, source: input.source, recordedChangeSetId: revision.changeSetId, targetChangeSetId: input.historyTargetChangeSetId, }); return { revision, inverse }; }); } 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 projectRoomInsertCommandType: assertProjectRoomInsertProjectCommand(command); return this.insertRoom( database, projectId, command.payload.room ); case projectRoomDeleteCommandType: assertProjectRoomDeleteProjectCommand(command); return this.deleteRoom( database, projectId, command.payload.room ); } } 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( "A project floor with assigned rooms cannot be removed by history." ); } 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 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(); const referencedLegacyConsumer = database .select({ id: consumers.id }) .from(consumers) .where(eq(consumers.roomId, expected.id)) .limit(1) .get(); if (referencedDeviceRow || referencedLegacyConsumer) { throw new Error( "A referenced project room cannot be removed by history." ); } 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) ); }