258 lines
9.0 KiB
TypeScript
258 lines
9.0 KiB
TypeScript
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<typeof circuits.$inferInsert> = {};
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
}
|