import path from "node:path"; import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { eq } from "drizzle-orm"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext, type DatabaseContext, } from "../src/db/database-context.js"; import { CircuitDeviceRowTransactionRepository } from "../src/db/repositories/circuit-device-row-transaction.repository.js"; import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js"; import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; import { circuitSections } from "../src/db/schema/circuit-sections.js"; import { circuits } from "../src/db/schema/circuits.js"; import { projects } from "../src/db/schema/projects.js"; function createTestDatabase(): DatabaseContext { const context = createDatabaseContext(":memory:"); migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations"), }); context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run(); const board = new DistributionBoardRepository( context.db ).createWithCircuitListAndDefaultSections( "project-1", "UV-01" ); const [section] = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, board.id)) .limit(1) .all(); context.db .insert(circuits) .values({ id: "circuit-1", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1F1", displayName: "Reserve", sortOrder: 10, isReserve: 1, }) .run(); return context; } function insertCircuit( context: DatabaseContext, input: { id: string; equipmentIdentifier: string; sortOrder: number; isReserve?: number; } ) { const [seedCircuit] = context.db.select().from(circuits).limit(1).all(); context.db .insert(circuits) .values({ id: input.id, circuitListId: seedCircuit.circuitListId, sectionId: seedCircuit.sectionId, equipmentIdentifier: input.equipmentIdentifier, displayName: input.equipmentIdentifier, sortOrder: input.sortOrder, isReserve: input.isReserve ?? 1, }) .run(); } function insertDeviceRow( context: DatabaseContext, input: { id?: string; circuitId?: string; sortOrder?: number; displayName?: string; } = {} ) { const rowId = input.id ?? "row-1"; const circuitId = input.circuitId ?? "circuit-1"; context.db .insert(circuitDeviceRows) .values({ id: rowId, circuitId, sortOrder: input.sortOrder ?? 10, name: "Leuchte", displayName: input.displayName ?? "Leuchte", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }) .run(); context.db .update(circuits) .set({ isReserve: 0 }) .where(eq(circuits.id, circuitId)) .run(); } describe("circuit device-row transaction repository", () => { it("commits a new device row and clears the circuit reserve status together", () => { const context = createTestDatabase(); try { const repository = new CircuitDeviceRowTransactionRepository(context.db); const rowId = repository.createInCircuit({ circuitId: "circuit-1", name: "Steckdose", displayName: "Steckdose", quantity: 1, powerPerUnit: 0.2, simultaneityFactor: 1, }); const [row] = context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, rowId)) .all(); const [circuit] = context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-1")) .all(); assert.equal(row.circuitId, "circuit-1"); assert.equal(row.sortOrder, 10); assert.equal(circuit.isReserve, 0); } finally { context.close(); } }); it("rolls back the row insert when clearing the reserve status fails", () => { const context = createTestDatabase(); try { context.sqlite.exec(` CREATE TRIGGER fail_reserve_clear BEFORE UPDATE OF is_reserve ON circuits WHEN NEW.is_reserve = 0 BEGIN SELECT RAISE(ABORT, 'forced reserve clear failure'); END; `); const repository = new CircuitDeviceRowTransactionRepository(context.db); assert.throws( () => repository.createInCircuit({ circuitId: "circuit-1", name: "Steckdose", displayName: "Steckdose", quantity: 1, powerPerUnit: 0.2, simultaneityFactor: 1, }), /forced reserve clear failure/ ); assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0); assert.equal(context.db.select().from(circuits).all()[0].isReserve, 1); } finally { context.close(); } }); 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 { insertDeviceRow(context); const repository = new CircuitDeviceRowTransactionRepository(context.db); repository.deleteFromCircuit("row-1", "circuit-1"); assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0); assert.equal(context.db.select().from(circuits).all()[0].isReserve, 1); } finally { context.close(); } }); it("rolls back the row deletion when activating the reserve status fails", () => { const context = createTestDatabase(); try { insertDeviceRow(context); context.sqlite.exec(` CREATE TRIGGER fail_reserve_activation BEFORE UPDATE OF is_reserve ON circuits WHEN NEW.is_reserve = 1 BEGIN SELECT RAISE(ABORT, 'forced reserve activation failure'); END; `); const repository = new CircuitDeviceRowTransactionRepository(context.db); assert.throws( () => repository.deleteFromCircuit("row-1", "circuit-1"), /forced reserve activation failure/ ); assert.equal(context.db.select().from(circuitDeviceRows).all().length, 1); assert.equal(context.db.select().from(circuits).all()[0].isReserve, 0); } finally { context.close(); } }); it("moves rows to an existing circuit and updates both reserve states together", () => { const context = createTestDatabase(); try { insertCircuit(context, { id: "circuit-2", equipmentIdentifier: "-1F2", sortOrder: 20, }); insertCircuit(context, { id: "circuit-3", equipmentIdentifier: "-1F3", sortOrder: 30, }); insertDeviceRow(context); insertDeviceRow(context, { id: "row-2", circuitId: "circuit-2", sortOrder: 10, displayName: "Zweite Quellzeile", }); insertDeviceRow(context, { id: "target-row", circuitId: "circuit-3", sortOrder: 10, displayName: "Bestehende Zielzeile", }); const repository = new CircuitDeviceRowTransactionRepository(context.db); const result = repository.moveRows({ rows: [ { id: "row-1", expectedCircuitId: "circuit-1" }, { id: "row-2", expectedCircuitId: "circuit-2" }, ], targetCircuitId: "circuit-3", }); const movedRows = context.db .select() .from(circuitDeviceRows) .all() .filter((row) => row.id === "row-1" || row.id === "row-2") .sort((left, right) => left.sortOrder - right.sortOrder); const persistedCircuits = context.db.select().from(circuits).all(); const targetCircuit = persistedCircuits.find( (circuit) => circuit.id === "circuit-3" ); assert.deepEqual(result, { movedRowIds: ["row-1", "row-2"], targetCircuitId: "circuit-3", createdCircuitId: undefined, }); assert.deepEqual( movedRows.map((row) => [row.id, row.circuitId, row.sortOrder]), [ ["row-1", "circuit-3", 20], ["row-2", "circuit-3", 30], ] ); assert.equal( persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve, 1 ); assert.equal( persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve, 1 ); assert.equal(targetCircuit?.isReserve, 0); } finally { context.close(); } }); it("rolls back an existing-target move when a reserve update fails", () => { const context = createTestDatabase(); try { insertCircuit(context, { id: "circuit-2", equipmentIdentifier: "-1F2", sortOrder: 20, }); insertDeviceRow(context); context.sqlite.exec(` CREATE TRIGGER fail_source_reserve_update BEFORE UPDATE OF is_reserve ON circuits WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1 BEGIN SELECT RAISE(ABORT, 'forced source reserve failure'); END; `); const repository = new CircuitDeviceRowTransactionRepository(context.db); assert.throws( () => repository.moveRows({ rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }], targetCircuitId: "circuit-2", }), /forced source reserve failure/ ); const [row] = context.db.select().from(circuitDeviceRows).all(); const persistedCircuits = context.db.select().from(circuits).all(); assert.equal(row.circuitId, "circuit-1"); assert.equal( persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve, 0 ); assert.equal( persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve, 1 ); } finally { context.close(); } }); it("creates a new target circuit and moves rows into it atomically", () => { const context = createTestDatabase(); try { insertDeviceRow(context); const [sourceCircuit] = context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-1")) .all(); const repository = new CircuitDeviceRowTransactionRepository(context.db); const result = repository.moveRows({ rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }], createTargetCircuit: { circuitListId: sourceCircuit.circuitListId, sectionId: sourceCircuit.sectionId, equipmentIdentifier: "-1F2", displayName: "Neues Ziel", sortOrder: 20, }, }); const [movedRow] = context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-1")) .all(); const [createdCircuit] = context.db .select() .from(circuits) .where(eq(circuits.id, result.createdCircuitId!)) .all(); const [updatedSource] = context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-1")) .all(); assert.equal(result.targetCircuitId, result.createdCircuitId); assert.equal(createdCircuit.equipmentIdentifier, "-1F2"); assert.equal(createdCircuit.isReserve, 0); assert.equal(movedRow.circuitId, createdCircuit.id); assert.equal(updatedSource.isReserve, 1); } finally { context.close(); } }); it("rolls back a newly created target circuit when the move fails", () => { const context = createTestDatabase(); try { insertDeviceRow(context); const [sourceCircuit] = context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-1")) .all(); context.sqlite.exec(` CREATE TRIGGER fail_new_target_source_reserve BEFORE UPDATE OF is_reserve ON circuits WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1 BEGIN SELECT RAISE(ABORT, 'forced new-target move failure'); END; `); const repository = new CircuitDeviceRowTransactionRepository(context.db); assert.throws( () => repository.moveRows({ rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }], createTargetCircuit: { circuitListId: sourceCircuit.circuitListId, sectionId: sourceCircuit.sectionId, equipmentIdentifier: "-1F2", displayName: "Rollback-Ziel", sortOrder: 20, }, }), /forced new-target move failure/ ); const [row] = context.db.select().from(circuitDeviceRows).all(); const persistedCircuits = context.db.select().from(circuits).all(); assert.equal(row.circuitId, "circuit-1"); assert.equal(persistedCircuits.length, 1); assert.equal(persistedCircuits[0].isReserve, 0); } finally { context.close(); } }); });