1161 lines
36 KiB
TypeScript
1161 lines
36 KiB
TypeScript
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 { CircuitDeviceRowProjectCommandRepository } from "../src/db/repositories/circuit-device-row-project-command.repository.js";
|
|
import { CircuitDeviceRowMoveProjectCommandRepository } from "../src/db/repositories/circuit-device-row-move-project-command.repository.js";
|
|
import { CircuitDeviceRowStructureProjectCommandRepository } from "../src/db/repositories/circuit-device-row-structure-project-command.repository.js";
|
|
import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-project-command.repository.js";
|
|
import { CircuitSectionReorderProjectCommandRepository } from "../src/db/repositories/circuit-section-reorder-project-command.repository.js";
|
|
import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/repositories/circuit-section-renumber-project-command.repository.js";
|
|
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
|
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
|
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
|
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
|
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
|
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
|
import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js";
|
|
import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/project-settings-project-command.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 { projectChangeSets } from "../src/db/schema/project-change-sets.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 { floors } from "../src/db/schema/floors.js";
|
|
import { rooms } from "../src/db/schema/rooms.js";
|
|
import { ProjectHistoryOperationUnavailableError } from "../src/domain/errors/project-history-operation-unavailable.error.js";
|
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
|
import { createCircuitDeviceRowUpdateProjectCommand } from "../src/domain/models/circuit-device-row-project-command.model.js";
|
|
import {
|
|
createCircuitDeviceRowMoveProjectCommand,
|
|
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
|
} from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
|
import { createCircuitDeviceRowInsertProjectCommand } from "../src/domain/models/circuit-device-row-structure-project-command.model.js";
|
|
import { createCircuitUpdateProjectCommand } from "../src/domain/models/circuit-project-command.model.js";
|
|
import { createCircuitSectionReorderProjectCommand } from "../src/domain/models/circuit-section-reorder-project-command.model.js";
|
|
import { createCircuitSectionsReorderProjectCommand } from "../src/domain/models/circuit-sections-reorder-project-command.model.js";
|
|
import { createCircuitSectionRenumberProjectCommand } from "../src/domain/models/circuit-section-renumber-project-command.model.js";
|
|
import { createCircuitInsertProjectCommand } from "../src/domain/models/circuit-structure-project-command.model.js";
|
|
import {
|
|
createDistributionBoardInsertProjectCommand,
|
|
createDistributionBoardStructureSnapshot,
|
|
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
|
import {
|
|
createProjectFloorInsertProjectCommand,
|
|
createProjectFloorSnapshot,
|
|
createProjectRoomInsertProjectCommand,
|
|
createProjectRoomSnapshot,
|
|
} from "../src/domain/models/project-location-structure-project-command.model.js";
|
|
import { createProjectDeviceRowSyncProjectCommand } from "../src/domain/models/project-device-row-sync-project-command.model.js";
|
|
import { createProjectDeviceUpdateProjectCommand } from "../src/domain/models/project-device-project-command.model.js";
|
|
import { createProjectDeviceInsertProjectCommand } from "../src/domain/models/project-device-structure-project-command.model.js";
|
|
import { createProjectSettingsUpdateProjectCommand } from "../src/domain/models/project-settings-project-command.model.js";
|
|
import { ProjectCommandService } from "../src/domain/services/project-command.service.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 DistributionBoardFixtureRepository(
|
|
context.db
|
|
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
|
const section = context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.circuitListId, board.id))
|
|
.get();
|
|
assert.ok(section);
|
|
context.db
|
|
.insert(circuits)
|
|
.values({
|
|
id: "circuit-1",
|
|
circuitListId: board.id,
|
|
sectionId: section.id,
|
|
equipmentIdentifier: "-1F1",
|
|
displayName: "Bestand",
|
|
sortOrder: 10,
|
|
})
|
|
.run();
|
|
context.db
|
|
.insert(circuitDeviceRows)
|
|
.values({
|
|
id: "row-1",
|
|
circuitId: "circuit-1",
|
|
sortOrder: 10,
|
|
name: "Leuchte",
|
|
displayName: "Leuchte",
|
|
quantity: 1,
|
|
powerPerUnit: 0.1,
|
|
simultaneityFactor: 1,
|
|
})
|
|
.run();
|
|
return context;
|
|
}
|
|
|
|
function createService(context: DatabaseContext) {
|
|
return new ProjectCommandService(
|
|
new CircuitProjectCommandRepository(context.db),
|
|
new CircuitDeviceRowProjectCommandRepository(context.db),
|
|
new CircuitDeviceRowStructureProjectCommandRepository(context.db),
|
|
new CircuitDeviceRowMoveProjectCommandRepository(context.db),
|
|
new CircuitStructureProjectCommandRepository(context.db),
|
|
new DistributionBoardStructureProjectCommandRepository(context.db),
|
|
new ProjectLocationStructureProjectCommandRepository(context.db),
|
|
new CircuitSectionReorderProjectCommandRepository(context.db),
|
|
new CircuitSectionRenumberProjectCommandRepository(context.db),
|
|
new ProjectDeviceStructureProjectCommandRepository(context.db),
|
|
new ProjectDeviceProjectCommandRepository(context.db),
|
|
new ProjectDeviceRowSyncProjectCommandRepository(context.db),
|
|
new ProjectSettingsProjectCommandRepository(context.db),
|
|
new ProjectStateRestoreCommandRepository(context.db),
|
|
new ProjectHistoryRepository(context.db)
|
|
);
|
|
}
|
|
|
|
function getCircuitName(context: DatabaseContext) {
|
|
return context.db
|
|
.select({ displayName: circuits.displayName })
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-1"))
|
|
.get()?.displayName;
|
|
}
|
|
|
|
function getRowQuantity(context: DatabaseContext) {
|
|
return context.db
|
|
.select({ quantity: circuitDeviceRows.quantity })
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, "row-1"))
|
|
.get()?.quantity;
|
|
}
|
|
|
|
describe("project command service", () => {
|
|
it("dispatches supported commands and persists undo/redo across service instances", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const circuitResult = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitUpdateProjectCommand("circuit-1", {
|
|
displayName: "Neu",
|
|
}),
|
|
});
|
|
assert.equal(circuitResult.revision.revisionNumber, 1);
|
|
assert.equal(circuitResult.history.undoDepth, 1);
|
|
assert.equal(getCircuitName(context), "Neu");
|
|
|
|
const rowResult = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
command: createCircuitDeviceRowUpdateProjectCommand("row-1", {
|
|
quantity: 2,
|
|
}),
|
|
});
|
|
assert.equal(rowResult.revision.revisionNumber, 2);
|
|
assert.equal(rowResult.history.undoDepth, 2);
|
|
assert.equal(getRowQuantity(context), 2);
|
|
|
|
const reloadedService = createService(context);
|
|
const firstUndo = reloadedService.undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 2,
|
|
});
|
|
assert.equal(firstUndo.revision.revisionNumber, 3);
|
|
assert.equal(firstUndo.history.undoDepth, 1);
|
|
assert.equal(firstUndo.history.redoDepth, 1);
|
|
assert.equal(getRowQuantity(context), 1);
|
|
|
|
const secondUndo = reloadedService.undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 3,
|
|
});
|
|
assert.equal(secondUndo.revision.revisionNumber, 4);
|
|
assert.equal(secondUndo.history.undoDepth, 0);
|
|
assert.equal(secondUndo.history.redoDepth, 2);
|
|
assert.equal(getCircuitName(context), "Bestand");
|
|
|
|
const redo = reloadedService.redo({
|
|
projectId: "project-1",
|
|
expectedRevision: 4,
|
|
});
|
|
assert.equal(redo.revision.revisionNumber, 5);
|
|
assert.equal(redo.history.undoDepth, 1);
|
|
assert.equal(redo.history.redoDepth, 1);
|
|
assert.equal(getCircuitName(context), "Neu");
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({ source: projectRevisions.source })
|
|
.from(projectRevisions)
|
|
.all()
|
|
.map((row) => row.source),
|
|
["user", "user", "undo", "undo", "redo"]
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches project-device row synchronization and its inverse", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
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,
|
|
})
|
|
.run();
|
|
context.db
|
|
.update(circuitDeviceRows)
|
|
.set({
|
|
linkedProjectDeviceId: "project-device-1",
|
|
overriddenFields: '["displayName"]',
|
|
})
|
|
.where(eq(circuitDeviceRows.id, "row-1"))
|
|
.run();
|
|
const expected = {
|
|
linkedProjectDeviceId: "project-device-1",
|
|
name: "Leuchte",
|
|
displayName: "Leuchte",
|
|
phaseType: null,
|
|
connectionKind: null,
|
|
costGroup: null,
|
|
category: null,
|
|
quantity: 1,
|
|
powerPerUnit: 0.1,
|
|
simultaneityFactor: 1,
|
|
cosPhi: null,
|
|
remark: null,
|
|
overriddenFields: '["displayName"]',
|
|
};
|
|
const service = createService(context);
|
|
const synchronized = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createProjectDeviceRowSyncProjectCommand(
|
|
"project-device-1",
|
|
"synchronize",
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expected,
|
|
target: {
|
|
...expected,
|
|
name: "Luminaire",
|
|
displayName: "Office lighting",
|
|
quantity: 4,
|
|
powerPerUnit: 0.04,
|
|
simultaneityFactor: 0.8,
|
|
overriddenFields: null,
|
|
},
|
|
},
|
|
]
|
|
),
|
|
});
|
|
assert.equal(synchronized.history.undoDepth, 1);
|
|
assert.equal(getRowQuantity(context), 4);
|
|
|
|
const undone = createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.redoDepth, 1);
|
|
assert.equal(getRowQuantity(context), 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, "row-1"))
|
|
.get()?.overriddenFields,
|
|
'["displayName"]'
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches project-device updates and their persisted inverse", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
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,
|
|
})
|
|
.run();
|
|
const service = createService(context);
|
|
const updated = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createProjectDeviceUpdateProjectCommand(
|
|
"project-device-1",
|
|
{
|
|
displayName: "Flurbeleuchtung",
|
|
powerPerUnit: 0.05,
|
|
}
|
|
),
|
|
});
|
|
assert.equal(updated.history.undoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(projectDevices)
|
|
.where(eq(projectDevices.id, "project-device-1"))
|
|
.get()?.displayName,
|
|
"Flurbeleuchtung"
|
|
);
|
|
|
|
const undone = createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.redoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(projectDevices)
|
|
.where(eq(projectDevices.id, "project-device-1"))
|
|
.get()?.displayName,
|
|
"Office lighting"
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches project-device insertion and deletion", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const inserted = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createProjectDeviceInsertProjectCommand({
|
|
id: "project-device-new",
|
|
projectId: "project-1",
|
|
name: "Socket",
|
|
displayName: "Office socket",
|
|
phaseType: "single_phase",
|
|
connectionKind: null,
|
|
costGroup: "440",
|
|
category: "socket",
|
|
quantity: 2,
|
|
powerPerUnit: 0.2,
|
|
simultaneityFactor: 0.5,
|
|
cosPhi: null,
|
|
remark: null,
|
|
voltageV: 230,
|
|
}),
|
|
});
|
|
assert.equal(inserted.history.undoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(projectDevices)
|
|
.where(eq(projectDevices.id, "project-device-new"))
|
|
.get()?.displayName,
|
|
"Office socket"
|
|
);
|
|
|
|
const undone = createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.redoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(projectDevices)
|
|
.where(eq(projectDevices.id, "project-device-new"))
|
|
.get(),
|
|
undefined
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("rejects stale undo and preserves the domain value and stack", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const changed = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitUpdateProjectCommand("circuit-1", {
|
|
displayName: "Neu",
|
|
}),
|
|
});
|
|
|
|
assert.throws(
|
|
() =>
|
|
service.undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
}),
|
|
(error) =>
|
|
error instanceof ProjectRevisionConflictError &&
|
|
error.actualRevision === 1
|
|
);
|
|
assert.equal(getCircuitName(context), "Neu");
|
|
assert.deepEqual(
|
|
new ProjectHistoryRepository(context.db).getState("project-1"),
|
|
changed.history
|
|
);
|
|
assert.equal(context.db.select().from(projectRevisions).all().length, 1);
|
|
assert.equal(context.db.select().from(projectChangeSets).all().length, 1);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches device-row insertion and its persisted inverse", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const inserted = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitDeviceRowInsertProjectCommand({
|
|
id: "row-2",
|
|
circuitId: "circuit-1",
|
|
linkedProjectDeviceId: null,
|
|
legacyConsumerId: null,
|
|
sortOrder: 20,
|
|
name: "Steckdose",
|
|
displayName: "Steckdose",
|
|
phaseType: "single_phase",
|
|
connectionKind: null,
|
|
costGroup: null,
|
|
category: null,
|
|
level: null,
|
|
roomId: null,
|
|
roomNumberSnapshot: null,
|
|
roomNameSnapshot: null,
|
|
quantity: 1,
|
|
powerPerUnit: 0.2,
|
|
simultaneityFactor: 1,
|
|
cosPhi: null,
|
|
remark: null,
|
|
overriddenFields: null,
|
|
}),
|
|
});
|
|
assert.equal(inserted.history.undoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, "row-2"))
|
|
.get()?.displayName,
|
|
"Steckdose"
|
|
);
|
|
|
|
const undone = createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.redoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, "row-2"))
|
|
.get(),
|
|
undefined
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches complete circuit insertion and its persisted inverse", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const existingCircuit = context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-1"))
|
|
.get();
|
|
assert.ok(existingCircuit);
|
|
const service = createService(context);
|
|
const inserted = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitInsertProjectCommand({
|
|
id: "circuit-new",
|
|
circuitListId: existingCircuit.circuitListId,
|
|
sectionId: existingCircuit.sectionId,
|
|
equipmentIdentifier: "-1F2",
|
|
displayName: "Reserve",
|
|
sortOrder: 20,
|
|
protectionType: null,
|
|
protectionRatedCurrent: null,
|
|
protectionCharacteristic: null,
|
|
cableType: null,
|
|
cableCrossSection: null,
|
|
cableLength: null,
|
|
rcdAssignment: null,
|
|
terminalDesignation: null,
|
|
voltage: 230,
|
|
controlRequirement: null,
|
|
status: null,
|
|
isReserve: true,
|
|
remark: null,
|
|
deviceRows: [],
|
|
}),
|
|
});
|
|
assert.equal(inserted.history.undoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-new"))
|
|
.get()?.equipmentIdentifier,
|
|
"-1F2"
|
|
);
|
|
|
|
const undone = createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.redoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-new"))
|
|
.get(),
|
|
undefined
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches deterministic device-row moves and their inverse", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const sourceCircuit = context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-1"))
|
|
.get();
|
|
assert.ok(sourceCircuit);
|
|
context.db
|
|
.insert(circuits)
|
|
.values({
|
|
id: "circuit-2",
|
|
circuitListId: sourceCircuit.circuitListId,
|
|
sectionId: sourceCircuit.sectionId,
|
|
equipmentIdentifier: "-1F2",
|
|
sortOrder: 20,
|
|
isReserve: 1,
|
|
})
|
|
.run();
|
|
|
|
const service = createService(context);
|
|
const moved = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitDeviceRowMoveProjectCommand([
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: "circuit-2",
|
|
targetSortOrder: 10,
|
|
},
|
|
]),
|
|
});
|
|
assert.equal(moved.history.undoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, "row-1"))
|
|
.get()?.circuitId,
|
|
"circuit-2"
|
|
);
|
|
|
|
createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, "row-1"))
|
|
.get()?.circuitId,
|
|
"circuit-1"
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches placeholder moves that create and remove a circuit", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const sourceCircuit = context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-1"))
|
|
.get();
|
|
assert.ok(sourceCircuit);
|
|
const targetCircuit = {
|
|
id: "circuit-placeholder-target",
|
|
circuitListId: sourceCircuit.circuitListId,
|
|
sectionId: sourceCircuit.sectionId,
|
|
equipmentIdentifier: "-1F2",
|
|
displayName: "Neuer Stromkreis",
|
|
sortOrder: 20,
|
|
protectionType: null,
|
|
protectionRatedCurrent: null,
|
|
protectionCharacteristic: null,
|
|
cableType: null,
|
|
cableCrossSection: null,
|
|
cableLength: null,
|
|
rcdAssignment: null,
|
|
terminalDesignation: null,
|
|
voltage: null,
|
|
controlRequirement: null,
|
|
status: null,
|
|
isReserve: true,
|
|
remark: null,
|
|
deviceRows: [],
|
|
};
|
|
|
|
const moved = createService(context).executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command:
|
|
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
|
"create",
|
|
targetCircuit,
|
|
[
|
|
{
|
|
rowId: "row-1",
|
|
expectedCircuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetCircuitId: targetCircuit.id,
|
|
targetSortOrder: 10,
|
|
},
|
|
]
|
|
),
|
|
});
|
|
assert.equal(moved.history.undoDepth, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, "row-1"))
|
|
.get()?.circuitId,
|
|
targetCircuit.id
|
|
);
|
|
|
|
createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(
|
|
context.db
|
|
.select({ id: circuits.id })
|
|
.from(circuits)
|
|
.where(eq(circuits.id, targetCircuit.id))
|
|
.get(),
|
|
undefined
|
|
);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, "row-1"))
|
|
.get()?.circuitId,
|
|
"circuit-1"
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches complete section reorders without changing identifiers", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const firstCircuit = context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-1"))
|
|
.get();
|
|
assert.ok(firstCircuit);
|
|
context.db
|
|
.insert(circuits)
|
|
.values({
|
|
id: "circuit-2",
|
|
circuitListId: firstCircuit.circuitListId,
|
|
sectionId: firstCircuit.sectionId,
|
|
equipmentIdentifier: "-1F2",
|
|
sortOrder: 20,
|
|
isReserve: 1,
|
|
})
|
|
.run();
|
|
|
|
const reordered = createService(context).executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitSectionReorderProjectCommand(
|
|
firstCircuit.sectionId,
|
|
[
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetSortOrder: 20,
|
|
},
|
|
{
|
|
circuitId: "circuit-2",
|
|
expectedSortOrder: 20,
|
|
targetSortOrder: 10,
|
|
},
|
|
]
|
|
),
|
|
});
|
|
assert.equal(reordered.history.undoDepth, 1);
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({
|
|
id: circuits.id,
|
|
equipmentIdentifier: circuits.equipmentIdentifier,
|
|
sortOrder: circuits.sortOrder,
|
|
})
|
|
.from(circuits)
|
|
.all()
|
|
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
[
|
|
{
|
|
id: "circuit-1",
|
|
equipmentIdentifier: "-1F1",
|
|
sortOrder: 20,
|
|
},
|
|
{
|
|
id: "circuit-2",
|
|
equipmentIdentifier: "-1F2",
|
|
sortOrder: 10,
|
|
},
|
|
]
|
|
);
|
|
|
|
createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({ id: circuits.id, sortOrder: circuits.sortOrder })
|
|
.from(circuits)
|
|
.all()
|
|
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
[
|
|
{ id: "circuit-1", sortOrder: 10 },
|
|
{ id: "circuit-2", sortOrder: 20 },
|
|
]
|
|
);
|
|
|
|
const multiSection = createService(context).executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 2,
|
|
command: createCircuitSectionsReorderProjectCommand([
|
|
{
|
|
sectionId: firstCircuit.sectionId,
|
|
assignments:
|
|
createCircuitSectionReorderProjectCommand(
|
|
firstCircuit.sectionId,
|
|
[
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedSortOrder: 10,
|
|
targetSortOrder: 20,
|
|
},
|
|
{
|
|
circuitId: "circuit-2",
|
|
expectedSortOrder: 20,
|
|
targetSortOrder: 10,
|
|
},
|
|
]
|
|
).payload.assignments,
|
|
},
|
|
]),
|
|
});
|
|
assert.equal(multiSection.history.currentRevision, 3);
|
|
assert.equal(multiSection.history.redoDepth, 0);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches explicit section renumbering and its inverse", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const firstCircuit = context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-1"))
|
|
.get();
|
|
assert.ok(firstCircuit);
|
|
context.db
|
|
.insert(circuits)
|
|
.values({
|
|
id: "circuit-2",
|
|
circuitListId: firstCircuit.circuitListId,
|
|
sectionId: firstCircuit.sectionId,
|
|
equipmentIdentifier: "-1F2",
|
|
sortOrder: 20,
|
|
isReserve: 1,
|
|
})
|
|
.run();
|
|
|
|
const renumbered = createService(context).executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitSectionRenumberProjectCommand(
|
|
firstCircuit.sectionId,
|
|
[
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedEquipmentIdentifier: "-1F1",
|
|
targetEquipmentIdentifier: "-1F2",
|
|
},
|
|
{
|
|
circuitId: "circuit-2",
|
|
expectedEquipmentIdentifier: "-1F2",
|
|
targetEquipmentIdentifier: "-1F1",
|
|
},
|
|
]
|
|
),
|
|
});
|
|
assert.equal(renumbered.history.undoDepth, 1);
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({
|
|
id: circuits.id,
|
|
equipmentIdentifier: circuits.equipmentIdentifier,
|
|
sortOrder: circuits.sortOrder,
|
|
})
|
|
.from(circuits)
|
|
.all()
|
|
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
[
|
|
{
|
|
id: "circuit-1",
|
|
equipmentIdentifier: "-1F2",
|
|
sortOrder: 10,
|
|
},
|
|
{
|
|
id: "circuit-2",
|
|
equipmentIdentifier: "-1F1",
|
|
sortOrder: 20,
|
|
},
|
|
]
|
|
);
|
|
|
|
createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({
|
|
id: circuits.id,
|
|
equipmentIdentifier: circuits.equipmentIdentifier,
|
|
})
|
|
.from(circuits)
|
|
.all()
|
|
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
[
|
|
{
|
|
id: "circuit-1",
|
|
equipmentIdentifier: "-1F1",
|
|
},
|
|
{
|
|
id: "circuit-2",
|
|
equipmentIdentifier: "-1F2",
|
|
},
|
|
]
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("rejects unavailable history directions without writing a revision", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
assert.throws(
|
|
() =>
|
|
service.undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
}),
|
|
(error) =>
|
|
error instanceof ProjectHistoryOperationUnavailableError &&
|
|
error.direction === "undo"
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
service.redo({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
}),
|
|
(error) =>
|
|
error instanceof ProjectHistoryOperationUnavailableError &&
|
|
error.direction === "redo"
|
|
);
|
|
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches project settings updates through persistent history", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const updated = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createProjectSettingsUpdateProjectCommand({
|
|
name: "Test project",
|
|
internalProjectNumber: null,
|
|
externalProjectNumber: null,
|
|
buildingOwner: null,
|
|
description: null,
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
}),
|
|
});
|
|
assert.equal(updated.history.currentRevision, 1);
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({
|
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
|
})
|
|
.from(projects)
|
|
.where(eq(projects.id, "project-1"))
|
|
.get(),
|
|
{
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
}
|
|
);
|
|
|
|
const undone = service.undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.currentRevision, 2);
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({
|
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
|
})
|
|
.from(projects)
|
|
.where(eq(projects.id, "project-1"))
|
|
.get(),
|
|
{
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
}
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches distribution-board setup and its persisted inverse", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const structure = createDistributionBoardStructureSnapshot(
|
|
"project-1",
|
|
"UV-02"
|
|
);
|
|
const created = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command:
|
|
createDistributionBoardInsertProjectCommand(structure),
|
|
});
|
|
assert.equal(created.history.currentRevision, 1);
|
|
assert.ok(
|
|
context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(
|
|
eq(
|
|
circuitSections.circuitListId,
|
|
structure.circuitList.id
|
|
)
|
|
)
|
|
.get()
|
|
);
|
|
|
|
const undone = createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.currentRevision, 2);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(
|
|
eq(
|
|
circuitSections.circuitListId,
|
|
structure.circuitList.id
|
|
)
|
|
)
|
|
.all().length,
|
|
0
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches floor and room setup with their persisted inverses", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const floor = createProjectFloorSnapshot(
|
|
"project-1",
|
|
"EG",
|
|
0
|
|
);
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createProjectFloorInsertProjectCommand(floor),
|
|
});
|
|
const room = createProjectRoomSnapshot("project-1", {
|
|
floorId: floor.id,
|
|
roomNumber: "001",
|
|
roomName: "Technik",
|
|
});
|
|
const created = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
command: createProjectRoomInsertProjectCommand(room),
|
|
});
|
|
assert.equal(created.history.currentRevision, 2);
|
|
assert.ok(
|
|
context.db
|
|
.select()
|
|
.from(rooms)
|
|
.where(eq(rooms.id, room.id))
|
|
.get()
|
|
);
|
|
|
|
createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 2,
|
|
});
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(rooms)
|
|
.where(eq(rooms.id, room.id))
|
|
.get(),
|
|
undefined
|
|
);
|
|
createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 3,
|
|
});
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(floors)
|
|
.where(eq(floors.id, floor.id))
|
|
.get(),
|
|
undefined
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("rejects unsupported and malformed commands before domain writes", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
assert.throws(
|
|
() =>
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: {
|
|
schemaVersion: 1,
|
|
type: "unknown.command",
|
|
payload: {},
|
|
},
|
|
}),
|
|
/Unsupported project command type/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: {
|
|
schemaVersion: 1,
|
|
type: "circuit.update",
|
|
payload: { circuitId: "circuit-1", changes: [] },
|
|
},
|
|
}),
|
|
/at least one change/
|
|
);
|
|
assert.equal(getCircuitName(context), "Bestand");
|
|
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
});
|