forked from jappel/leistungsbilanz-ts
Editing "Anzahl" on a row with linked Revit objects set manualQuantity to the new total, so the row no longer matched its objects and detaching them failed with "row.manualQuantity must not exceed row.quantity". Such rows now lock "Anzahl" in the editor. The new optional columns "Anz. manuell" (editable) and "Anz. Revit" (read-only) show both shares; stored column layouts get them hidden next to "Anzahl". The device-row update derives the total from a manual-only edit and rejects any change that breaks quantity = manual + Revit, for every source. Two history fixtures inserted rows with quantity 1 and the column default manualQuantity 0, a state no runtime path creates; they now insert 1/1. Undoing "Revit-Objekte lösen" re-validated planning-value conflicts that the user had confirmed on the original assignment and was rejected. Undo/redo of row assignments now restores the accepted state without a new confirmation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1782 lines
57 KiB
TypeScript
1782 lines
57 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 { CircuitProtectionProjectCommandRepository } from "../src/db/repositories/circuit-protection-project-command.repository.js";
|
|
import { CircuitGroupStructureProjectCommandRepository } from "../src/db/repositories/circuit-group-structure-project-command.repository.js";
|
|
import { CircuitGroupRenumberProjectCommandRepository } from "../src/db/repositories/circuit-group-renumber-project-command.repository.js";
|
|
import { CircuitGroupMoveProjectCommandRepository } from "../src/db/repositories/circuit-group-move-project-command.repository.js";
|
|
import { CircuitGroupSubtreeProjectCommandRepository } from "../src/db/repositories/circuit-group-subtree-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 { DistributionBoardSubtreeProjectCommandRepository } from "../src/db/repositories/distribution-board-subtree-project-command.repository.js";
|
|
import { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-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 { ExternalModelConfigurationProjectCommandRepository } from "../src/db/repositories/external-model-configuration-project-command.repository.js";
|
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
|
import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js";
|
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
|
import { circuits } from "../src/db/schema/circuits.js";
|
|
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js";
|
|
import { distributionBoards } from "../src/db/schema/distribution-boards.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 { buildings } from "../src/db/schema/buildings.js";
|
|
import { floors } from "../src/db/schema/floors.js";
|
|
import { rooms } from "../src/db/schema/rooms.js";
|
|
import { externalModelConfigurations } from "../src/db/schema/external-model-configurations.js";
|
|
import { createCircuitProtectionUpdateProjectCommand } from "../src/domain/models/circuit-protection-project-command.model.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 {
|
|
createCircuitGroupInsertProjectCommand,
|
|
createCircuitGroupReorderProjectCommand,
|
|
createCircuitGroupUpdateProjectCommand,
|
|
} from "../src/domain/models/circuit-group-structure-project-command.model.js";
|
|
import { createCircuitGroupRenumberProjectCommand } from "../src/domain/models/circuit-group-renumber-project-command.model.js";
|
|
import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.js";
|
|
import { createCircuitGroupMoveProjectCommand } from "../src/domain/models/circuit-group-move-project-command.model.js";
|
|
import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js";
|
|
import { createCircuitGroupDeleteSubtreeProjectCommand } from "../src/domain/models/circuit-group-subtree-project-command.model.js";
|
|
import {
|
|
createDistributionBoardInsertProjectCommand,
|
|
createDistributionBoardStructureSnapshot,
|
|
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
|
import { createDistributionBoardDeleteSubtreeProjectCommand } from "../src/domain/models/distribution-board-subtree-project-command.model.js";
|
|
import {
|
|
createDistributionBoardComponentInsertProjectCommand,
|
|
createDistributionBoardComponentUpdateProjectCommand,
|
|
} from "../src/domain/models/distribution-board-component-structure-project-command.model.js";
|
|
import {
|
|
createProjectBuildingInsertProjectCommand,
|
|
createProjectBuildingSnapshot,
|
|
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 { createExternalModelConfigurationUpdateProjectCommand } from "../src/domain/models/external-model-configuration-project-command.model.js";
|
|
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.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,
|
|
manualQuantity: 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 DistributionBoardSubtreeProjectCommandRepository(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 DistributionBoardComponentStructureProjectCommandRepository(
|
|
context.db
|
|
),
|
|
new CircuitGroupStructureProjectCommandRepository(context.db),
|
|
new CircuitGroupRenumberProjectCommandRepository(context.db),
|
|
new CircuitGroupMoveProjectCommandRepository(context.db),
|
|
new CircuitGroupSubtreeProjectCommandRepository(context.db),
|
|
new CircuitProtectionProjectCommandRepository(context.db),
|
|
new ProjectHistoryRepository(context.db),
|
|
new ExternalModelConfigurationProjectCommandRepository(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 populated distribution-board subtree deletion through history", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const repository =
|
|
new DistributionBoardSubtreeProjectCommandRepository(context.db);
|
|
const board = context.db.select().from(distributionBoards).get();
|
|
assert.ok(board);
|
|
const snapshot = repository.capture("project-1", board.id);
|
|
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command:
|
|
createDistributionBoardDeleteSubtreeProjectCommand(snapshot),
|
|
});
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(distributionBoards)
|
|
.where(eq(distributionBoards.id, board.id))
|
|
.get(),
|
|
undefined
|
|
);
|
|
|
|
service.undo({ projectId: "project-1", expectedRevision: 1 });
|
|
assert.deepEqual(repository.capture("project-1", board.id), snapshot);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches circuit-protection updates through project history", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const protection = {
|
|
circuitId: "circuit-1",
|
|
type: "LS" as const,
|
|
ratedCurrentA: 10,
|
|
fuseUtilizationCategory: null,
|
|
tripCharacteristic: "B" as const,
|
|
rcdType: null,
|
|
ratedResidualCurrentMa: null,
|
|
};
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitProtectionUpdateProjectCommand(
|
|
"circuit-1",
|
|
null,
|
|
protection
|
|
),
|
|
});
|
|
assert.deepEqual(
|
|
context.db.select().from(circuitProtectionDevices).get(),
|
|
protection
|
|
);
|
|
service.undo({ projectId: "project-1", expectedRevision: 1 });
|
|
assert.equal(
|
|
context.db.select().from(circuitProtectionDevices).get(),
|
|
undefined
|
|
);
|
|
service.redo({ projectId: "project-1", expectedRevision: 2 });
|
|
assert.deepEqual(
|
|
context.db.select().from(circuitProtectionDevices).get(),
|
|
protection
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
isPublicBuilding: true,
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
siteAddress: null,
|
|
workStage: null,
|
|
projectLead: null,
|
|
projectEditor: null,
|
|
}),
|
|
});
|
|
assert.equal(updated.history.currentRevision, 1);
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({
|
|
isPublicBuilding: projects.isPublicBuilding,
|
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
|
})
|
|
.from(projects)
|
|
.where(eq(projects.id, "project-1"))
|
|
.get(),
|
|
{
|
|
isPublicBuilding: true,
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
}
|
|
);
|
|
|
|
const undone = service.undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.currentRevision, 2);
|
|
assert.deepEqual(
|
|
context.db
|
|
.select({
|
|
isPublicBuilding: projects.isPublicBuilding,
|
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
|
})
|
|
.from(projects)
|
|
.where(eq(projects.id, "project-1"))
|
|
.get(),
|
|
{
|
|
isPublicBuilding: false,
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
}
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches external XLSX configuration updates through persistent history", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const target = {
|
|
id: "external-config-1",
|
|
projectId: "project-1",
|
|
configuration: createDefaultExternalModelConfiguration(),
|
|
};
|
|
const updated = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createExternalModelConfigurationUpdateProjectCommand(null, target),
|
|
});
|
|
assert.equal(updated.history.currentRevision, 1);
|
|
assert.equal(context.db.select().from(externalModelConfigurations).all().length, 1);
|
|
|
|
const undone = service.undo({ projectId: "project-1", expectedRevision: 1 });
|
|
assert.equal(undone.history.currentRevision, 2);
|
|
assert.equal(context.db.select().from(externalModelConfigurations).all().length, 0);
|
|
|
|
const redone = service.redo({ projectId: "project-1", expectedRevision: 2 });
|
|
assert.equal(redone.history.currentRevision, 3);
|
|
assert.equal(context.db.select().from(externalModelConfigurations).all().length, 1);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("rejects legacy CSV commands stored in project history with a clear error", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createExternalModelConfigurationUpdateProjectCommand(null, {
|
|
id: "external-config-1",
|
|
projectId: "project-1",
|
|
configuration: createDefaultExternalModelConfiguration(),
|
|
}),
|
|
});
|
|
context.db.update(projectChangeSets).set({
|
|
inversePayloadJson: JSON.stringify({
|
|
schemaVersion: 1,
|
|
type: "external-import.apply-initial",
|
|
payload: {},
|
|
}),
|
|
}).run();
|
|
|
|
assert.throws(
|
|
() => service.undo({ projectId: "project-1", expectedRevision: 1 }),
|
|
/alten Revit-CSV-Anbindung/
|
|
);
|
|
assert.equal(context.db.select().from(externalModelConfigurations).all().length, 1);
|
|
assert.equal(context.db.select({ revision: projects.currentRevision }).from(projects).get()?.revision, 1);
|
|
} 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 distribution-board component insertion and deletion", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const created = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command:
|
|
createDistributionBoardComponentInsertProjectCommand({
|
|
component: {
|
|
id: "component-service",
|
|
circuitListId: context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.get()!.circuitListId,
|
|
sectionId: null,
|
|
equipmentIdentifier: "-K10",
|
|
name: "KNX Aktor",
|
|
role: "auxiliary",
|
|
placement: "footer",
|
|
sortOrder: 10,
|
|
},
|
|
protectionDevice: null,
|
|
}),
|
|
});
|
|
assert.equal(created.history.currentRevision, 1);
|
|
assert.ok(
|
|
context.db
|
|
.select()
|
|
.from(distributionBoardComponents)
|
|
.where(
|
|
eq(distributionBoardComponents.id, "component-service")
|
|
)
|
|
.get()
|
|
);
|
|
|
|
const undone = createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(undone.history.currentRevision, 2);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(distributionBoardComponents)
|
|
.where(
|
|
eq(distributionBoardComponents.id, "component-service")
|
|
)
|
|
.get(),
|
|
undefined
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches distribution-board component updates", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const circuitListId = context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.get()!.circuitListId;
|
|
const expected = {
|
|
component: {
|
|
id: "component-service-update",
|
|
circuitListId,
|
|
sectionId: null,
|
|
equipmentIdentifier: "-K11",
|
|
name: "KNX Aktor",
|
|
role: "auxiliary" as const,
|
|
placement: "footer" as const,
|
|
sortOrder: 10,
|
|
},
|
|
protectionDevice: null,
|
|
};
|
|
const target = {
|
|
component: {
|
|
...expected.component,
|
|
name: "KNX Jalousieaktor",
|
|
sortOrder: 20,
|
|
},
|
|
protectionDevice: null,
|
|
};
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command:
|
|
createDistributionBoardComponentInsertProjectCommand(
|
|
expected
|
|
),
|
|
});
|
|
const updated = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
command:
|
|
createDistributionBoardComponentUpdateProjectCommand(
|
|
expected,
|
|
target
|
|
),
|
|
});
|
|
assert.equal(updated.history.currentRevision, 2);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(distributionBoardComponents)
|
|
.where(
|
|
eq(
|
|
distributionBoardComponents.id,
|
|
expected.component.id
|
|
)
|
|
)
|
|
.get()?.name,
|
|
target.component.name
|
|
);
|
|
|
|
createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 2,
|
|
});
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(distributionBoardComponents)
|
|
.where(
|
|
eq(
|
|
distributionBoardComponents.id,
|
|
expected.component.id
|
|
)
|
|
)
|
|
.get()?.name,
|
|
expected.component.name
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches circuit-group structure commands", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const circuitListId = context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.get()!.circuitListId;
|
|
const expected = {
|
|
id: "group-service",
|
|
circuitListId,
|
|
key: "lighting_2",
|
|
displayName: "Beleuchtung 2",
|
|
prefix: "-1F2.",
|
|
sortOrder: 40,
|
|
category: "lighting" as const,
|
|
groupNumber: 2,
|
|
};
|
|
const target = {
|
|
...expected,
|
|
displayName: "Beleuchtung Nebenräume",
|
|
};
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitGroupInsertProjectCommand(expected),
|
|
});
|
|
const updated = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
command: createCircuitGroupUpdateProjectCommand(
|
|
expected,
|
|
target
|
|
),
|
|
});
|
|
assert.equal(updated.history.currentRevision, 2);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.id, expected.id))
|
|
.get()?.displayName,
|
|
target.displayName
|
|
);
|
|
|
|
createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 2,
|
|
});
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.id, expected.id))
|
|
.get()?.displayName,
|
|
expected.displayName
|
|
);
|
|
|
|
const groups = context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.circuitListId, circuitListId))
|
|
.all();
|
|
const reorderAssignments = groups.map((group, index) => ({
|
|
groupId: group.id,
|
|
expectedSortOrder: group.sortOrder,
|
|
targetSortOrder: (groups.length - index) * 10,
|
|
}));
|
|
const reordered = createService(context).executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 3,
|
|
command: createCircuitGroupReorderProjectCommand(
|
|
circuitListId,
|
|
reorderAssignments
|
|
),
|
|
});
|
|
assert.equal(reordered.history.currentRevision, 4);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches explicit circuit-group renumbering", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const existing = context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.category, "three_phase"))
|
|
.get()!;
|
|
const added = {
|
|
id: "group-three-phase-2",
|
|
circuitListId: existing.circuitListId,
|
|
key: "three_phase_2",
|
|
displayName: "3-phasig 2",
|
|
prefix: "-3F2.",
|
|
sortOrder: 40,
|
|
category: "three_phase" as const,
|
|
groupNumber: 2,
|
|
};
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitGroupInsertProjectCommand(added),
|
|
});
|
|
const plan = createCircuitGroupRenumberPlan(
|
|
[
|
|
{
|
|
id: existing.id,
|
|
category: "three_phase",
|
|
groupNumber: 1,
|
|
prefix: "-3F1.",
|
|
circuits: [],
|
|
components: [],
|
|
},
|
|
{
|
|
id: added.id,
|
|
category: "three_phase",
|
|
groupNumber: 2,
|
|
prefix: "-3F2.",
|
|
circuits: [],
|
|
components: [],
|
|
},
|
|
],
|
|
[
|
|
{ groupId: existing.id, targetGroupNumber: 2 },
|
|
{ groupId: added.id, targetGroupNumber: 1 },
|
|
]
|
|
);
|
|
const result = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
command: createCircuitGroupRenumberProjectCommand(
|
|
existing.circuitListId,
|
|
plan
|
|
),
|
|
});
|
|
assert.equal(result.history.currentRevision, 2);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.id, existing.id))
|
|
.get()?.prefix,
|
|
"-3F2."
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches same-category circuit group moves", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const source = context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.category, "three_phase"))
|
|
.get()!;
|
|
const target = {
|
|
id: "group-move-service-target",
|
|
circuitListId: source.circuitListId,
|
|
key: "three_phase_2",
|
|
displayName: "3-phasig 2",
|
|
prefix: "-3F2.",
|
|
sortOrder: 40,
|
|
category: "three_phase" as const,
|
|
groupNumber: 2,
|
|
};
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createCircuitGroupInsertProjectCommand(target),
|
|
});
|
|
context.db
|
|
.insert(circuits)
|
|
.values({
|
|
id: "circuit-group-move-service",
|
|
circuitListId: source.circuitListId,
|
|
sectionId: source.id,
|
|
equipmentIdentifier: "-3F1.1",
|
|
displayName: "Drehstrom",
|
|
sortOrder: 10,
|
|
})
|
|
.run();
|
|
const result = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
command: createCircuitGroupMoveProjectCommand(
|
|
createCircuitGroupMovePlan({
|
|
circuit: {
|
|
id: "circuit-group-move-service",
|
|
circuitListId: source.circuitListId,
|
|
sectionId: source.id,
|
|
equipmentIdentifier: "-3F1.1",
|
|
sortOrder: 10,
|
|
},
|
|
sourceGroup: {
|
|
id: source.id,
|
|
category: "three_phase",
|
|
groupNumber: 1,
|
|
prefix: "-3F1.",
|
|
},
|
|
targetGroup: target,
|
|
targetCircuitEquipmentIdentifiers: [],
|
|
targetSortOrder: 20,
|
|
})
|
|
),
|
|
});
|
|
assert.equal(result.history.currentRevision, 2);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-group-move-service"))
|
|
.get()?.equipmentIdentifier,
|
|
"-3F2.1"
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches populated circuit-group deletion and restoration", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const repository =
|
|
new CircuitGroupSubtreeProjectCommandRepository(context.db);
|
|
const groupId = context.db
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.id, "circuit-1"))
|
|
.get()!.sectionId;
|
|
const snapshot = repository.capture("project-1", groupId);
|
|
const deleted = createService(context).executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command:
|
|
createCircuitGroupDeleteSubtreeProjectCommand(snapshot),
|
|
});
|
|
assert.equal(deleted.history.currentRevision, 1);
|
|
assert.equal(
|
|
context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.id, groupId))
|
|
.get(),
|
|
undefined
|
|
);
|
|
|
|
const restored = createService(context).undo({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
});
|
|
assert.equal(restored.history.currentRevision, 2);
|
|
assert.deepEqual(
|
|
repository.capture("project-1", groupId),
|
|
snapshot
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("dispatches building commands and floor assignments with persisted inverses", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const service = createService(context);
|
|
const building = createProjectBuildingSnapshot("project-1", "Haus A", 0);
|
|
service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
command: createProjectBuildingInsertProjectCommand(building),
|
|
});
|
|
const floor = createProjectFloorSnapshot("project-1", "EG", 0, building.id);
|
|
const created = service.executeUser({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
command: createProjectFloorInsertProjectCommand(floor),
|
|
});
|
|
assert.equal(created.history.currentRevision, 2);
|
|
assert.equal(
|
|
context.db.select().from(floors).where(eq(floors.id, floor.id)).get()
|
|
?.buildingId,
|
|
building.id
|
|
);
|
|
|
|
createService(context).undo({ projectId: "project-1", expectedRevision: 2 });
|
|
createService(context).undo({ projectId: "project-1", expectedRevision: 3 });
|
|
assert.equal(context.db.select().from(buildings).all().length, 0);
|
|
createService(context).redo({ projectId: "project-1", expectedRevision: 4 });
|
|
createService(context).redo({ projectId: "project-1", expectedRevision: 5 });
|
|
assert.deepEqual(
|
|
context.db.select().from(floors).where(eq(floors.id, floor.id)).get(),
|
|
floor
|
|
);
|
|
} 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();
|
|
}
|
|
});
|
|
});
|