Make initial circuit creation atomic

This commit is contained in:
2026-07-23 18:46:50 +02:00
parent 30c7555c68
commit a6837d7242
9 changed files with 234 additions and 169 deletions
@@ -134,6 +134,115 @@ describe("circuit device-row transaction repository", () => {
}
});
it("commits a new circuit and all initial device rows together", () => {
const context = createTestDatabase();
try {
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
const repository = new CircuitDeviceRowTransactionRepository(context.db);
const created = repository.createCircuitWithDeviceRows({
circuit: {
circuitListId: seedCircuit.circuitListId,
sectionId: seedCircuit.sectionId,
equipmentIdentifier: "-1F2",
displayName: "Beleuchtung",
sortOrder: 20,
},
deviceRows: [
{
name: "Leuchte",
displayName: "Leuchte 1",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
sortOrder: 15,
},
{
name: "Leuchte",
displayName: "Leuchte 2",
quantity: 2,
powerPerUnit: 0.1,
simultaneityFactor: 0.8,
},
],
});
const [createdCircuit] = context.db
.select()
.from(circuits)
.where(eq(circuits.id, created.circuitId))
.all();
const createdRows = context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, created.circuitId))
.all()
.sort((left, right) => left.sortOrder - right.sortOrder);
assert.equal(createdCircuit.equipmentIdentifier, "-1F2");
assert.equal(createdCircuit.isReserve, 0);
assert.deepEqual(
createdRows.map((row) => [row.id, row.displayName, row.sortOrder]),
[
[created.rowIds[0], "Leuchte 1", 15],
[created.rowIds[1], "Leuchte 2", 20],
]
);
} finally {
context.close();
}
});
it("rolls back the circuit and earlier rows when an initial row insert fails", () => {
const context = createTestDatabase();
try {
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
context.sqlite.exec(`
CREATE TRIGGER fail_initial_device_row
BEFORE INSERT ON circuit_device_rows
WHEN NEW.name = 'Fail'
BEGIN
SELECT RAISE(ABORT, 'forced initial row failure');
END;
`);
const repository = new CircuitDeviceRowTransactionRepository(context.db);
assert.throws(
() =>
repository.createCircuitWithDeviceRows({
circuit: {
circuitListId: seedCircuit.circuitListId,
sectionId: seedCircuit.sectionId,
equipmentIdentifier: "-1F2",
displayName: "Rollback",
sortOrder: 20,
},
deviceRows: [
{
name: "First",
displayName: "Erste Zeile",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
},
{
name: "Fail",
displayName: "Fehlerhafte Zeile",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
},
],
}),
/forced initial row failure/
);
assert.equal(context.db.select().from(circuits).all().length, 1);
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
} finally {
context.close();
}
});
it("commits the last-row deletion and activates the circuit reserve status together", () => {
const context = createTestDatabase();
try {
+6 -74
View File
@@ -251,14 +251,16 @@ describe("circuit write service rules", () => {
},
} as never,
deviceRowRepository: {
createCircuitWithDeviceRowsTransactional(input: typeof transactionalCreate) {
transactionalCreate = input;
return { circuitId: "c-new", rowIds: ["r1", "r2"] };
},
async findById(rowId: string) {
return { id: rowId, circuitId: "c-new" } as never;
},
} as never,
deviceRowTransactionStore: {
createCircuitWithDeviceRows(input: typeof transactionalCreate) {
transactionalCreate = input;
return { circuitId: "c-new", rowIds: ["r1", "r2"] };
},
} as never,
});
const created = await service.createCircuitWithDeviceRows("p1", "l1", {
@@ -925,76 +927,6 @@ describe("circuit write service rules", () => {
}
});
it("new circuit and initial device rows share one synchronous transaction", () => {
const repository = new CircuitDeviceRowRepository();
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
let callbackReturnedPromise = false;
const insertedValues: Array<Record<string, unknown>> = [];
(db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => {
const fakeTx = {
insert() {
return {
values(values: Record<string, unknown>) {
insertedValues.push(values);
return {
run() {
return { changes: 1 };
},
};
},
};
},
};
const callbackResult = cb(fakeTx);
callbackReturnedPromise = Boolean(
callbackResult && typeof (callbackResult as Promise<unknown>).then === "function"
);
};
try {
const created = repository.createCircuitWithDeviceRowsTransactional({
circuit: {
circuitListId: "l1",
sectionId: "s1",
equipmentIdentifier: "-2F1",
displayName: "Steckdosen",
sortOrder: 10,
},
deviceRows: [
{
name: "Socket",
displayName: "Steckdose",
quantity: 1,
powerPerUnit: 0.2,
simultaneityFactor: 1,
sortOrder: 15,
},
{
name: "Manual",
displayName: "Manuell",
quantity: 2,
powerPerUnit: 0.1,
simultaneityFactor: 0.8,
},
],
});
assert.equal(callbackReturnedPromise, false);
assert.equal(insertedValues.length, 3);
assert.equal(insertedValues[0].id, created.circuitId);
assert.equal(insertedValues[0].isReserve, 0);
assert.equal(insertedValues[1].id, created.rowIds[0]);
assert.equal(insertedValues[1].circuitId, created.circuitId);
assert.equal(insertedValues[1].sortOrder, 15);
assert.equal(insertedValues[2].id, created.rowIds[1]);
assert.equal(insertedValues[2].circuitId, created.circuitId);
assert.equal(insertedValues[2].sortOrder, 20);
} finally {
(db as unknown as { transaction: unknown }).transaction = originalTransaction;
}
});
it("tracks local edits on linked device rows as overridden fields", async () => {
let updatePayload: Record<string, unknown> = {};
const current = {