Make device row writes atomic

This commit is contained in:
2026-07-23 17:28:42 +02:00
parent eb945a9622
commit 7d276d1139
4 changed files with 288 additions and 113 deletions
@@ -26,6 +26,13 @@ export interface CircuitDeviceRowUpdateInput {
overriddenFields?: string;
}
export interface CircuitDeviceRowCreateInput extends CircuitDeviceRowUpdateInput {
circuitId: string;
linkedProjectDeviceId?: string;
legacyConsumerId?: string;
sortOrder: number;
}
export interface CircuitDeviceRowsBulkMoveInput {
rows: Array<{ id: string; expectedCircuitId: string }>;
targetCircuitId?: string;
@@ -60,6 +67,16 @@ function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
};
}
function toCreateValues(id: string, input: CircuitDeviceRowCreateInput) {
return {
id,
circuitId: input.circuitId,
legacyConsumerId: input.legacyConsumerId ?? null,
sortOrder: input.sortOrder,
...toUpdateValues(input),
};
}
export class CircuitDeviceRowRepository {
async findById(rowId: string) {
const [row] = await db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId)).limit(1);
@@ -141,51 +158,43 @@ export class CircuitDeviceRowRepository {
.where(and(eq(circuitLists.projectId, projectId), inArray(circuitDeviceRows.id, rowIds)));
}
async create(input: {
circuitId: string;
linkedProjectDeviceId?: string;
legacyConsumerId?: string;
sortOrder: number;
name: string;
displayName: string;
phaseType?: string;
connectionKind?: string;
costGroup?: string;
category?: string;
level?: string;
roomId?: string;
roomNumberSnapshot?: string;
roomNameSnapshot?: string;
quantity: number;
powerPerUnit: number;
simultaneityFactor: number;
cosPhi?: number;
remark?: string;
overriddenFields?: string;
}) {
async create(input: CircuitDeviceRowCreateInput) {
const id = crypto.randomUUID();
await db.insert(circuitDeviceRows).values({
id,
circuitId: input.circuitId,
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
legacyConsumerId: input.legacyConsumerId ?? null,
sortOrder: input.sortOrder,
name: input.name,
displayName: input.displayName,
phaseType: input.phaseType ?? null,
connectionKind: input.connectionKind ?? null,
costGroup: input.costGroup ?? null,
category: input.category ?? null,
level: input.level ?? null,
roomId: input.roomId ?? null,
roomNumberSnapshot: input.roomNumberSnapshot ?? null,
roomNameSnapshot: input.roomNameSnapshot ?? null,
quantity: input.quantity,
powerPerUnit: input.powerPerUnit,
simultaneityFactor: input.simultaneityFactor,
cosPhi: input.cosPhi ?? null,
remark: input.remark ?? null,
overriddenFields: input.overriddenFields ?? null,
await db.insert(circuitDeviceRows).values(toCreateValues(id, input));
return id;
}
createInCircuitTransactional(
input: Omit<CircuitDeviceRowCreateInput, "sortOrder"> & { sortOrder?: number }
) {
const id = crypto.randomUUID();
db.transaction((tx) => {
const [circuit] = tx
.select({ id: circuits.id })
.from(circuits)
.where(eq(circuits.id, input.circuitId))
.limit(1)
.all();
if (!circuit) {
throw new Error("Der Stromkreis ist ungültig.");
}
const existingRows = tx
.select({ sortOrder: circuitDeviceRows.sortOrder })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, input.circuitId))
.all();
const lastSortOrder = existingRows.reduce(
(highest, row) => Math.max(highest, row.sortOrder),
0
);
const sortOrder = input.sortOrder ?? lastSortOrder + 10;
tx
.insert(circuitDeviceRows)
.values(toCreateValues(id, { ...input, sortOrder }))
.run();
tx.update(circuits).set({ isReserve: 0 }).where(eq(circuits.id, input.circuitId)).run();
});
return id;
}
@@ -262,6 +271,44 @@ export class CircuitDeviceRowRepository {
await db.delete(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId));
}
deleteFromCircuitTransactional(rowId: string, expectedCircuitId: string) {
db.transaction((tx) => {
const [row] = tx
.select({ id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, rowId))
.limit(1)
.all();
if (!row || row.circuitId !== expectedCircuitId) {
throw new Error("Die Gerätezeile wurde vor dem Löschen verändert.");
}
const result = tx
.delete(circuitDeviceRows)
.where(
and(
eq(circuitDeviceRows.id, rowId),
eq(circuitDeviceRows.circuitId, expectedCircuitId)
)
)
.run();
if (result.changes !== 1) {
throw new Error("Die Gerätezeile konnte nicht gelöscht werden.");
}
const remainingRows = tx
.select({ id: circuitDeviceRows.id })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, expectedCircuitId))
.all();
tx
.update(circuits)
.set({ isReserve: remainingRows.length === 0 ? 1 : 0 })
.where(eq(circuits.id, expectedCircuitId))
.run();
});
}
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
await db
.update(circuitDeviceRows)
+3 -50
View File
@@ -171,11 +171,10 @@ export class CircuitWriteService {
}
await this.assertValidLinkedProjectDevice(circuitId, input.linkedProjectDeviceId);
const existingRows = await this.deviceRowRepository.countByCircuit(circuitId);
const rowId = await this.deviceRowRepository.create({
const rowId = this.deviceRowRepository.createInCircuitTransactional({
circuitId,
linkedProjectDeviceId: input.linkedProjectDeviceId,
sortOrder: input.sortOrder ?? (existingRows + 1) * 10,
sortOrder: input.sortOrder,
name: input.name,
displayName: input.displayName,
phaseType: input.phaseType,
@@ -194,29 +193,6 @@ export class CircuitWriteService {
overriddenFields: input.overriddenFields,
});
// Reserve circuits become active as soon as at least one device row exists.
if (Boolean(circuit.isReserve)) {
await this.circuitRepository.update(circuit.id, {
sectionId: circuit.sectionId,
equipmentIdentifier: circuit.equipmentIdentifier,
displayName: circuit.displayName ?? undefined,
sortOrder: circuit.sortOrder,
protectionType: circuit.protectionType ?? undefined,
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
cableType: circuit.cableType ?? undefined,
cableCrossSection: circuit.cableCrossSection ?? undefined,
cableLength: circuit.cableLength ?? undefined,
rcdAssignment: circuit.rcdAssignment ?? undefined,
terminalDesignation: circuit.terminalDesignation ?? undefined,
voltage: circuit.voltage ?? undefined,
controlRequirement: circuit.controlRequirement ?? undefined,
status: circuit.status ?? undefined,
isReserve: false,
remark: circuit.remark ?? undefined,
});
}
return this.deviceRowRepository.findById(rowId);
}
@@ -272,30 +248,7 @@ export class CircuitWriteService {
if (!circuit) {
throw new Error("Invalid circuit id.");
}
await this.deviceRowRepository.delete(rowId);
const remaining = await this.deviceRowRepository.countByCircuit(current.circuitId);
// When last row is removed, keep circuit and mark it reserve instead of deleting it.
if (remaining === 0) {
await this.circuitRepository.update(circuit.id, {
sectionId: circuit.sectionId,
equipmentIdentifier: circuit.equipmentIdentifier,
displayName: circuit.displayName ?? undefined,
sortOrder: circuit.sortOrder,
protectionType: circuit.protectionType ?? undefined,
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
cableType: circuit.cableType ?? undefined,
cableCrossSection: circuit.cableCrossSection ?? undefined,
cableLength: circuit.cableLength ?? undefined,
rcdAssignment: circuit.rcdAssignment ?? undefined,
terminalDesignation: circuit.terminalDesignation ?? undefined,
voltage: circuit.voltage ?? undefined,
controlRequirement: circuit.controlRequirement ?? undefined,
status: circuit.status ?? undefined,
isReserve: true,
remark: circuit.remark ?? undefined,
});
}
this.deviceRowRepository.deleteFromCircuitTransactional(rowId, circuit.id);
}
async moveDeviceRow(rowId: string, input: MoveCircuitDeviceRowInput) {