Make bulk row moves atomic
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
||||
import { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js";
|
||||
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
||||
import { db } from "../src/db/client.js";
|
||||
import { createCircuitSchema } from "../src/shared/validation/circuit.schemas.js";
|
||||
@@ -424,8 +425,13 @@ describe("circuit write service rules", () => {
|
||||
});
|
||||
|
||||
it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => {
|
||||
const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = [];
|
||||
const reserveUpdates: Array<{ id: string; isReserve: boolean }> = [];
|
||||
let transactionalMove:
|
||||
| {
|
||||
rows: Array<{ id: string; expectedCircuitId: string }>;
|
||||
targetCircuitId?: string;
|
||||
createTargetCircuit?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
const service = new CircuitWriteService({
|
||||
deviceRowRepository: {
|
||||
async findById(rowId: string) {
|
||||
@@ -434,14 +440,12 @@ describe("circuit write service rules", () => {
|
||||
}
|
||||
return { id: "r2", circuitId: "c2" } as never;
|
||||
},
|
||||
async countByCircuit(circuitId: string) {
|
||||
if (circuitId === "c3") {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
|
||||
movedCalls.push({ rowId, targetCircuitId, sortOrder });
|
||||
moveRowsTransactional(input: typeof transactionalMove) {
|
||||
transactionalMove = input;
|
||||
return {
|
||||
movedRowIds: input!.rows.map((row) => row.id),
|
||||
targetCircuitId: input!.targetCircuitId!,
|
||||
};
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
@@ -454,39 +458,48 @@ describe("circuit write service rules", () => {
|
||||
}
|
||||
return { id: "c3", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F3", sortOrder: 30, isReserve: 1 } as never;
|
||||
},
|
||||
async update(id: string, payload: { isReserve: boolean }) {
|
||||
reserveUpdates.push({ id, isReserve: payload.isReserve });
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetCircuitId: "c3" });
|
||||
assert.deepEqual(movedCalls, [
|
||||
{ rowId: "r1", targetCircuitId: "c3", sortOrder: 20 },
|
||||
{ rowId: "r2", targetCircuitId: "c3", sortOrder: 30 },
|
||||
]);
|
||||
assert.deepEqual(reserveUpdates, [
|
||||
{ id: "c1", isReserve: true },
|
||||
{ id: "c2", isReserve: true },
|
||||
{ id: "c3", isReserve: false },
|
||||
]);
|
||||
const result = await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetCircuitId: "c3" });
|
||||
assert.deepEqual(transactionalMove, {
|
||||
rows: [
|
||||
{ id: "r1", expectedCircuitId: "c1" },
|
||||
{ id: "r2", expectedCircuitId: "c2" },
|
||||
],
|
||||
targetCircuitId: "c3",
|
||||
createTargetCircuit: undefined,
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
movedRowIds: ["r1", "r2"],
|
||||
targetCircuitId: "c3",
|
||||
});
|
||||
});
|
||||
|
||||
it("moving multiple device rows to placeholder creates exactly one new circuit", async () => {
|
||||
let createCount = 0;
|
||||
let transactionCount = 0;
|
||||
let preparedTarget:
|
||||
| {
|
||||
circuitListId: string;
|
||||
sectionId: string;
|
||||
equipmentIdentifier: string;
|
||||
displayName: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
| undefined;
|
||||
const service = new CircuitWriteService({
|
||||
deviceRowRepository: {
|
||||
async findById(rowId: string) {
|
||||
return { id: rowId, circuitId: rowId === "r1" ? "c1" : "c2" } as never;
|
||||
},
|
||||
async countByCircuit(circuitId: string) {
|
||||
if (circuitId === "c-new") {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
async moveToCircuit() {
|
||||
return;
|
||||
moveRowsTransactional(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) {
|
||||
transactionCount += 1;
|
||||
preparedTarget = input.createTargetCircuit;
|
||||
return {
|
||||
movedRowIds: input.rows.map((row) => row.id),
|
||||
targetCircuitId: "c-new",
|
||||
createdCircuitId: "c-new",
|
||||
};
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
@@ -494,21 +507,11 @@ describe("circuit write service rules", () => {
|
||||
if (circuitId === "c1" || circuitId === "c2") {
|
||||
return { id: circuitId, sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
||||
}
|
||||
if (circuitId === "c-new") {
|
||||
return { id: "c-new", sectionId: "s2", circuitListId: "l1", equipmentIdentifier: "-2F8", sortOrder: 40, isReserve: 0 } as never;
|
||||
}
|
||||
return null as never;
|
||||
},
|
||||
async listBySection() {
|
||||
return [{ sortOrder: 30 }] as never[];
|
||||
},
|
||||
async create() {
|
||||
createCount += 1;
|
||||
return "c-new";
|
||||
},
|
||||
async update() {
|
||||
return;
|
||||
},
|
||||
} as never,
|
||||
circuitSectionRepository: {
|
||||
async findById() {
|
||||
@@ -527,7 +530,14 @@ describe("circuit write service rules", () => {
|
||||
targetSectionId: "s2",
|
||||
createNewCircuit: true,
|
||||
});
|
||||
assert.equal(createCount, 1);
|
||||
assert.equal(transactionCount, 1);
|
||||
assert.deepEqual(preparedTarget, {
|
||||
circuitListId: "l1",
|
||||
sectionId: "s2",
|
||||
equipmentIdentifier: "-2F8",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 40,
|
||||
});
|
||||
assert.equal(result.createdCircuitId, "c-new");
|
||||
});
|
||||
|
||||
@@ -650,6 +660,94 @@ describe("circuit write service rules", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("bulk device row move uses one synchronous transaction callback", () => {
|
||||
const repository = new CircuitDeviceRowRepository();
|
||||
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
||||
|
||||
let callbackReturnedPromise = false;
|
||||
let selectCall = 0;
|
||||
let rowUpdateCount = 0;
|
||||
const selectedRows = [
|
||||
[
|
||||
{ id: "r1", circuitId: "c1" },
|
||||
{ id: "r2", circuitId: "c2" },
|
||||
],
|
||||
[{ id: "c3", circuitListId: "l1" }],
|
||||
[
|
||||
{ id: "c1", circuitListId: "l1" },
|
||||
{ id: "c2", circuitListId: "l1" },
|
||||
],
|
||||
[{ id: "existing-target-row", sortOrder: 10 }],
|
||||
[],
|
||||
[],
|
||||
];
|
||||
|
||||
(db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => {
|
||||
const fakeTx = {
|
||||
select() {
|
||||
const rows = selectedRows[selectCall++] ?? [];
|
||||
const query = {
|
||||
from() {
|
||||
return query;
|
||||
},
|
||||
where() {
|
||||
return query;
|
||||
},
|
||||
limit() {
|
||||
return query;
|
||||
},
|
||||
all() {
|
||||
return rows;
|
||||
},
|
||||
};
|
||||
return query;
|
||||
},
|
||||
update() {
|
||||
return {
|
||||
set() {
|
||||
return {
|
||||
where() {
|
||||
return {
|
||||
run() {
|
||||
rowUpdateCount += 1;
|
||||
return { changes: 1 };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const callbackResult = cb(fakeTx);
|
||||
callbackReturnedPromise = Boolean(
|
||||
callbackResult && typeof (callbackResult as Promise<unknown>).then === "function"
|
||||
);
|
||||
if (callbackReturnedPromise) {
|
||||
throw new Error("Transaction function cannot return a promise");
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = repository.moveRowsTransactional({
|
||||
rows: [
|
||||
{ id: "r1", expectedCircuitId: "c1" },
|
||||
{ id: "r2", expectedCircuitId: "c2" },
|
||||
],
|
||||
targetCircuitId: "c3",
|
||||
});
|
||||
assert.equal(callbackReturnedPromise, false);
|
||||
assert.equal(rowUpdateCount, 5);
|
||||
assert.deepEqual(result, {
|
||||
movedRowIds: ["r1", "r2"],
|
||||
targetCircuitId: "c3",
|
||||
createdCircuitId: undefined,
|
||||
});
|
||||
} finally {
|
||||
(db as unknown as { transaction: unknown }).transaction = originalTransaction;
|
||||
}
|
||||
});
|
||||
|
||||
it("tracks local edits on linked device rows as overridden fields", async () => {
|
||||
let savedOverrides: string | undefined;
|
||||
const current = {
|
||||
|
||||
Reference in New Issue
Block a user