import { and, eq } from "drizzle-orm"; import { assertCircuitGroupDeleteProjectCommand, assertCircuitGroupInsertProjectCommand, assertCircuitGroupReorderProjectCommand, assertCircuitGroupUpdateProjectCommand, circuitGroupDeleteCommandType, circuitGroupInsertCommandType, circuitGroupReorderCommandType, circuitGroupUpdateCommandType, createCircuitGroupDeleteProjectCommand, createCircuitGroupInsertProjectCommand, createCircuitGroupReorderProjectCommand, createCircuitGroupUpdateProjectCommand, type CircuitGroupReorderAssignment, type CircuitGroupSnapshot, type CircuitGroupStructureProjectCommand, } from "../../domain/models/circuit-group-structure-project-command.model.js"; import type { CircuitGroupStructureProjectCommandStore, ExecuteCircuitGroupStructureCommandInput, } from "../../domain/ports/circuit-group-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 { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class CircuitGroupStructureProjectCommandRepository implements CircuitGroupStructureProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteCircuitGroupStructureCommandInput) { return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) ); } private applyCommand( database: AppDatabase, projectId: string, command: CircuitGroupStructureProjectCommand ): CircuitGroupStructureProjectCommand { if (command.type === circuitGroupInsertCommandType) { assertCircuitGroupInsertProjectCommand(command); this.insert(database, projectId, command.payload.snapshot); return createCircuitGroupDeleteProjectCommand( command.payload.snapshot ); } if (command.type === circuitGroupDeleteCommandType) { assertCircuitGroupDeleteProjectCommand(command); this.delete(database, projectId, command.payload.snapshot); return createCircuitGroupInsertProjectCommand( command.payload.snapshot ); } if (command.type === circuitGroupUpdateCommandType) { assertCircuitGroupUpdateProjectCommand(command); this.update( database, projectId, command.payload.expected, command.payload.target ); return createCircuitGroupUpdateProjectCommand( command.payload.target, command.payload.expected ); } if (command.type === circuitGroupReorderCommandType) { assertCircuitGroupReorderProjectCommand(command); const inverseAssignments = this.reorder( database, projectId, command.payload.circuitListId, command.payload.assignments ); return createCircuitGroupReorderProjectCommand( command.payload.circuitListId, inverseAssignments ); } throw new Error("Unsupported circuit-group structure command."); } private insert( database: AppDatabase, projectId: string, snapshot: CircuitGroupSnapshot ) { this.assertListOwnership(database, projectId, snapshot.circuitListId); const existing = database .select({ id: circuitSections.id }) .from(circuitSections) .where(eq(circuitSections.id, snapshot.id)) .get(); if (existing) { throw new Error("Circuit-group id already exists."); } database.insert(circuitSections).values(snapshot).run(); } private delete( database: AppDatabase, projectId: string, snapshot: CircuitGroupSnapshot ) { this.assertListOwnership(database, projectId, snapshot.circuitListId); const current = this.getCurrent(database, snapshot.id); if (!current || !sameRecord(snapshot, current)) { throw new Error("Circuit group changed before deletion."); } const circuit = database .select({ id: circuits.id }) .from(circuits) .where(eq(circuits.sectionId, snapshot.id)) .get(); const component = database .select({ id: distributionBoardComponents.id }) .from(distributionBoardComponents) .where(eq(distributionBoardComponents.sectionId, snapshot.id)) .get(); if (circuit || component) { throw new Error( "Only empty circuit groups can be deleted by this command." ); } const deleted = database .delete(circuitSections) .where( and( eq(circuitSections.id, snapshot.id), eq(circuitSections.circuitListId, snapshot.circuitListId) ) ) .run(); if (deleted.changes !== 1) { throw new Error("Circuit group could not be deleted."); } } private update( database: AppDatabase, projectId: string, expected: CircuitGroupSnapshot, target: CircuitGroupSnapshot ) { this.assertListOwnership(database, projectId, expected.circuitListId); const current = this.getCurrent(database, expected.id); if (!current || !sameRecord(expected, current)) { throw new Error("Circuit group changed before update."); } const updated = database .update(circuitSections) .set({ displayName: target.displayName }) .where( and( eq(circuitSections.id, expected.id), eq(circuitSections.circuitListId, expected.circuitListId) ) ) .run(); if (updated.changes !== 1) { throw new Error("Circuit group could not be updated."); } } private assertListOwnership( database: AppDatabase, projectId: string, circuitListId: string ) { const list = database .select({ projectId: circuitLists.projectId }) .from(circuitLists) .where(eq(circuitLists.id, circuitListId)) .get(); if (!list || list.projectId !== projectId) { throw new Error("Circuit group does not belong to project."); } } private reorder( database: AppDatabase, projectId: string, circuitListId: string, assignments: CircuitGroupReorderAssignment[] ) { this.assertListOwnership(database, projectId, circuitListId); const persisted = database .select({ id: circuitSections.id, sortOrder: circuitSections.sortOrder, }) .from(circuitSections) .where(eq(circuitSections.circuitListId, circuitListId)) .all(); const persistedById = new Map( persisted.map((group) => [group.id, group]) ); if ( persisted.length !== assignments.length || assignments.some((assignment) => { const group = persistedById.get(assignment.groupId); return !group || group.sortOrder !== assignment.expectedSortOrder; }) ) { throw new Error( "Circuit-group reorder must include every unchanged group in the list." ); } for (const assignment of assignments) { if (assignment.expectedSortOrder === assignment.targetSortOrder) { continue; } const updated = database .update(circuitSections) .set({ sortOrder: assignment.targetSortOrder }) .where( and( eq(circuitSections.id, assignment.groupId), eq(circuitSections.circuitListId, circuitListId), eq(circuitSections.sortOrder, assignment.expectedSortOrder) ) ) .run(); if (updated.changes !== 1) { throw new Error("Circuit group changed during reorder."); } } return assignments.map((assignment) => ({ groupId: assignment.groupId, expectedSortOrder: assignment.targetSortOrder, targetSortOrder: assignment.expectedSortOrder, })); } private getCurrent(database: AppDatabase, id: string) { return database .select() .from(circuitSections) .where(eq(circuitSections.id, id)) .get(); } } 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 ) ); }