Make bulk row moves atomic
This commit is contained in:
@@ -26,6 +26,18 @@ export interface CircuitDeviceRowUpdateInput {
|
||||
overriddenFields?: string;
|
||||
}
|
||||
|
||||
export interface CircuitDeviceRowsBulkMoveInput {
|
||||
rows: Array<{ id: string; expectedCircuitId: string }>;
|
||||
targetCircuitId?: string;
|
||||
createTargetCircuit?: {
|
||||
circuitListId: string;
|
||||
sectionId: string;
|
||||
equipmentIdentifier: string;
|
||||
displayName: string;
|
||||
sortOrder: number;
|
||||
};
|
||||
}
|
||||
|
||||
function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
|
||||
return {
|
||||
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
|
||||
@@ -259,4 +271,138 @@ export class CircuitDeviceRowRepository {
|
||||
})
|
||||
.where(eq(circuitDeviceRows.id, rowId));
|
||||
}
|
||||
|
||||
moveRowsTransactional(input: CircuitDeviceRowsBulkMoveInput) {
|
||||
if (input.rows.length === 0) {
|
||||
throw new Error("Keine Gerätezeilen zum Verschieben angegeben.");
|
||||
}
|
||||
if (Boolean(input.targetCircuitId) === Boolean(input.createTargetCircuit)) {
|
||||
throw new Error("Für die Mehrfachverschiebung muss genau ein Ziel angegeben werden.");
|
||||
}
|
||||
|
||||
const rowIds = input.rows.map((row) => row.id);
|
||||
if (new Set(rowIds).size !== rowIds.length) {
|
||||
throw new Error("Gerätezeilen dürfen nicht mehrfach ausgewählt sein.");
|
||||
}
|
||||
|
||||
const createdCircuitId = input.createTargetCircuit ? crypto.randomUUID() : undefined;
|
||||
const targetCircuitId = input.targetCircuitId ?? createdCircuitId!;
|
||||
|
||||
// better-sqlite3 transactions must remain synchronous. Every row assignment,
|
||||
// reserve-state update and optional target creation is committed or rolled back
|
||||
// as one operation.
|
||||
db.transaction((tx) => {
|
||||
const persistedRows = tx
|
||||
.select({ id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId })
|
||||
.from(circuitDeviceRows)
|
||||
.where(inArray(circuitDeviceRows.id, rowIds))
|
||||
.all();
|
||||
if (persistedRows.length !== input.rows.length) {
|
||||
throw new Error("Eine oder mehrere Gerätezeilen sind ungültig.");
|
||||
}
|
||||
|
||||
const persistedRowsById = new Map(persistedRows.map((row) => [row.id, row]));
|
||||
for (const expectedRow of input.rows) {
|
||||
if (persistedRowsById.get(expectedRow.id)?.circuitId !== expectedRow.expectedCircuitId) {
|
||||
throw new Error("Eine Gerätezeile wurde vor Abschluss der Verschiebung verändert.");
|
||||
}
|
||||
}
|
||||
|
||||
let targetCircuit: { id: string; circuitListId: string };
|
||||
if (input.createTargetCircuit) {
|
||||
tx
|
||||
.insert(circuits)
|
||||
.values({
|
||||
id: targetCircuitId,
|
||||
circuitListId: input.createTargetCircuit.circuitListId,
|
||||
sectionId: input.createTargetCircuit.sectionId,
|
||||
equipmentIdentifier: input.createTargetCircuit.equipmentIdentifier,
|
||||
displayName: input.createTargetCircuit.displayName,
|
||||
sortOrder: input.createTargetCircuit.sortOrder,
|
||||
isReserve: 0,
|
||||
})
|
||||
.run();
|
||||
targetCircuit = {
|
||||
id: targetCircuitId,
|
||||
circuitListId: input.createTargetCircuit.circuitListId,
|
||||
};
|
||||
} else {
|
||||
const [persistedTargetCircuit] = tx
|
||||
.select({ id: circuits.id, circuitListId: circuits.circuitListId })
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, targetCircuitId))
|
||||
.limit(1)
|
||||
.all();
|
||||
if (!persistedTargetCircuit) {
|
||||
throw new Error("Der Zielstromkreis ist ungültig.");
|
||||
}
|
||||
targetCircuit = persistedTargetCircuit;
|
||||
}
|
||||
|
||||
const sourceCircuitIds = [...new Set(input.rows.map((row) => row.expectedCircuitId))];
|
||||
const sourceCircuits = tx
|
||||
.select({ id: circuits.id, circuitListId: circuits.circuitListId })
|
||||
.from(circuits)
|
||||
.where(inArray(circuits.id, sourceCircuitIds))
|
||||
.all();
|
||||
if (sourceCircuits.length !== sourceCircuitIds.length) {
|
||||
throw new Error("Einer oder mehrere Quellstromkreise sind ungültig.");
|
||||
}
|
||||
if (sourceCircuits.some((circuit) => circuit.circuitListId !== targetCircuit.circuitListId)) {
|
||||
throw new Error("Alle verschobenen Zeilen müssen zur Stromkreisliste des Ziels gehören.");
|
||||
}
|
||||
|
||||
const movedRowIdSet = new Set(rowIds);
|
||||
const retainedTargetRows = tx
|
||||
.select({ id: circuitDeviceRows.id, sortOrder: circuitDeviceRows.sortOrder })
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.circuitId, targetCircuitId))
|
||||
.all()
|
||||
.filter((row) => !movedRowIdSet.has(row.id));
|
||||
const lastTargetSortOrder = retainedTargetRows.reduce(
|
||||
(highest, row) => Math.max(highest, row.sortOrder),
|
||||
0
|
||||
);
|
||||
|
||||
for (let index = 0; index < input.rows.length; index += 1) {
|
||||
const row = input.rows[index];
|
||||
const result = tx
|
||||
.update(circuitDeviceRows)
|
||||
.set({
|
||||
circuitId: targetCircuitId,
|
||||
sortOrder: lastTargetSortOrder + (index + 1) * 10,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(circuitDeviceRows.id, row.id),
|
||||
eq(circuitDeviceRows.circuitId, row.expectedCircuitId)
|
||||
)
|
||||
)
|
||||
.run();
|
||||
if (result.changes !== 1) {
|
||||
throw new Error("Eine Gerätezeile wurde vor Abschluss der Verschiebung verändert.");
|
||||
}
|
||||
}
|
||||
|
||||
for (const sourceCircuitId of sourceCircuitIds) {
|
||||
const remainingRows = tx
|
||||
.select({ id: circuitDeviceRows.id })
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.circuitId, sourceCircuitId))
|
||||
.all();
|
||||
tx
|
||||
.update(circuits)
|
||||
.set({ isReserve: remainingRows.length === 0 ? 1 : 0 })
|
||||
.where(eq(circuits.id, sourceCircuitId))
|
||||
.run();
|
||||
}
|
||||
tx.update(circuits).set({ isReserve: 0 }).where(eq(circuits.id, targetCircuitId)).run();
|
||||
});
|
||||
|
||||
return {
|
||||
movedRowIds: rowIds,
|
||||
targetCircuitId,
|
||||
createdCircuitId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user