Include components in project snapshots

This commit is contained in:
2026-07-30 19:24:28 +02:00
parent e7607c90c4
commit 012308984d
13 changed files with 915 additions and 46 deletions
+73
View File
@@ -14,8 +14,11 @@ import { ProjectTransferRepository } from "../src/db/repositories/project-transf
import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js";
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
import { circuitLists } from "../src/db/schema/circuit-lists.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 { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js";
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js";
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
import { floors } from "../src/db/schema/floors.js";
import { projectDevices } from "../src/db/schema/project-devices.js";
@@ -129,9 +132,58 @@ function createTestDatabase(): DatabaseContext {
cosPhi: 0.9,
})
.run();
context.db
.insert(circuitProtectionDevices)
.values({
circuitId: "circuit-1",
type: "LS",
ratedCurrentA: 10,
fuseUtilizationCategory: null,
tripCharacteristic: "B",
rcdType: null,
ratedResidualCurrentMa: null,
})
.run();
context.db
.insert(distributionBoardComponents)
.values({
id: "component-1",
circuitListId: list.id,
sectionId: section.id,
equipmentIdentifier: "-1Q1.0",
name: "Gruppen-FI",
role: "group_residual_current_protection",
placement: "group",
sortOrder: 10,
})
.run();
context.db
.insert(distributionBoardComponentProtectionDevices)
.values({
componentId: "component-1",
type: "FI",
ratedCurrentA: 40,
fuseUtilizationCategory: null,
tripCharacteristic: null,
rcdType: "A",
ratedResidualCurrentMa: 30,
})
.run();
return context;
}
function removeVersionSevenFields(state: Record<string, unknown>) {
delete state.distributionBoardComponents;
delete state.circuitProtectionDevices;
delete state.distributionBoardComponentProtectionDevices;
for (const section of state.circuitSections as Array<
Record<string, unknown>
>) {
delete section.category;
delete section.groupNumber;
}
}
describe("project snapshot repository", () => {
it("captures, hashes and lists a complete named logical snapshot", () => {
const context = createTestDatabase();
@@ -190,6 +242,12 @@ describe("project snapshot repository", () => {
context.db.select().from(circuitSections).all().length
);
assert.equal(state.circuits.length, 1);
assert.equal(state.circuitProtectionDevices.length, 1);
assert.equal(state.distributionBoardComponents.length, 1);
assert.equal(
state.distributionBoardComponentProtectionDevices.length,
1
);
assert.equal(state.circuits[0].deviceRows.length, 1);
assert.equal(
state.circuits[0].deviceRows[0].linkedProjectDeviceId,
@@ -291,6 +349,7 @@ describe("project snapshot repository", () => {
project: Record<string, unknown>;
};
currentPayload.schemaVersion = 1;
removeVersionSevenFields(currentPayload);
delete currentPayload.project.internalProjectNumber;
delete currentPayload.project.externalProjectNumber;
delete currentPayload.project.buildingOwner;
@@ -368,6 +427,19 @@ describe("portable project transfer", () => {
copied.state.circuits[0].id,
source.state.circuits[0].id
);
assert.notEqual(
copied.state.distributionBoardComponents[0].id,
source.state.distributionBoardComponents[0].id
);
assert.equal(
copied.state.circuitProtectionDevices[0].circuitId,
copied.state.circuits[0].id
);
assert.equal(
copied.state.distributionBoardComponentProtectionDevices[0]
.componentId,
copied.state.distributionBoardComponents[0].id
);
assert.equal(
copied.state.circuits[0].deviceRows[0].linkedProjectDeviceId,
copied.state.projectDevices[0].id
@@ -438,6 +510,7 @@ describe("portable project transfer", () => {
distributionBoards: Array<Record<string, unknown>>;
};
legacyState.schemaVersion = 2;
removeVersionSevenFields(legacyState);
delete (
legacyState.project as Record<string, unknown>
).enabledDistributionBoardSupplyTypes;
@@ -27,9 +27,12 @@ import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/pro
import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/project-settings-project-command.repository.js";
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
import { circuitLists } from "../src/db/schema/circuit-lists.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 { consumers } from "../src/db/schema/consumers.js";
import { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js";
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js";
import { floors } from "../src/db/schema/floors.js";
import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js";
import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js";
@@ -132,6 +135,43 @@ function createTestDatabase(): DatabaseContext {
simultaneityFactor: 0.8,
})
.run();
context.db
.insert(circuitProtectionDevices)
.values({
circuitId: "circuit-1",
type: "LS",
ratedCurrentA: 10,
fuseUtilizationCategory: null,
tripCharacteristic: "B",
rcdType: null,
ratedResidualCurrentMa: null,
})
.run();
context.db
.insert(distributionBoardComponents)
.values({
id: "component-1",
circuitListId: list.id,
sectionId: section.id,
equipmentIdentifier: "-1Q1.0",
name: "Gruppen-FI",
role: "group_residual_current_protection",
placement: "group",
sortOrder: 10,
})
.run();
context.db
.insert(distributionBoardComponentProtectionDevices)
.values({
componentId: "component-1",
type: "FI",
ratedCurrentA: 40,
fuseUtilizationCategory: null,
tripCharacteristic: null,
rcdType: "A",
ratedResidualCurrentMa: 30,
})
.run();
return context;
}
@@ -173,6 +213,16 @@ function changeCompleteProjectState(context: DatabaseContext) {
})
.where(eq(circuits.id, "circuit-1"))
.run();
context.db
.update(circuitProtectionDevices)
.set({ ratedCurrentA: 16 })
.where(eq(circuitProtectionDevices.circuitId, "circuit-1"))
.run();
context.db
.update(distributionBoardComponents)
.set({ name: "Geänderter Gruppen-FI" })
.where(eq(distributionBoardComponents.id, "component-1"))
.run();
context.db
.insert(floors)
.values({
+223 -5
View File
@@ -32,6 +32,9 @@ function minimalSnapshot() {
distributionBoards: [],
circuitLists: [],
circuitSections: [],
distributionBoardComponents: [],
circuitProtectionDevices: [],
distributionBoardComponentProtectionDevices: [],
circuits: [],
projectDevices: [],
floors: [],
@@ -39,6 +42,111 @@ function minimalSnapshot() {
};
}
function legacyMinimalSnapshot() {
const {
distributionBoardComponents: _distributionBoardComponents,
circuitProtectionDevices: _circuitProtectionDevices,
distributionBoardComponentProtectionDevices:
_distributionBoardComponentProtectionDevices,
...snapshot
} = minimalSnapshot();
return snapshot;
}
function protectedSnapshot() {
return {
...minimalSnapshot(),
distributionBoards: [
{
id: "board-1",
projectId: "project-1",
name: "UV 1",
floorId: null,
supplyType: null,
simultaneityFactor: 1,
},
],
circuitLists: [
{
id: "list-1",
projectId: "project-1",
distributionBoardId: "board-1",
name: "UV 1 Stromkreisliste",
},
],
circuitSections: [
{
id: "section-1",
circuitListId: "list-1",
key: "lighting",
displayName: "Beleuchtung 1",
prefix: "-1F1.",
sortOrder: 10,
category: "lighting" as const,
groupNumber: 1,
},
],
circuits: [
{
id: "circuit-1",
circuitListId: "list-1",
sectionId: "section-1",
equipmentIdentifier: "-1F1.1",
displayName: "Reserve",
sortOrder: 10,
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: [],
},
],
distributionBoardComponents: [
{
id: "component-1",
circuitListId: "list-1",
sectionId: "section-1",
equipmentIdentifier: "-1Q1.0",
name: "Gruppen-FI",
role: "group_residual_current_protection" as const,
placement: "group" as const,
sortOrder: 10,
},
],
circuitProtectionDevices: [
{
circuitId: "circuit-1",
type: "LS" as const,
ratedCurrentA: 10,
fuseUtilizationCategory: null,
tripCharacteristic: "B" as const,
rcdType: null,
ratedResidualCurrentMa: null,
},
],
distributionBoardComponentProtectionDevices: [
{
componentId: "component-1",
type: "FI" as const,
ratedCurrentA: 40,
fuseUtilizationCategory: null,
tripCharacteristic: null,
rcdType: "A" as const,
ratedResidualCurrentMa: 30,
},
],
};
}
describe("project state snapshot model", () => {
it("round-trips a versioned logical project state", () => {
const snapshot = parseProjectStateSnapshot(minimalSnapshot());
@@ -52,7 +160,7 @@ describe("project state snapshot model", () => {
it("upgrades version-one snapshots with empty project metadata", () => {
const legacy = {
...minimalSnapshot(),
...legacyMinimalSnapshot(),
schemaVersion: 1 as const,
project: {
id: "project-1",
@@ -82,7 +190,7 @@ describe("project state snapshot model", () => {
it("upgrades version-two distribution boards without inferred assignments", () => {
const previous = {
...minimalSnapshot(),
...legacyMinimalSnapshot(),
schemaVersion: 2 as const,
project: {
...minimalSnapshot().project,
@@ -110,7 +218,7 @@ describe("project state snapshot model", () => {
it("upgrades version-three snapshots with all project supply types", () => {
const previous = {
...minimalSnapshot(),
...legacyMinimalSnapshot(),
schemaVersion: 3 as const,
project: {
...minimalSnapshot().project,
@@ -131,7 +239,7 @@ describe("project state snapshot model", () => {
it("normalizes version-four device voltages to project settings", () => {
const previous = {
...minimalSnapshot(),
...legacyMinimalSnapshot(),
schemaVersion: 4 as const,
projectDevices: [
{
@@ -161,7 +269,7 @@ describe("project state snapshot model", () => {
it("upgrades version-five distribution boards with a neutral factor", () => {
const previous = {
...minimalSnapshot(),
...legacyMinimalSnapshot(),
schemaVersion: 5 as const,
distributionBoards: [
{
@@ -183,6 +291,116 @@ describe("project state snapshot model", () => {
);
});
it("upgrades version-six sections without inventing component data", () => {
const previous = {
...legacyMinimalSnapshot(),
schemaVersion: 6 as const,
circuitSections: [
{
id: "section-lighting",
circuitListId: "list-1",
key: "lighting",
displayName: "Beleuchtung",
prefix: "-1F",
sortOrder: 10,
},
{
id: "section-unassigned",
circuitListId: "list-1",
key: "unassigned",
displayName: "Nicht zugeordnet",
prefix: "-UF",
sortOrder: 90,
},
],
distributionBoards: [
{
id: "board-1",
projectId: "project-1",
name: "UV 1",
floorId: null,
supplyType: null,
simultaneityFactor: 1,
},
],
circuitLists: [
{
id: "list-1",
projectId: "project-1",
distributionBoardId: "board-1",
name: "UV 1 Stromkreisliste",
},
],
};
const snapshot = parseProjectStateSnapshot(previous);
assert.deepEqual(
snapshot.circuitSections.map(
({ key, category, groupNumber }) => ({
key,
category,
groupNumber,
})
),
[
{
key: "lighting",
category: "lighting",
groupNumber: 1,
},
{
key: "unassigned",
category: null,
groupNumber: null,
},
]
);
assert.deepEqual(snapshot.distributionBoardComponents, []);
assert.deepEqual(snapshot.circuitProtectionDevices, []);
assert.deepEqual(
snapshot.distributionBoardComponentProtectionDevices,
[]
);
});
it("validates the complete group, component and protection graph", () => {
const snapshot = parseProjectStateSnapshot(protectedSnapshot());
assert.equal(snapshot.circuitSections[0]?.groupNumber, 1);
assert.equal(
snapshot.distributionBoardComponents[0]?.sectionId,
"section-1"
);
assert.equal(
snapshot.circuitProtectionDevices[0]?.tripCharacteristic,
"B"
);
assert.equal(
snapshot.distributionBoardComponentProtectionDevices[0]
?.ratedResidualCurrentMa,
30
);
const duplicateBmk = protectedSnapshot();
duplicateBmk.distributionBoardComponents[0]!.equipmentIdentifier =
" -1f1.1 ";
assert.throws(
() => parseProjectStateSnapshot(duplicateBmk),
/duplicate equipment identifiers/
);
const invalidProtection = protectedSnapshot();
invalidProtection.circuitProtectionDevices[0] = {
...invalidProtection.circuitProtectionDevices[0]!,
ratedCurrentA: 35,
};
assert.throws(
() => parseProjectStateSnapshot(invalidProtection),
/protection-device configuration is invalid/
);
});
it("rejects duplicate ids and cross-project ownership", () => {
assert.throws(
() =>