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 { CircuitDeviceRowStructureProjectCommandRepository } from "../src/db/repositories/circuit-device-row-structure-project-command.repository.js"; import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js"; import { ProjectHistoryRepository } from "../src/db/repositories/project-history.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 { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; import { rooms } from "../src/db/schema/rooms.js"; import { createCircuitDeviceRowDeleteProjectCommand, createCircuitDeviceRowInsertProjectCommand, type CircuitDeviceRowSnapshot, } from "../src/domain/models/circuit-device-row-structure-project-command.model.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" }, { id: "project-2", name: "Other project" }, ]) .run(); const boards = new DistributionBoardRepository(context.db); const board = boards.createWithCircuitListAndDefaultSections( "project-1", "UV-01" ); const foreignBoard = boards.createWithCircuitListAndDefaultSections( "project-2", "UV-02" ); const section = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, board.id)) .get(); const foreignSection = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, foreignBoard.id)) .get(); assert.ok(section); assert.ok(foreignSection); context.db .insert(circuits) .values([ { id: "circuit-1", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0, }, { id: "circuit-reserve", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 1, }, { id: "circuit-foreign", circuitListId: foreignBoard.id, sectionId: foreignSection.id, equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 1, }, ]) .run(); context.db .insert(projectDevices) .values([ { id: "device-1", projectId: "project-1", name: "Leuchte", displayName: "Leuchte", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }, { id: "device-foreign", projectId: "project-2", name: "Fremdgerät", displayName: "Fremdgerät", quantity: 1, powerPerUnit: 0.2, simultaneityFactor: 1, }, ]) .run(); context.db .insert(rooms) .values([ { id: "room-1", projectId: "project-1", roomNumber: "001", roomName: "Büro", }, { id: "room-foreign", projectId: "project-2", roomNumber: "999", roomName: "Fremdraum", }, ]) .run(); context.db .insert(circuitDeviceRows) .values({ id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: "device-1", legacyConsumerId: "legacy-1", sortOrder: 10, name: "Leuchte", displayName: "Leuchte lokal", phaseType: "single_phase", connectionKind: "fixed", costGroup: "440", category: "lighting", level: "EG", roomId: "room-1", roomNumberSnapshot: "001", roomNameSnapshot: "Büro", quantity: 2, powerPerUnit: 0.12, simultaneityFactor: 0.8, cosPhi: 0.9, remark: "Bestand", overriddenFields: "[\"displayName\"]", }) .run(); return context; } function createInsertSnapshot( overrides: Partial = {} ): CircuitDeviceRowSnapshot { return { id: "row-new", circuitId: "circuit-reserve", linkedProjectDeviceId: "device-1", legacyConsumerId: null, sortOrder: 10, name: "Steckdose", displayName: "Steckdose lokal", phaseType: "single_phase", connectionKind: "socket", costGroup: "440", category: "socket", level: "EG", roomId: "room-1", roomNumberSnapshot: "001", roomNameSnapshot: "Büro", quantity: 1, powerPerUnit: 0.2, simultaneityFactor: 1, cosPhi: 0.95, remark: null, overriddenFields: null, ...overrides, }; } function getIsReserve(context: DatabaseContext, circuitId: string) { return Boolean( context.db .select({ isReserve: circuits.isReserve }) .from(circuits) .where(eq(circuits.id, circuitId)) .get()?.isReserve ); } describe("circuit device-row structure project-command repository", () => { it("inserts a stable row and supports persisted undo and redo", () => { const context = createTestDatabase(); try { const store = new CircuitDeviceRowStructureProjectCommandRepository(context.db); const command = createCircuitDeviceRowInsertProjectCommand( createInsertSnapshot() ); const inserted = store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.equal( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-new")) .get()?.displayName, "Steckdose lokal" ); assert.equal(getIsReserve(context, "circuit-reserve"), false); store.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: inserted.revision.changeSetId, command: inserted.inverse, }); assert.equal( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-new")) .get(), undefined ); assert.equal(getIsReserve(context, "circuit-reserve"), true); store.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: inserted.revision.changeSetId, command, }); assert.equal( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-new")) .get()?.id, "row-new" ); assert.deepEqual( new ProjectHistoryRepository(context.db).getState("project-1"), { projectId: "project-1", currentRevision: 3, undoDepth: 1, redoDepth: 0, undoChangeSetId: inserted.revision.changeSetId, redoChangeSetId: null, } ); } finally { context.close(); } }); it("deletes and restores the complete persisted row snapshot", () => { const context = createTestDatabase(); try { const store = new CircuitDeviceRowStructureProjectCommandRepository(context.db); const before = context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-1")) .get(); assert.ok(before); const deleted = store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitDeviceRowDeleteProjectCommand( "row-1", "circuit-1" ), }); assert.equal(getIsReserve(context, "circuit-1"), true); assert.deepEqual(deleted.inverse.payload, { row: before }); store.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: deleted.revision.changeSetId, command: deleted.inverse, }); assert.deepEqual( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-1")) .get(), before ); assert.equal(getIsReserve(context, "circuit-1"), false); } finally { context.close(); } }); it("rolls back insert and delete when late history persistence fails", () => { const context = createTestDatabase(); try { context.sqlite.exec(` CREATE TRIGGER fail_structure_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced structure history failure'); END; `); const store = new CircuitDeviceRowStructureProjectCommandRepository(context.db); assert.throws( () => store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitDeviceRowInsertProjectCommand( createInsertSnapshot() ), }), /forced structure history failure/ ); assert.equal( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-new")) .get(), undefined ); assert.equal(getIsReserve(context, "circuit-reserve"), true); assert.throws( () => store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitDeviceRowDeleteProjectCommand( "row-1", "circuit-1" ), }), /forced structure history failure/ ); assert.ok( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-1")) .get() ); assert.equal(getIsReserve(context, "circuit-1"), false); assert.equal(context.db.select().from(projectRevisions).all().length, 0); } finally { context.close(); } }); it("rolls back a structural write for a stale expected revision", () => { const context = createTestDatabase(); try { const store = new CircuitDeviceRowStructureProjectCommandRepository(context.db); assert.throws( () => store.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createCircuitDeviceRowDeleteProjectCommand( "row-1", "circuit-1" ), }), /at revision 0, expected 1/ ); assert.ok( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-1")) .get() ); assert.equal(getIsReserve(context, "circuit-1"), false); assert.equal(context.db.select().from(projectRevisions).all().length, 0); } finally { context.close(); } }); it("rejects foreign ownership, references and duplicate row ids", () => { const context = createTestDatabase(); try { const store = new CircuitDeviceRowStructureProjectCommandRepository(context.db); for (const snapshot of [ createInsertSnapshot({ circuitId: "circuit-foreign" }), createInsertSnapshot({ id: "row-foreign-device", linkedProjectDeviceId: "device-foreign", }), createInsertSnapshot({ id: "row-foreign-room", roomId: "room-foreign", }), createInsertSnapshot({ id: "row-1" }), createInsertSnapshot({ id: "row-legacy", legacyConsumerId: "legacy-user-value", }), ]) { assert.throws(() => store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitDeviceRowInsertProjectCommand(snapshot), }) ); } assert.equal(context.db.select().from(projectRevisions).all().length, 0); } finally { context.close(); } }); });