Include components in project snapshots
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
Reference in New Issue
Block a user