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 { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js"; import { ProjectDeviceRowSyncRepository } from "../src/db/repositories/project-device-row-sync.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 { projectDevices } from "../src/db/schema/project-devices.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: "Beleuchtung", sortOrder: 10, isReserve: 0, }) .run(); context.db .insert(projectDevices) .values({ id: "project-device-1", projectId: "project-1", name: "Luminaire", displayName: "Office lighting", phaseType: "single_phase", quantity: 4, powerPerUnit: 0.04, simultaneityFactor: 0.8, installedPowerPerUnitKw: 0.04, demandFactor: 0.8, }) .run(); context.db .insert(circuitDeviceRows) .values([ { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: "project-device-1", sortOrder: 10, name: "Old luminaire", displayName: "Local row 1", quantity: 1, powerPerUnit: 0.03, simultaneityFactor: 1, }, { id: "row-2", circuitId: "circuit-1", linkedProjectDeviceId: "project-device-1", sortOrder: 20, name: "Old luminaire", displayName: "Local row 2", quantity: 2, powerPerUnit: 0.03, simultaneityFactor: 1, }, ]) .run(); return context; } function synchronizedValues(displayName: string) { return { linkedProjectDeviceId: "project-device-1", name: "Luminaire", displayName, phaseType: "single_phase", quantity: 4, powerPerUnit: 0.04, simultaneityFactor: 0.8, cosPhi: 0.95, remark: "DALI", }; } function listRows(context: DatabaseContext) { return context.db .select() .from(circuitDeviceRows) .all() .sort((left, right) => left.id.localeCompare(right.id)); } describe("project-device row sync repository", () => { it("commits synchronized values for all selected linked rows", () => { const context = createTestDatabase(); try { const repository = new ProjectDeviceRowSyncRepository(context.db); repository.updateLinkedRows("project-device-1", [ { rowId: "row-1", input: synchronizedValues("Synced row 1") }, { rowId: "row-2", input: synchronizedValues("Synced row 2") }, ]); assert.deepEqual( listRows(context).map((row) => [ row.displayName, row.quantity, row.powerPerUnit, row.simultaneityFactor, ]), [ ["Synced row 1", 4, 0.04, 0.8], ["Synced row 2", 4, 0.04, 0.8], ] ); } finally { context.close(); } }); it("rolls back earlier synchronized values when a later row update fails", () => { const context = createTestDatabase(); try { context.sqlite.exec(` CREATE TRIGGER fail_second_sync_update BEFORE UPDATE ON circuit_device_rows WHEN OLD.id = 'row-2' AND NEW.display_name = 'Synced' BEGIN SELECT RAISE(ABORT, 'forced sync failure'); END; `); const repository = new ProjectDeviceRowSyncRepository(context.db); assert.throws( () => repository.updateLinkedRows("project-device-1", [ { rowId: "row-1", input: synchronizedValues("Synced") }, { rowId: "row-2", input: synchronizedValues("Synced") }, ]), /forced sync failure/ ); assert.deepEqual( listRows(context).map((row) => [row.displayName, row.quantity]), [ ["Local row 1", 1], ["Local row 2", 2], ] ); } finally { context.close(); } }); it("disconnects all selected rows without changing local values", () => { const context = createTestDatabase(); try { const repository = new ProjectDeviceRowSyncRepository(context.db); repository.disconnectLinkedRows("project-device-1", ["row-1", "row-2"]); assert.deepEqual( listRows(context).map((row) => [ row.linkedProjectDeviceId, row.displayName, row.quantity, ]), [ [null, "Local row 1", 1], [null, "Local row 2", 2], ] ); } finally { context.close(); } }); it("rolls back earlier disconnects when a later row update fails", () => { const context = createTestDatabase(); try { context.sqlite.exec(` CREATE TRIGGER fail_second_disconnect BEFORE UPDATE ON circuit_device_rows WHEN OLD.id = 'row-2' AND NEW.linked_project_device_id IS NULL BEGIN SELECT RAISE(ABORT, 'forced disconnect failure'); END; `); const repository = new ProjectDeviceRowSyncRepository(context.db); assert.throws( () => repository.disconnectLinkedRows("project-device-1", ["row-1", "row-2"]), /forced disconnect failure/ ); assert.deepEqual( listRows(context).map((row) => row.linkedProjectDeviceId), ["project-device-1", "project-device-1"] ); } finally { context.close(); } }); it("reconnects all selected disconnected rows", () => { const context = createTestDatabase(); try { context.db.update(circuitDeviceRows).set({ linkedProjectDeviceId: null }).run(); const repository = new ProjectDeviceRowSyncRepository(context.db); repository.reconnectRows("project-device-1", ["row-1", "row-2"]); assert.deepEqual( listRows(context).map((row) => row.linkedProjectDeviceId), ["project-device-1", "project-device-1"] ); } finally { context.close(); } }); it("rolls back earlier reconnects when a later row update fails", () => { const context = createTestDatabase(); try { context.db.update(circuitDeviceRows).set({ linkedProjectDeviceId: null }).run(); context.sqlite.exec(` CREATE TRIGGER fail_second_reconnect BEFORE UPDATE ON circuit_device_rows WHEN OLD.id = 'row-2' AND NEW.linked_project_device_id = 'project-device-1' BEGIN SELECT RAISE(ABORT, 'forced reconnect failure'); END; `); const repository = new ProjectDeviceRowSyncRepository(context.db); assert.throws( () => repository.reconnectRows("project-device-1", ["row-1", "row-2"]), /forced reconnect failure/ ); assert.deepEqual( listRows(context).map((row) => row.linkedProjectDeviceId), [null, null] ); } finally { context.close(); } }); });