import crypto from "node:crypto"; import { and, asc, eq, inArray, ne } from "drizzle-orm"; import { db } from "../client.js"; import { circuits } from "../schema/circuits.js"; import { toCircuitCreateValues, type CircuitCreatePersistenceInput, } from "./circuit.persistence.js"; export type { CircuitCreatePersistenceInput } from "./circuit.persistence.js"; export { toCircuitCreateValues } from "./circuit.persistence.js"; export interface CircuitPatchPersistenceInput { sectionId?: string; equipmentIdentifier?: string; displayName?: string; sortOrder?: number; protectionType?: string; protectionRatedCurrent?: number; protectionCharacteristic?: string; cableType?: string; cableCrossSection?: string; cableLength?: number; voltage?: number; controlRequirement?: string; remark?: string; rcdAssignment?: string; terminalDesignation?: string; status?: string; isReserve?: boolean; } function toCircuitPatchValues(input: CircuitPatchPersistenceInput) { const values: Partial = {}; const has = (field: keyof CircuitPatchPersistenceInput) => Object.prototype.hasOwnProperty.call(input, field); if (input.sectionId !== undefined) values.sectionId = input.sectionId; if (input.equipmentIdentifier !== undefined) { values.equipmentIdentifier = input.equipmentIdentifier; } if (has("displayName")) values.displayName = input.displayName ?? null; if (input.sortOrder !== undefined) values.sortOrder = input.sortOrder; if (has("protectionType")) values.protectionType = input.protectionType ?? null; if (has("protectionRatedCurrent")) { values.protectionRatedCurrent = input.protectionRatedCurrent ?? null; } if (has("protectionCharacteristic")) { values.protectionCharacteristic = input.protectionCharacteristic ?? null; } if (has("cableType")) values.cableType = input.cableType ?? null; if (has("cableCrossSection")) values.cableCrossSection = input.cableCrossSection ?? null; if (has("cableLength")) values.cableLength = input.cableLength ?? null; if (has("rcdAssignment")) values.rcdAssignment = input.rcdAssignment ?? null; if (has("terminalDesignation")) { values.terminalDesignation = input.terminalDesignation ?? null; } if (has("voltage")) values.voltage = input.voltage ?? null; if (has("controlRequirement")) { values.controlRequirement = input.controlRequirement ?? null; } if (has("status")) values.status = input.status ?? null; if (input.isReserve !== undefined) values.isReserve = input.isReserve ? 1 : 0; if (has("remark")) values.remark = input.remark ?? null; return values; } export class CircuitRepository { async findById(circuitId: string) { const [row] = await db.select().from(circuits).where(eq(circuits.id, circuitId)).limit(1); return row ?? null; } async listByCircuitList(circuitListId: string) { return db .select() .from(circuits) .where(eq(circuits.circuitListId, circuitListId)) .orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier)); } async create(input: CircuitCreatePersistenceInput) { const id = crypto.randomUUID(); await db.insert(circuits).values(toCircuitCreateValues(id, input)); return id; } async update( circuitId: string, input: { sectionId: string; equipmentIdentifier: string; displayName?: string; sortOrder: number; protectionType?: string; protectionRatedCurrent?: number; protectionCharacteristic?: string; cableType?: string; cableCrossSection?: string; cableLength?: number; rcdAssignment?: string; terminalDesignation?: string; voltage?: number; controlRequirement?: string; status?: string; isReserve: boolean; remark?: string; } ) { await db .update(circuits) .set({ sectionId: input.sectionId, equipmentIdentifier: input.equipmentIdentifier, displayName: input.displayName ?? null, sortOrder: input.sortOrder, protectionType: input.protectionType ?? null, protectionRatedCurrent: input.protectionRatedCurrent ?? null, protectionCharacteristic: input.protectionCharacteristic ?? null, cableType: input.cableType ?? null, cableCrossSection: input.cableCrossSection ?? null, cableLength: input.cableLength ?? null, rcdAssignment: input.rcdAssignment ?? null, terminalDesignation: input.terminalDesignation ?? null, voltage: input.voltage ?? null, controlRequirement: input.controlRequirement ?? null, status: input.status ?? null, isReserve: input.isReserve ? 1 : 0, remark: input.remark ?? null, }) .where(eq(circuits.id, circuitId)); } async updateFields(circuitId: string, input: CircuitPatchPersistenceInput) { const values = toCircuitPatchValues(input); if (Object.keys(values).length === 0) { return; } await db.update(circuits).set(values).where(eq(circuits.id, circuitId)); } async delete(circuitId: string) { await db.delete(circuits).where(eq(circuits.id, circuitId)); } async existsByEquipmentIdentifier(circuitListId: string, equipmentIdentifier: string, excludeCircuitId?: string) { const rows = await db .select({ id: circuits.id }) .from(circuits) .where( excludeCircuitId ? and( eq(circuits.circuitListId, circuitListId), eq(circuits.equipmentIdentifier, equipmentIdentifier), ne(circuits.id, excludeCircuitId) ) : and(eq(circuits.circuitListId, circuitListId), eq(circuits.equipmentIdentifier, equipmentIdentifier)) ) .limit(1); return Boolean(rows.length); } async listBySection(sectionId: string) { return db .select() .from(circuits) .where(eq(circuits.sectionId, sectionId)) .orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier)); } updateSortOrdersSafely(sectionId: string, orderedCircuitIds: string[]) { if (orderedCircuitIds.length === 0) { return; } if (new Set(orderedCircuitIds).size !== orderedCircuitIds.length) { throw new Error("Stromkreise dürfen in der Reihenfolge nicht mehrfach vorkommen."); } db.transaction((tx) => { const sectionCircuits = tx .select({ id: circuits.id }) .from(circuits) .where(eq(circuits.sectionId, sectionId)) .all(); const sectionCircuitIds = new Set(sectionCircuits.map((circuit) => circuit.id)); if ( sectionCircuits.length !== orderedCircuitIds.length || orderedCircuitIds.some((circuitId) => !sectionCircuitIds.has(circuitId)) ) { throw new Error("Die Reihenfolge muss alle Stromkreise des Bereichs enthalten."); } for (let index = 0; index < orderedCircuitIds.length; index += 1) { const result = tx .update(circuits) .set({ sortOrder: (index + 1) * 10 }) .where( and( eq(circuits.sectionId, sectionId), eq(circuits.id, orderedCircuitIds[index]) ) ) .run(); if (result.changes !== 1) { throw new Error("Ein Stromkreis wurde vor Abschluss der Sortierung verändert."); } } }); } async updateEquipmentIdentifiersSafely( circuitListId: string, updates: Array<{ id: string; equipmentIdentifier: string }>, tempNamespace: string ) { if (updates.length === 0) { return; } // better-sqlite3 transactions are synchronous callbacks. Do not make this callback // async or return a Promise, otherwise statements may run outside the transaction scope. db.transaction((tx) => { const ids = updates.map((entry) => entry.id); const existing = tx .select({ id: circuits.id }) .from(circuits) .where(and(eq(circuits.circuitListId, circuitListId), inArray(circuits.id, ids))) .all(); if (existing.length !== ids.length) { throw new Error("One or more circuit ids are invalid for circuit list."); } // Direct identifier swaps can violate UNIQUE(circuit_list_id, equipment_identifier) // mid-update (for example A->B while B->A). Two-phase strategy prevents that: // 1) assign unique temporary identifiers for all affected circuits // 2) assign final user-visible identifiers const stamp = Date.now(); for (let index = 0; index < updates.length; index += 1) { const entry = updates[index]; const tempIdentifier = `__tmp_renumber_${tempNamespace}_${stamp}_${index}`; tx .update(circuits) .set({ equipmentIdentifier: tempIdentifier }) .where(and(eq(circuits.circuitListId, circuitListId), eq(circuits.id, entry.id))) .run(); } for (const entry of updates) { tx .update(circuits) .set({ equipmentIdentifier: entry.equipmentIdentifier }) .where(and(eq(circuits.circuitListId, circuitListId), eq(circuits.id, entry.id))) .run(); } }); } }