import { and, eq } from "drizzle-orm"; import { assertCircuitGroupRenumberProjectCommand, createCircuitGroupRenumberProjectCommand, type CircuitGroupRenumberProjectCommand, } from "../../domain/models/circuit-group-renumber-project-command.model.js"; import type { CircuitGroupRenumberProjectCommandStore } from "../../domain/ports/circuit-group-renumber-project-command.store.js"; import type { ExecuteCircuitGroupRenumberCommandInput } from "../../domain/ports/circuit-group-renumber-project-command.store.js"; import type { CircuitGroupRenumberPlan } from "../../domain/services/circuit-group-renumbering.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 CircuitGroupRenumberProjectCommandRepository implements CircuitGroupRenumberProjectCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteCircuitGroupRenumberCommandInput) { assertCircuitGroupRenumberProjectCommand(input.command); return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) ); } private applyCommand( database: AppDatabase, projectId: string, command: CircuitGroupRenumberProjectCommand ) { const { circuitListId, groups } = command.payload; this.assertCurrentState(database, projectId, circuitListId, groups); const inverse = createCircuitGroupRenumberProjectCommand( circuitListId, invertPlan(groups) ); const occupiedIdentifiers = new Set( [ ...database .select({ value: circuits.equipmentIdentifier }) .from(circuits) .where(eq(circuits.circuitListId, circuitListId)) .all(), ...database .select({ value: distributionBoardComponents.equipmentIdentifier }) .from(distributionBoardComponents) .where( eq(distributionBoardComponents.circuitListId, circuitListId) ) .all(), ].map(({ value }) => normalizeIdentifier(value)) ); const temporaryIdentifiers = new Map(); for (const group of groups) { for (const assignment of [ ...group.circuits.map((entry) => ({ id: entry.circuitId, expected: entry.expectedEquipmentIdentifier, table: "circuit" as const, })), ...group.components.map((entry) => ({ id: entry.componentId, expected: entry.expectedEquipmentIdentifier, table: "component" as const, })), ]) { const key = `${assignment.table}:${assignment.id}`; const temporary = createTemporaryIdentifier( key, occupiedIdentifiers ); temporaryIdentifiers.set(key, temporary); this.updateEntityIdentifier( database, assignment.table, assignment.id, circuitListId, assignment.expected, temporary ); } } const occupiedPrefixes = new Set( database .select({ prefix: circuitSections.prefix }) .from(circuitSections) .where(eq(circuitSections.circuitListId, circuitListId)) .all() .map(({ prefix }) => prefix) ); const temporaryPrefixes = new Map(); for (const group of groups) { const temporaryPrefix = createTemporaryValue( `__tmp_group_prefix_${group.groupId}`, occupiedPrefixes ); temporaryPrefixes.set(group.groupId, temporaryPrefix); const updated = database .update(circuitSections) .set({ groupNumber: null, prefix: temporaryPrefix }) .where( and( eq(circuitSections.id, group.groupId), eq(circuitSections.circuitListId, circuitListId), eq(circuitSections.groupNumber, group.expectedGroupNumber), eq(circuitSections.prefix, group.expectedPrefix) ) ) .run(); if (updated.changes !== 1) { throw new Error("Circuit group changed during renumbering."); } } for (const group of groups) { const updated = database .update(circuitSections) .set({ groupNumber: group.targetGroupNumber, prefix: group.targetPrefix, }) .where( and( eq(circuitSections.id, group.groupId), eq(circuitSections.circuitListId, circuitListId), eq(circuitSections.prefix, temporaryPrefixes.get(group.groupId)!) ) ) .run(); if (updated.changes !== 1) { throw new Error("Circuit group changed during final renumbering."); } } for (const group of groups) { for (const assignment of group.circuits) { this.updateEntityIdentifier( database, "circuit", assignment.circuitId, circuitListId, temporaryIdentifiers.get(`circuit:${assignment.circuitId}`)!, assignment.targetEquipmentIdentifier ); } for (const assignment of group.components) { this.updateEntityIdentifier( database, "component", assignment.componentId, circuitListId, temporaryIdentifiers.get(`component:${assignment.componentId}`)!, assignment.targetEquipmentIdentifier ); } } return inverse; } private assertCurrentState( database: AppDatabase, projectId: string, circuitListId: string, groups: CircuitGroupRenumberPlan["groups"] ) { 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 list does not belong to project."); } const selectedIds = new Set(groups.map((group) => group.groupId)); const allGroups = database .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, circuitListId)) .all(); for (const group of groups) { const current = allGroups.find((entry) => entry.id === group.groupId); if ( !current || current.category !== group.category || current.groupNumber !== group.expectedGroupNumber || current.prefix !== group.expectedPrefix ) { throw new Error("Circuit group changed before renumbering."); } const currentCircuits = database .select({ id: circuits.id, equipmentIdentifier: circuits.equipmentIdentifier, }) .from(circuits) .where(eq(circuits.sectionId, group.groupId)) .all(); const currentComponents = database .select({ id: distributionBoardComponents.id, equipmentIdentifier: distributionBoardComponents.equipmentIdentifier, }) .from(distributionBoardComponents) .where(eq(distributionBoardComponents.sectionId, group.groupId)) .all(); if ( !matchesAssignments(currentCircuits, group.circuits, "circuitId") || !matchesAssignments( currentComponents, group.components, "componentId" ) ) { throw new Error( "Circuit-group renumber must include every unchanged child." ); } } for (const group of groups) { if ( allGroups.some( (entry) => !selectedIds.has(entry.id) && entry.category === group.category && entry.groupNumber === group.targetGroupNumber ) ) { throw new Error( "Target group number belongs to an unchanged group." ); } } } private updateEntityIdentifier( database: AppDatabase, table: "circuit" | "component", id: string, circuitListId: string, expected: string, target: string ) { const schema = table === "circuit" ? circuits : distributionBoardComponents; const updated = database .update(schema) .set({ equipmentIdentifier: target }) .where( and( eq(schema.id, id), eq(schema.circuitListId, circuitListId), eq(schema.equipmentIdentifier, expected) ) ) .run(); if (updated.changes !== 1) { throw new Error( "Circuit-group child changed during renumbering." ); } } } function matchesAssignments< TAssignment extends { expectedEquipmentIdentifier: string; }, >( current: Array<{ id: string; equipmentIdentifier: string }>, assignments: TAssignment[], idField: keyof TAssignment ) { if (current.length !== assignments.length) { return false; } const currentById = new Map(current.map((entry) => [entry.id, entry])); return assignments.every((assignment) => { const id = assignment[idField]; return ( typeof id === "string" && currentById.get(id)?.equipmentIdentifier === assignment.expectedEquipmentIdentifier ); }); } function invertPlan( groups: CircuitGroupRenumberPlan["groups"] ): CircuitGroupRenumberPlan { return { groups: groups.map((group) => ({ ...group, expectedGroupNumber: group.targetGroupNumber, targetGroupNumber: group.expectedGroupNumber, expectedPrefix: group.targetPrefix, targetPrefix: group.expectedPrefix, circuits: group.circuits.map((circuit) => ({ ...circuit, expectedEquipmentIdentifier: circuit.targetEquipmentIdentifier, targetEquipmentIdentifier: circuit.expectedEquipmentIdentifier, })), components: group.components.map((component) => ({ ...component, expectedEquipmentIdentifier: component.targetEquipmentIdentifier, targetEquipmentIdentifier: component.expectedEquipmentIdentifier, })), })), }; } function createTemporaryIdentifier( key: string, occupied: Set ) { return createTemporaryValue( `__tmp_group_renumber_${key.replace(":", "_")}`, occupied, normalizeIdentifier ); } function createTemporaryValue( base: string, occupied: Set, normalize: (value: string) => string = (value) => value ) { let candidate = base; while (occupied.has(normalize(candidate))) { candidate += "_"; } occupied.add(normalize(candidate)); return candidate; } function normalizeIdentifier(value: string) { return value.trim().toLocaleLowerCase(); }