Isolate circuit section transactions
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import type { CircuitSectionTransactionStore } from "../../domain/ports/circuit-section-transaction.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
|
||||
export class CircuitSectionTransactionRepository
|
||||
implements CircuitSectionTransactionStore
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
updateSortOrders(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."
|
||||
);
|
||||
}
|
||||
|
||||
this.database.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."
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateEquipmentIdentifiers(
|
||||
circuitListId: string,
|
||||
updates: Array<{ id: string; equipmentIdentifier: string }>,
|
||||
tempNamespace: string
|
||||
) {
|
||||
if (updates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.database.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."
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
import { and, asc, eq, inArray, ne } from "drizzle-orm";
|
||||
import { and, asc, eq, ne } from "drizzle-orm";
|
||||
import { db } from "../client.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import {
|
||||
@@ -169,89 +169,4 @@ export class CircuitRepository {
|
||||
.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface CircuitSectionTransactionStore {
|
||||
updateEquipmentIdentifiers(
|
||||
circuitListId: string,
|
||||
updates: Array<{ id: string; equipmentIdentifier: string }>,
|
||||
tempNamespace: string
|
||||
): void;
|
||||
updateSortOrders(sectionId: string, orderedCircuitIds: string[]): void;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
||||
import type { CircuitDeviceRowTransactionStore } from "../ports/circuit-device-row-transaction.store.js";
|
||||
import type { CircuitSectionTransactionStore } from "../ports/circuit-section-transaction.store.js";
|
||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||
@@ -28,6 +29,7 @@ export class CircuitWriteService {
|
||||
private readonly circuitListRepository: CircuitListRepository;
|
||||
private readonly deviceRowRepository: CircuitDeviceRowRepository;
|
||||
private readonly deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
||||
private readonly circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
||||
private readonly projectDeviceRepository: ProjectDeviceRepository;
|
||||
private readonly numberingService: CircuitNumberingService;
|
||||
|
||||
@@ -37,6 +39,7 @@ export class CircuitWriteService {
|
||||
circuitListRepository?: CircuitListRepository;
|
||||
deviceRowRepository?: CircuitDeviceRowRepository;
|
||||
deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
||||
circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
||||
projectDeviceRepository?: ProjectDeviceRepository;
|
||||
numberingService?: CircuitNumberingService;
|
||||
}) {
|
||||
@@ -45,6 +48,7 @@ export class CircuitWriteService {
|
||||
this.circuitListRepository = deps?.circuitListRepository ?? new CircuitListRepository();
|
||||
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
|
||||
this.deviceRowTransactionStore = deps?.deviceRowTransactionStore;
|
||||
this.circuitSectionTransactionStore = deps?.circuitSectionTransactionStore;
|
||||
this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository();
|
||||
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
|
||||
}
|
||||
@@ -98,6 +102,13 @@ export class CircuitWriteService {
|
||||
return this.deviceRowTransactionStore;
|
||||
}
|
||||
|
||||
private getCircuitSectionTransactionStore() {
|
||||
if (!this.circuitSectionTransactionStore) {
|
||||
throw new Error("Circuit-section transactions are not configured.");
|
||||
}
|
||||
return this.circuitSectionTransactionStore;
|
||||
}
|
||||
|
||||
private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
|
||||
if (!linkedProjectDeviceId) {
|
||||
return;
|
||||
@@ -451,7 +462,7 @@ export class CircuitWriteService {
|
||||
index += 1;
|
||||
}
|
||||
// Uses safe two-phase identifier update to avoid UNIQUE collisions during swaps.
|
||||
await this.circuitRepository.updateEquipmentIdentifiersSafely(
|
||||
this.getCircuitSectionTransactionStore().updateEquipmentIdentifiers(
|
||||
section.circuitListId,
|
||||
finalAssignments,
|
||||
sectionId
|
||||
@@ -479,7 +490,7 @@ export class CircuitWriteService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.circuitRepository.updateEquipmentIdentifiersSafely(
|
||||
this.getCircuitSectionTransactionStore().updateEquipmentIdentifiers(
|
||||
section.circuitListId,
|
||||
input.identifiers.map((entry) => ({ id: entry.circuitId, equipmentIdentifier: entry.equipmentIdentifier })),
|
||||
sectionId
|
||||
@@ -504,7 +515,10 @@ export class CircuitWriteService {
|
||||
}
|
||||
}
|
||||
|
||||
this.circuitRepository.updateSortOrdersSafely(sectionId, input.orderedCircuitIds);
|
||||
this.getCircuitSectionTransactionStore().updateSortOrders(
|
||||
sectionId,
|
||||
input.orderedCircuitIds
|
||||
);
|
||||
return this.circuitRepository.listBySection(sectionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||
import { db } from "../../db/client.js";
|
||||
import { CircuitDeviceRowTransactionRepository } from "../../db/repositories/circuit-device-row-transaction.repository.js";
|
||||
import { CircuitSectionTransactionRepository } from "../../db/repositories/circuit-section-transaction.repository.js";
|
||||
|
||||
export const circuitWriteService = new CircuitWriteService({
|
||||
deviceRowTransactionStore: new CircuitDeviceRowTransactionRepository(db),
|
||||
circuitSectionTransactionStore: new CircuitSectionTransactionRepository(db),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user