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"; 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: { circuitListId: string; sectionId: string; equipmentIdentifier: string; displayName?: string; sortOrder: number; protectionType?: string; protectionRatedCurrent?: number; protectionCharacteristic?: string; cableType?: string; cableCrossSection?: string; cableLength?: number; voltage?: number; remark?: string; rcdAssignment?: string; terminalDesignation?: string; status?: string; isReserve?: boolean; }) { const id = crypto.randomUUID(); await db.insert(circuits).values({ id, circuitListId: input.circuitListId, 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, status: input.status ?? null, isReserve: input.isReserve ? 1 : 0, remark: input.remark ?? null, }); 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; 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, status: input.status ?? null, isReserve: input.isReserve ? 1 : 0, remark: input.remark ?? null, }) .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)); } 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(); } }); } }