Make device sync atomic and undoable
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { ProjectDeviceSyncService } from "../src/domain/services/project-device-sync.service.js";
|
||||
import { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js";
|
||||
import { db } from "../src/db/client.js";
|
||||
|
||||
function projectDevice() {
|
||||
return {
|
||||
@@ -56,6 +58,7 @@ function linkedRow() {
|
||||
function createService() {
|
||||
const rows = [linkedRow()];
|
||||
const updates: Array<Record<string, unknown>> = [];
|
||||
let transactionCalls = 0;
|
||||
const service = new ProjectDeviceSyncService({
|
||||
projectDeviceRepository: {
|
||||
async findById() {
|
||||
@@ -64,15 +67,35 @@ function createService() {
|
||||
},
|
||||
deviceRowRepository: {
|
||||
async listLinkedByProjectDevice() {
|
||||
return rows as never;
|
||||
return rows.filter((row) => row.linkedProjectDeviceId === "pd1") as never;
|
||||
},
|
||||
async update(rowId, input) {
|
||||
updates.push({ rowId, ...input });
|
||||
Object.assign(rows[0], input);
|
||||
async listLinkStatesByIds() {
|
||||
return rows.map((row) => ({ id: row.id, linkedProjectDeviceId: row.linkedProjectDeviceId })) as never;
|
||||
},
|
||||
updateLinkedRowsTransactional(_projectDeviceId, changes) {
|
||||
transactionCalls += 1;
|
||||
for (const change of changes) {
|
||||
updates.push({ rowId: change.rowId, ...change.input });
|
||||
Object.assign(rows.find((row) => row.id === change.rowId)!, change.input);
|
||||
}
|
||||
},
|
||||
disconnectLinkedRowsTransactional(_projectDeviceId, rowIds) {
|
||||
transactionCalls += 1;
|
||||
for (const rowId of rowIds) {
|
||||
const row = rows.find((entry) => entry.id === rowId)!;
|
||||
updates.push({ rowId, linkedProjectDeviceId: undefined, displayName: row.displayName, quantity: row.quantity });
|
||||
Object.assign(row, { linkedProjectDeviceId: null });
|
||||
}
|
||||
},
|
||||
reconnectRowsTransactional(projectDeviceId, rowIds) {
|
||||
transactionCalls += 1;
|
||||
for (const rowId of rowIds) {
|
||||
Object.assign(rows.find((row) => row.id === rowId)!, { linkedProjectDeviceId: projectDeviceId });
|
||||
}
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
return { service, rows, updates };
|
||||
return { service, rows, updates, transactionCalls: () => transactionCalls };
|
||||
}
|
||||
|
||||
describe("project device synchronization", () => {
|
||||
@@ -90,13 +113,14 @@ describe("project device synchronization", () => {
|
||||
});
|
||||
|
||||
it("synchronizes only explicitly selected fields", async () => {
|
||||
const { service, rows } = createService();
|
||||
const { service, rows, transactionCalls } = createService();
|
||||
await service.synchronize("p1", "pd1", ["row1"], ["quantity", "powerPerUnit"]);
|
||||
|
||||
assert.equal(rows[0].quantity, 6);
|
||||
assert.equal(rows[0].powerPerUnit, 0.04);
|
||||
assert.equal(rows[0].displayName, "Local display name");
|
||||
assert.deepEqual(JSON.parse(rows[0].overriddenFields ?? "[]"), ["displayName"]);
|
||||
assert.equal(transactionCalls(), 1);
|
||||
});
|
||||
|
||||
it("rejects rows that are not linked to the selected project device", async () => {
|
||||
@@ -115,4 +139,75 @@ describe("project device synchronization", () => {
|
||||
assert.equal(updates[0].displayName, "Local display name");
|
||||
assert.equal(updates[0].quantity, 2);
|
||||
});
|
||||
|
||||
it("restores synchronized values and override markers", async () => {
|
||||
const { service, rows } = createService();
|
||||
const result = await service.synchronize("p1", "pd1", ["row1"], ["quantity", "powerPerUnit"]);
|
||||
|
||||
await service.restore("p1", "pd1", result.undo.rows);
|
||||
|
||||
assert.equal(rows[0].quantity, 2);
|
||||
assert.equal(rows[0].powerPerUnit, 0.03);
|
||||
assert.deepEqual(JSON.parse(rows[0].overriddenFields ?? "[]"), ["displayName", "quantity"]);
|
||||
});
|
||||
|
||||
it("reconnects rows only while they remain disconnected", async () => {
|
||||
const { service, rows } = createService();
|
||||
const result = await service.disconnect("p1", "pd1", ["row1"]);
|
||||
|
||||
await service.reconnect("p1", "pd1", result.undo.rowIds);
|
||||
|
||||
assert.equal(rows[0].linkedProjectDeviceId, "pd1");
|
||||
});
|
||||
|
||||
it("executes multi-row updates inside one synchronous transaction", () => {
|
||||
const repository = new CircuitDeviceRowRepository();
|
||||
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
||||
let transactionCalls = 0;
|
||||
let updateCalls = 0;
|
||||
let returnedPromise = false;
|
||||
(db as unknown as { transaction: (callback: (tx: unknown) => unknown) => void }).transaction = (callback) => {
|
||||
transactionCalls += 1;
|
||||
const fakeTx = {
|
||||
update() {
|
||||
return {
|
||||
set() {
|
||||
return {
|
||||
where() {
|
||||
return {
|
||||
run() {
|
||||
updateCalls += 1;
|
||||
return { changes: 1 };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const result = callback(fakeTx);
|
||||
returnedPromise = Boolean(result && typeof (result as Promise<unknown>).then === "function");
|
||||
};
|
||||
|
||||
try {
|
||||
const input = {
|
||||
linkedProjectDeviceId: "pd1",
|
||||
name: "Luminaire",
|
||||
displayName: "Office lighting",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
};
|
||||
repository.updateLinkedRowsTransactional("pd1", [
|
||||
{ rowId: "r1", input },
|
||||
{ rowId: "r2", input },
|
||||
]);
|
||||
assert.equal(transactionCalls, 1);
|
||||
assert.equal(updateCalls, 2);
|
||||
assert.equal(returnedPromise, false);
|
||||
} finally {
|
||||
(db as unknown as { transaction: unknown }).transaction = originalTransaction;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user