Include components in project snapshots
This commit is contained in:
@@ -255,6 +255,11 @@ not change the project revision or undo/redo stacks. Restoring a server-stored
|
|||||||
snapshot verifies its checksum and the current-state hash, replaces supported
|
snapshot verifies its checksum and the current-state hash, replaces supported
|
||||||
project data atomically and records a new `restore` revision with a complete
|
project data atomically and records a new `restore` revision with a complete
|
||||||
inverse command. Restore can therefore be undone and redone after a restart.
|
inverse command. Restore can therefore be undone and redone after a restart.
|
||||||
|
Snapshot schema version 7 also contains circuit-group identity,
|
||||||
|
distribution-board components and the separate circuit/component protection
|
||||||
|
records. Versions 1 through 6 remain readable. Portable duplicate imports
|
||||||
|
remap component ids and protection-owner references together with the existing
|
||||||
|
project graph.
|
||||||
The central revision boundary creates an automatic logical snapshot after each
|
The central revision boundary creates an automatic logical snapshot after each
|
||||||
25 new revisions and retains only the newest 12 automatic snapshots per
|
25 new revisions and retains only the newest 12 automatic snapshots per
|
||||||
project. Named snapshots are never removed by this retention policy.
|
project. Named snapshots are never removed by this retention policy.
|
||||||
|
|||||||
@@ -261,8 +261,9 @@ mit SHA-256-Prüfsumme. `POST /api/projects/:projectId/import` prüft Format,
|
|||||||
Snapshot-Relationen und Prüfsumme vor jedem Schreibzugriff. Der Modus `replace`
|
Snapshot-Relationen und Prüfsumme vor jedem Schreibzugriff. Der Modus `replace`
|
||||||
läuft über `project.restore-state` und ist dadurch eine atomare, dauerhaft
|
läuft über `project.restore-state` und ist dadurch eine atomare, dauerhaft
|
||||||
rückgängig machbare Projektrevision. Der Modus `duplicate` ordnet Projekt-,
|
rückgängig machbare Projektrevision. Der Modus `duplicate` ordnet Projekt-,
|
||||||
Struktur-, Geräte-, Raum-, Stromkreis- und Gerätezeilen-UUIDs vollständig neu
|
Struktur-, Gruppen-, Verteilerkomponenten-, Raum-, Stromkreis- und
|
||||||
zu und legt die Kopie mit Revision `0` in einer Transaktion an. Upgrade-only-
|
Gerätezeilen-UUIDs sowie Schutzgeräte-Referenzen vollständig neu zu und legt
|
||||||
|
die Kopie mit Revision `0` in einer Transaktion an. Upgrade-only-
|
||||||
Consumer-Verweise werden nicht in die Kopie übernommen; fachliche Verknüpfungen
|
Consumer-Verweise werden nicht in die Kopie übernommen; fachliche Verknüpfungen
|
||||||
innerhalb des unterstützten Laufzeitmodells bleiben erhalten. Die
|
innerhalb des unterstützten Laufzeitmodells bleiben erhalten. Die
|
||||||
Projektübersicht verwendet dafür den separaten Collection-Endpunkt
|
Projektübersicht verwendet dafür den separaten Collection-Endpunkt
|
||||||
@@ -290,6 +291,13 @@ Snapshot-Schema 4 enthält bereits Etage und Netzart; Schema 1/2 sowie
|
|||||||
gespeicherte Version-1-Strukturcommands werden ohne erfundene Zuordnung
|
gespeicherte Version-1-Strukturcommands werden ohne erfundene Zuordnung
|
||||||
hochgestuft. Snapshot-Schema 3 wird mit allen sechs Netzarten als
|
hochgestuft. Snapshot-Schema 3 wird mit allen sechs Netzarten als
|
||||||
Projektauswahl hochgestuft.
|
Projektauswahl hochgestuft.
|
||||||
|
Snapshot-Schema 7 ergänzt Gruppenkategorie und -nummer,
|
||||||
|
Verteilerkomponenten sowie die getrennten 1:1-Schutzgerätedaten. Capture,
|
||||||
|
benannte und automatische Snapshots, Wiederherstellung, Undo/Redo und beide
|
||||||
|
JSON-Importmodi verwenden denselben vollständigen Zustand. Schema 6 und älter
|
||||||
|
bleiben lesbar: Die drei bekannten Abschnittsschlüssel werden deterministisch
|
||||||
|
Gruppe 1 zugeordnet, neue Komponenten- und Schutzgerätesammlungen bleiben leer
|
||||||
|
und bestehende flache Schutzangaben unverändert erhalten.
|
||||||
`project-floor.insert` und `project-room.insert` versionieren die Anlage von
|
`project-floor.insert` und `project-room.insert` versionieren die Anlage von
|
||||||
Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden
|
Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden
|
||||||
jeweils die persistierte Inverse für Undo/Redo. Ein Geschoss wird durch Undo nur
|
jeweils die persistierte Inverse für Undo/Redo. Ein Geschoss wird durch Undo nur
|
||||||
@@ -342,8 +350,9 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
|
|||||||
bisherigen flachen Schutzfelder bleiben in dieser Übergangsphase unverändert.
|
bisherigen flachen Schutzfelder bleiben in dieser Übergangsphase unverändert.
|
||||||
Ein triggergeführtes Register erzwingt bereits eine normalisierte,
|
Ein triggergeführtes Register erzwingt bereits eine normalisierte,
|
||||||
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
|
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
|
||||||
Verteilerkomponenten. Snapshot-, Transfer-, Command- und UI-Integration
|
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden
|
||||||
folgen in den nächsten abgegrenzten Arbeitspaketen.
|
Snapshot-Schema 7; Command- und UI-Integration folgen in den nächsten
|
||||||
|
abgegrenzten Arbeitspaketen.
|
||||||
|
|
||||||
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
||||||
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
BMK rules.
|
BMK rules.
|
||||||
- [x] Phase B1: additive relational schema, group backfill, component/protection
|
- [x] Phase B1: additive relational schema, group backfill, component/protection
|
||||||
tables and shared BMK uniqueness.
|
tables and shared BMK uniqueness.
|
||||||
- [ ] Phase B2: snapshot/transfer schema upgrade and deterministic compatibility
|
- [x] Phase B2: snapshot/transfer schema upgrade and deterministic compatibility
|
||||||
mapping.
|
mapping.
|
||||||
- [ ] Phase C: persistent commands and new-board defaults.
|
- [ ] Phase C: persistent commands and new-board defaults.
|
||||||
- [ ] Phase D: group numbering, moves and destructive operations.
|
- [ ] Phase D: group numbering, moves and destructive operations.
|
||||||
|
|||||||
@@ -545,8 +545,7 @@ Acceptance:
|
|||||||
|
|
||||||
### B. Persistence and Compatibility Model
|
### B. Persistence and Compatibility Model
|
||||||
|
|
||||||
Status: In progress. Relational foundation B1 is complete; logical
|
Status: Complete.
|
||||||
snapshot/transfer integration B2 is pending.
|
|
||||||
|
|
||||||
- add group category and group number
|
- add group category and group number
|
||||||
- add distribution-board component persistence
|
- add distribution-board component persistence
|
||||||
@@ -570,6 +569,21 @@ Implemented in B1:
|
|||||||
- newly created legacy-compatible board structures receive the same initial
|
- newly created legacy-compatible board structures receive the same initial
|
||||||
group identity while their persisted command format remains unchanged
|
group identity while their persisted command format remains unchanged
|
||||||
|
|
||||||
|
Implemented in B2:
|
||||||
|
|
||||||
|
- snapshot schema version 7 includes group identity, distribution-board
|
||||||
|
components and both one-to-one protection collections
|
||||||
|
- capture, named and automatic snapshots, restore, Undo/Redo and portable JSON
|
||||||
|
transfers preserve the complete new relational state
|
||||||
|
- duplicate imports remap component UUIDs and both protection-owner references
|
||||||
|
together with the existing project structure
|
||||||
|
- versions 1 through 6 remain readable; version 6 maps the three established
|
||||||
|
section keys to group 1 and leaves new collections empty
|
||||||
|
- older flat circuit protection values remain present and are not guessed into
|
||||||
|
strict new protection configurations
|
||||||
|
- current snapshots reject invalid ownership, duplicate group numbers,
|
||||||
|
cross-entity BMKs and invalid protection configurations before persistence
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
- a current database migrates without data loss
|
- a current database migrates without data loss
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
legacyProjectStateSnapshotSchemaVersion,
|
legacyProjectStateSnapshotSchemaVersion,
|
||||||
previousProjectStateSnapshotSchemaVersion,
|
previousProjectStateSnapshotSchemaVersion,
|
||||||
projectStateSnapshotSchemaVersion,
|
projectStateSnapshotSchemaVersion,
|
||||||
|
simultaneityFactorProjectStateSnapshotSchemaVersion,
|
||||||
supplyTypesProjectStateSnapshotSchemaVersion,
|
supplyTypesProjectStateSnapshotSchemaVersion,
|
||||||
voltageProjectStateSnapshotSchemaVersion,
|
voltageProjectStateSnapshotSchemaVersion,
|
||||||
} from "../../domain/models/project-state-snapshot.model.js";
|
} from "../../domain/models/project-state-snapshot.model.js";
|
||||||
@@ -152,6 +153,8 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
|||||||
supplyTypesProjectStateSnapshotSchemaVersion &&
|
supplyTypesProjectStateSnapshotSchemaVersion &&
|
||||||
stored.schemaVersion !==
|
stored.schemaVersion !==
|
||||||
voltageProjectStateSnapshotSchemaVersion &&
|
voltageProjectStateSnapshotSchemaVersion &&
|
||||||
|
stored.schemaVersion !==
|
||||||
|
simultaneityFactorProjectStateSnapshotSchemaVersion &&
|
||||||
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
||||||
) {
|
) {
|
||||||
throw new Error("Project snapshot schema version is not supported.");
|
throw new Error("Project snapshot schema version is not supported.");
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ import type {
|
|||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { consumers } from "../schema/consumers.js";
|
import { consumers } from "../schema/consumers.js";
|
||||||
|
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
|
||||||
|
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { floors } from "../schema/floors.js";
|
import { floors } from "../schema/floors.js";
|
||||||
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
|
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
|
||||||
@@ -141,6 +144,17 @@ function canonicalProjectState(state: ProjectStateSnapshot) {
|
|||||||
distributionBoards: byId(state.distributionBoards),
|
distributionBoards: byId(state.distributionBoards),
|
||||||
circuitLists: byId(state.circuitLists),
|
circuitLists: byId(state.circuitLists),
|
||||||
circuitSections: byId(state.circuitSections),
|
circuitSections: byId(state.circuitSections),
|
||||||
|
distributionBoardComponents: byId(
|
||||||
|
state.distributionBoardComponents
|
||||||
|
),
|
||||||
|
circuitProtectionDevices: byKey(
|
||||||
|
state.circuitProtectionDevices,
|
||||||
|
"circuitId"
|
||||||
|
),
|
||||||
|
distributionBoardComponentProtectionDevices: byKey(
|
||||||
|
state.distributionBoardComponentProtectionDevices,
|
||||||
|
"componentId"
|
||||||
|
),
|
||||||
circuits: byId(
|
circuits: byId(
|
||||||
state.circuits.map((circuit) => ({
|
state.circuits.map((circuit) => ({
|
||||||
...circuit,
|
...circuit,
|
||||||
@@ -159,6 +173,15 @@ function byId<T extends { id: string }>(entries: readonly T[]) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function byKey<
|
||||||
|
TKey extends string,
|
||||||
|
TEntry extends Record<TKey, string>,
|
||||||
|
>(entries: readonly TEntry[], key: TKey) {
|
||||||
|
return [...entries].sort((left, right) =>
|
||||||
|
left[key].localeCompare(right[key])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function replaceProjectState(
|
function replaceProjectState(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
state: ProjectStateSnapshot
|
state: ProjectStateSnapshot
|
||||||
@@ -214,6 +237,21 @@ function replaceProjectState(
|
|||||||
isReserve: circuit.isReserve ? 1 : 0,
|
isReserve: circuit.isReserve ? 1 : 0,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
insertMany(
|
||||||
|
database,
|
||||||
|
distributionBoardComponents,
|
||||||
|
state.distributionBoardComponents
|
||||||
|
);
|
||||||
|
insertMany(
|
||||||
|
database,
|
||||||
|
circuitProtectionDevices,
|
||||||
|
state.circuitProtectionDevices
|
||||||
|
);
|
||||||
|
insertMany(
|
||||||
|
database,
|
||||||
|
distributionBoardComponentProtectionDevices,
|
||||||
|
state.distributionBoardComponentProtectionDevices
|
||||||
|
);
|
||||||
insertMany(
|
insertMany(
|
||||||
database,
|
database,
|
||||||
circuitDeviceRows,
|
circuitDeviceRows,
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import {
|
|||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
|
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
|
||||||
|
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { floors } from "../schema/floors.js";
|
import { floors } from "../schema/floors.js";
|
||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
@@ -69,6 +72,8 @@ export function readProjectStateSnapshot(
|
|||||||
displayName: circuitSections.displayName,
|
displayName: circuitSections.displayName,
|
||||||
prefix: circuitSections.prefix,
|
prefix: circuitSections.prefix,
|
||||||
sortOrder: circuitSections.sortOrder,
|
sortOrder: circuitSections.sortOrder,
|
||||||
|
category: circuitSections.category,
|
||||||
|
groupNumber: circuitSections.groupNumber,
|
||||||
})
|
})
|
||||||
.from(circuitSections)
|
.from(circuitSections)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -82,6 +87,56 @@ export function readProjectStateSnapshot(
|
|||||||
asc(circuitSections.id)
|
asc(circuitSections.id)
|
||||||
)
|
)
|
||||||
.all();
|
.all();
|
||||||
|
const componentRows = database
|
||||||
|
.select({ component: distributionBoardComponents })
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.innerJoin(
|
||||||
|
circuitLists,
|
||||||
|
eq(circuitLists.id, distributionBoardComponents.circuitListId)
|
||||||
|
)
|
||||||
|
.where(eq(circuitLists.projectId, projectId))
|
||||||
|
.orderBy(
|
||||||
|
asc(distributionBoardComponents.circuitListId),
|
||||||
|
asc(distributionBoardComponents.placement),
|
||||||
|
asc(distributionBoardComponents.sortOrder),
|
||||||
|
asc(distributionBoardComponents.id)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
.map((row) => row.component);
|
||||||
|
const circuitProtectionRows = database
|
||||||
|
.select({ protection: circuitProtectionDevices })
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.innerJoin(
|
||||||
|
circuits,
|
||||||
|
eq(circuits.id, circuitProtectionDevices.circuitId)
|
||||||
|
)
|
||||||
|
.innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId))
|
||||||
|
.where(eq(circuitLists.projectId, projectId))
|
||||||
|
.orderBy(asc(circuitProtectionDevices.circuitId))
|
||||||
|
.all()
|
||||||
|
.map((row) => row.protection);
|
||||||
|
const componentProtectionRows = database
|
||||||
|
.select({
|
||||||
|
protection: distributionBoardComponentProtectionDevices,
|
||||||
|
})
|
||||||
|
.from(distributionBoardComponentProtectionDevices)
|
||||||
|
.innerJoin(
|
||||||
|
distributionBoardComponents,
|
||||||
|
eq(
|
||||||
|
distributionBoardComponents.id,
|
||||||
|
distributionBoardComponentProtectionDevices.componentId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
circuitLists,
|
||||||
|
eq(circuitLists.id, distributionBoardComponents.circuitListId)
|
||||||
|
)
|
||||||
|
.where(eq(circuitLists.projectId, projectId))
|
||||||
|
.orderBy(
|
||||||
|
asc(distributionBoardComponentProtectionDevices.componentId)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
.map((row) => row.protection);
|
||||||
const circuitRows = database
|
const circuitRows = database
|
||||||
.select({
|
.select({
|
||||||
id: circuits.id,
|
id: circuits.id,
|
||||||
@@ -190,6 +245,10 @@ export function readProjectStateSnapshot(
|
|||||||
distributionBoards: boardRows,
|
distributionBoards: boardRows,
|
||||||
circuitLists: listRows,
|
circuitLists: listRows,
|
||||||
circuitSections: sectionRows,
|
circuitSections: sectionRows,
|
||||||
|
distributionBoardComponents: componentRows,
|
||||||
|
circuitProtectionDevices: circuitProtectionRows,
|
||||||
|
distributionBoardComponentProtectionDevices:
|
||||||
|
componentProtectionRows,
|
||||||
circuits: circuitRows.map((circuit) => ({
|
circuits: circuitRows.map((circuit) => ({
|
||||||
...circuit,
|
...circuit,
|
||||||
isReserve: Boolean(circuit.isReserve),
|
isReserve: Boolean(circuit.isReserve),
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ import type { ProjectTransferStore } from "../../domain/ports/project-transfer.s
|
|||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
|
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
|
||||||
|
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { floors } from "../schema/floors.js";
|
import { floors } from "../schema/floors.js";
|
||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
@@ -127,6 +130,21 @@ function insertProjectState(
|
|||||||
isReserve: circuit.isReserve ? 1 : 0,
|
isReserve: circuit.isReserve ? 1 : 0,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
insertMany(
|
||||||
|
database,
|
||||||
|
distributionBoardComponents,
|
||||||
|
state.distributionBoardComponents
|
||||||
|
);
|
||||||
|
insertMany(
|
||||||
|
database,
|
||||||
|
circuitProtectionDevices,
|
||||||
|
state.circuitProtectionDevices
|
||||||
|
);
|
||||||
|
insertMany(
|
||||||
|
database,
|
||||||
|
distributionBoardComponentProtectionDevices,
|
||||||
|
state.distributionBoardComponentProtectionDevices
|
||||||
|
);
|
||||||
insertMany(
|
insertMany(
|
||||||
database,
|
database,
|
||||||
circuitDeviceRows,
|
circuitDeviceRows,
|
||||||
|
|||||||
@@ -3,6 +3,21 @@ import {
|
|||||||
defaultDistributionBoardSupplyTypes,
|
defaultDistributionBoardSupplyTypes,
|
||||||
distributionBoardSupplyTypes,
|
distributionBoardSupplyTypes,
|
||||||
} from "../../shared/constants/distribution-board.js";
|
} from "../../shared/constants/distribution-board.js";
|
||||||
|
import {
|
||||||
|
circuitGroupCategories,
|
||||||
|
type CircuitGroupCategory,
|
||||||
|
} from "../../shared/constants/circuit-group.js";
|
||||||
|
import {
|
||||||
|
distributionBoardComponentPlacements,
|
||||||
|
distributionBoardComponentRoles,
|
||||||
|
} from "../../shared/constants/distribution-board-component.js";
|
||||||
|
import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js";
|
||||||
|
import {
|
||||||
|
breakerTripCharacteristics,
|
||||||
|
fuseUtilizationCategories,
|
||||||
|
protectionDeviceTypes,
|
||||||
|
rcdTypes,
|
||||||
|
} from "../../shared/constants/protection-device.js";
|
||||||
import {
|
import {
|
||||||
resolveCircuitPhaseType,
|
resolveCircuitPhaseType,
|
||||||
resolveProjectVoltage,
|
resolveProjectVoltage,
|
||||||
@@ -14,7 +29,8 @@ export const previousProjectStateSnapshotSchemaVersion = 2 as const;
|
|||||||
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
|
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
|
||||||
export const supplyTypesProjectStateSnapshotSchemaVersion = 4 as const;
|
export const supplyTypesProjectStateSnapshotSchemaVersion = 4 as const;
|
||||||
export const voltageProjectStateSnapshotSchemaVersion = 5 as const;
|
export const voltageProjectStateSnapshotSchemaVersion = 5 as const;
|
||||||
export const projectStateSnapshotSchemaVersion = 6 as const;
|
export const simultaneityFactorProjectStateSnapshotSchemaVersion = 6 as const;
|
||||||
|
export const projectStateSnapshotSchemaVersion = 7 as const;
|
||||||
|
|
||||||
const idSchema = z.string().trim().min(1);
|
const idSchema = z.string().trim().min(1);
|
||||||
const nullableStringSchema = z.string().nullable();
|
const nullableStringSchema = z.string().nullable();
|
||||||
@@ -73,7 +89,7 @@ const circuitListSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
const circuitSectionSchema = z
|
const legacyCircuitSectionSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
circuitListId: idSchema,
|
circuitListId: idSchema,
|
||||||
@@ -84,6 +100,22 @@ const circuitSectionSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const circuitSectionSchema = legacyCircuitSectionSchema
|
||||||
|
.extend({
|
||||||
|
category: z.enum(circuitGroupCategories).nullable(),
|
||||||
|
groupNumber: z.number().int().positive().nullable(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.superRefine((section, context) => {
|
||||||
|
if ((section.category === null) !== (section.groupNumber === null)) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message:
|
||||||
|
"Circuit-section category and group number must both be set or both be null.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const circuitDeviceRowSchema = z
|
const circuitDeviceRowSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
@@ -173,21 +205,106 @@ const roomSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
const commonProjectStateSnapshotContents = {
|
const distributionBoardComponentSchema = z
|
||||||
|
.object({
|
||||||
|
id: idSchema,
|
||||||
|
circuitListId: idSchema,
|
||||||
|
sectionId: idSchema.nullable(),
|
||||||
|
equipmentIdentifier: z.string().trim().min(1),
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
role: z.enum(distributionBoardComponentRoles),
|
||||||
|
placement: z.enum(distributionBoardComponentPlacements),
|
||||||
|
sortOrder: finiteNumberSchema,
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const persistedProtectionDeviceFields = {
|
||||||
|
type: z.enum(protectionDeviceTypes),
|
||||||
|
ratedCurrentA: finiteNumberSchema.positive(),
|
||||||
|
fuseUtilizationCategory: z.enum(fuseUtilizationCategories).nullable(),
|
||||||
|
tripCharacteristic: z.enum(breakerTripCharacteristics).nullable(),
|
||||||
|
rcdType: z.enum(rcdTypes).nullable(),
|
||||||
|
ratedResidualCurrentMa: finiteNumberSchema.positive().nullable(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function validatePersistedProtectionDevice(
|
||||||
|
value: {
|
||||||
|
type: (typeof protectionDeviceTypes)[number];
|
||||||
|
ratedCurrentA: number;
|
||||||
|
fuseUtilizationCategory:
|
||||||
|
| (typeof fuseUtilizationCategories)[number]
|
||||||
|
| null;
|
||||||
|
tripCharacteristic:
|
||||||
|
| (typeof breakerTripCharacteristics)[number]
|
||||||
|
| null;
|
||||||
|
rcdType: (typeof rcdTypes)[number] | null;
|
||||||
|
ratedResidualCurrentMa: number | null;
|
||||||
|
},
|
||||||
|
context: z.RefinementCtx
|
||||||
|
) {
|
||||||
|
const result = protectionDeviceConfigurationSchema.safeParse({
|
||||||
|
type: value.type,
|
||||||
|
ratedCurrentA: value.ratedCurrentA,
|
||||||
|
...(value.fuseUtilizationCategory === null
|
||||||
|
? {}
|
||||||
|
: { fuseUtilizationCategory: value.fuseUtilizationCategory }),
|
||||||
|
...(value.tripCharacteristic === null
|
||||||
|
? {}
|
||||||
|
: { tripCharacteristic: value.tripCharacteristic }),
|
||||||
|
...(value.rcdType === null ? {} : { rcdType: value.rcdType }),
|
||||||
|
...(value.ratedResidualCurrentMa === null
|
||||||
|
? {}
|
||||||
|
: { ratedResidualCurrentMa: value.ratedResidualCurrentMa }),
|
||||||
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "Snapshot protection-device configuration is invalid.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const circuitProtectionDeviceSchema = z
|
||||||
|
.object({
|
||||||
|
circuitId: idSchema,
|
||||||
|
...persistedProtectionDeviceFields,
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.superRefine(validatePersistedProtectionDevice);
|
||||||
|
|
||||||
|
const componentProtectionDeviceSchema = z
|
||||||
|
.object({
|
||||||
|
componentId: idSchema,
|
||||||
|
...persistedProtectionDeviceFields,
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.superRefine(validatePersistedProtectionDevice);
|
||||||
|
|
||||||
|
const legacyProjectStateSnapshotContents = {
|
||||||
circuitLists: z.array(circuitListSchema),
|
circuitLists: z.array(circuitListSchema),
|
||||||
circuitSections: z.array(circuitSectionSchema),
|
circuitSections: z.array(legacyCircuitSectionSchema),
|
||||||
circuits: z.array(circuitSchema),
|
circuits: z.array(circuitSchema),
|
||||||
projectDevices: z.array(projectDeviceSchema),
|
projectDevices: z.array(projectDeviceSchema),
|
||||||
floors: z.array(floorSchema),
|
floors: z.array(floorSchema),
|
||||||
rooms: z.array(roomSchema),
|
rooms: z.array(roomSchema),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentProjectStateSnapshotContents = {
|
||||||
|
...legacyProjectStateSnapshotContents,
|
||||||
|
circuitSections: z.array(circuitSectionSchema),
|
||||||
|
distributionBoardComponents: z.array(distributionBoardComponentSchema),
|
||||||
|
circuitProtectionDevices: z.array(circuitProtectionDeviceSchema),
|
||||||
|
distributionBoardComponentProtectionDevices: z.array(
|
||||||
|
componentProtectionDeviceSchema
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
const legacyProjectStateSnapshotSchema = z
|
const legacyProjectStateSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
|
||||||
project: legacyProjectSchema,
|
project: legacyProjectSchema,
|
||||||
distributionBoards: z.array(legacyDistributionBoardSchema),
|
distributionBoards: z.array(legacyDistributionBoardSchema),
|
||||||
...commonProjectStateSnapshotContents,
|
...legacyProjectStateSnapshotContents,
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -196,7 +313,7 @@ const previousProjectStateSnapshotSchema = z
|
|||||||
schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion),
|
||||||
project: projectSchema,
|
project: projectSchema,
|
||||||
distributionBoards: z.array(legacyDistributionBoardSchema),
|
distributionBoards: z.array(legacyDistributionBoardSchema),
|
||||||
...commonProjectStateSnapshotContents,
|
...legacyProjectStateSnapshotContents,
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -207,7 +324,7 @@ const distributionBoardProjectStateSnapshotSchema = z
|
|||||||
),
|
),
|
||||||
project: projectSchema,
|
project: projectSchema,
|
||||||
distributionBoards: z.array(distributionBoardSchema),
|
distributionBoards: z.array(distributionBoardSchema),
|
||||||
...commonProjectStateSnapshotContents,
|
...legacyProjectStateSnapshotContents,
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -216,7 +333,7 @@ const supplyTypesProjectStateSnapshotSchema = z
|
|||||||
schemaVersion: z.literal(supplyTypesProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(supplyTypesProjectStateSnapshotSchemaVersion),
|
||||||
project: currentProjectSchema,
|
project: currentProjectSchema,
|
||||||
distributionBoards: z.array(distributionBoardSchema),
|
distributionBoards: z.array(distributionBoardSchema),
|
||||||
...commonProjectStateSnapshotContents,
|
...legacyProjectStateSnapshotContents,
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -227,12 +344,20 @@ const voltageProjectStateSnapshotSchema =
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const projectStateSnapshotSchema =
|
const simultaneityFactorProjectStateSnapshotSchema =
|
||||||
voltageProjectStateSnapshotSchema.extend({
|
voltageProjectStateSnapshotSchema.extend({
|
||||||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(
|
||||||
|
simultaneityFactorProjectStateSnapshotSchemaVersion
|
||||||
|
),
|
||||||
distributionBoards: z.array(currentDistributionBoardSchema),
|
distributionBoards: z.array(currentDistributionBoardSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const projectStateSnapshotSchema =
|
||||||
|
simultaneityFactorProjectStateSnapshotSchema.extend({
|
||||||
|
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
||||||
|
...currentProjectStateSnapshotContents,
|
||||||
|
});
|
||||||
|
|
||||||
export type ProjectStateSnapshot = z.infer<
|
export type ProjectStateSnapshot = z.infer<
|
||||||
typeof projectStateSnapshotSchema
|
typeof projectStateSnapshotSchema
|
||||||
>;
|
>;
|
||||||
@@ -243,7 +368,8 @@ export function parseProjectStateSnapshot(
|
|||||||
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
||||||
const parsedSnapshot =
|
const parsedSnapshot =
|
||||||
version === legacyProjectStateSnapshotSchemaVersion
|
version === legacyProjectStateSnapshotSchemaVersion
|
||||||
? upgradeVoltageProjectStateSnapshot(
|
? upgradeSimultaneityFactorProjectStateSnapshot(
|
||||||
|
upgradeVoltageProjectStateSnapshot(
|
||||||
upgradeSupplyTypesProjectStateSnapshot(
|
upgradeSupplyTypesProjectStateSnapshot(
|
||||||
upgradeDistributionBoardProjectStateSnapshot(
|
upgradeDistributionBoardProjectStateSnapshot(
|
||||||
upgradePreviousProjectStateSnapshot(
|
upgradePreviousProjectStateSnapshot(
|
||||||
@@ -254,8 +380,10 @@ export function parseProjectStateSnapshot(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
: version === previousProjectStateSnapshotSchemaVersion
|
: version === previousProjectStateSnapshotSchemaVersion
|
||||||
? upgradeVoltageProjectStateSnapshot(
|
? upgradeSimultaneityFactorProjectStateSnapshot(
|
||||||
|
upgradeVoltageProjectStateSnapshot(
|
||||||
upgradeSupplyTypesProjectStateSnapshot(
|
upgradeSupplyTypesProjectStateSnapshot(
|
||||||
upgradeDistributionBoardProjectStateSnapshot(
|
upgradeDistributionBoardProjectStateSnapshot(
|
||||||
upgradePreviousProjectStateSnapshot(
|
upgradePreviousProjectStateSnapshot(
|
||||||
@@ -264,24 +392,40 @@ export function parseProjectStateSnapshot(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
: version === distributionBoardProjectStateSnapshotSchemaVersion
|
: version === distributionBoardProjectStateSnapshotSchemaVersion
|
||||||
? upgradeVoltageProjectStateSnapshot(
|
? upgradeSimultaneityFactorProjectStateSnapshot(
|
||||||
|
upgradeVoltageProjectStateSnapshot(
|
||||||
upgradeSupplyTypesProjectStateSnapshot(
|
upgradeSupplyTypesProjectStateSnapshot(
|
||||||
upgradeDistributionBoardProjectStateSnapshot(
|
upgradeDistributionBoardProjectStateSnapshot(
|
||||||
distributionBoardProjectStateSnapshotSchema.parse(value)
|
distributionBoardProjectStateSnapshotSchema.parse(
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: version === supplyTypesProjectStateSnapshotSchemaVersion
|
: version === supplyTypesProjectStateSnapshotSchemaVersion
|
||||||
? upgradeVoltageProjectStateSnapshot(
|
? upgradeSimultaneityFactorProjectStateSnapshot(
|
||||||
|
upgradeVoltageProjectStateSnapshot(
|
||||||
upgradeSupplyTypesProjectStateSnapshot(
|
upgradeSupplyTypesProjectStateSnapshot(
|
||||||
supplyTypesProjectStateSnapshotSchema.parse(value)
|
supplyTypesProjectStateSnapshotSchema.parse(value)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
: version === voltageProjectStateSnapshotSchemaVersion
|
: version === voltageProjectStateSnapshotSchemaVersion
|
||||||
? upgradeVoltageProjectStateSnapshot(
|
? upgradeSimultaneityFactorProjectStateSnapshot(
|
||||||
|
upgradeVoltageProjectStateSnapshot(
|
||||||
voltageProjectStateSnapshotSchema.parse(value)
|
voltageProjectStateSnapshotSchema.parse(value)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
: version ===
|
||||||
|
simultaneityFactorProjectStateSnapshotSchemaVersion
|
||||||
|
? upgradeSimultaneityFactorProjectStateSnapshot(
|
||||||
|
simultaneityFactorProjectStateSnapshotSchema.parse(
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
: projectStateSnapshotSchema.parse(value);
|
: projectStateSnapshotSchema.parse(value);
|
||||||
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
|
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
|
||||||
assertProjectStateSnapshotRelations(snapshot);
|
assertProjectStateSnapshotRelations(snapshot);
|
||||||
@@ -412,10 +556,10 @@ function upgradeSupplyTypesProjectStateSnapshot(
|
|||||||
|
|
||||||
function upgradeVoltageProjectStateSnapshot(
|
function upgradeVoltageProjectStateSnapshot(
|
||||||
snapshot: z.infer<typeof voltageProjectStateSnapshotSchema>
|
snapshot: z.infer<typeof voltageProjectStateSnapshotSchema>
|
||||||
): ProjectStateSnapshot {
|
): z.infer<typeof simultaneityFactorProjectStateSnapshotSchema> {
|
||||||
return {
|
return {
|
||||||
...snapshot,
|
...snapshot,
|
||||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
schemaVersion: simultaneityFactorProjectStateSnapshotSchemaVersion,
|
||||||
distributionBoards: snapshot.distributionBoards.map((board) => ({
|
distributionBoards: snapshot.distributionBoards.map((board) => ({
|
||||||
...board,
|
...board,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
@@ -423,6 +567,39 @@ function upgradeVoltageProjectStateSnapshot(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upgradeSimultaneityFactorProjectStateSnapshot(
|
||||||
|
snapshot: z.infer<typeof simultaneityFactorProjectStateSnapshotSchema>
|
||||||
|
): ProjectStateSnapshot {
|
||||||
|
return {
|
||||||
|
...snapshot,
|
||||||
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||||
|
circuitSections: snapshot.circuitSections.map((section) => {
|
||||||
|
const category = initialCategoryBySectionKey(section.key);
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
category,
|
||||||
|
groupNumber: category === null ? null : 1,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
distributionBoardComponents: [],
|
||||||
|
circuitProtectionDevices: [],
|
||||||
|
distributionBoardComponentProtectionDevices: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialCategoryBySectionKey(
|
||||||
|
key: string
|
||||||
|
): CircuitGroupCategory | null {
|
||||||
|
if (
|
||||||
|
key === "lighting" ||
|
||||||
|
key === "single_phase" ||
|
||||||
|
key === "three_phase"
|
||||||
|
) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
@@ -461,10 +638,22 @@ function assertProjectStateSnapshotRelations(
|
|||||||
);
|
);
|
||||||
const floorIds = uniqueIds(snapshot.floors, "floor");
|
const floorIds = uniqueIds(snapshot.floors, "floor");
|
||||||
const roomIds = uniqueIds(snapshot.rooms, "room");
|
const roomIds = uniqueIds(snapshot.rooms, "room");
|
||||||
uniqueIds(snapshot.circuits, "circuit");
|
const circuitIds = uniqueIds(snapshot.circuits, "circuit");
|
||||||
|
const componentIds = uniqueIds(
|
||||||
|
snapshot.distributionBoardComponents,
|
||||||
|
"distribution board component"
|
||||||
|
);
|
||||||
const sectionById = new Map(
|
const sectionById = new Map(
|
||||||
snapshot.circuitSections.map((entry) => [entry.id, entry])
|
snapshot.circuitSections.map((entry) => [entry.id, entry])
|
||||||
);
|
);
|
||||||
|
const componentById = new Map(
|
||||||
|
snapshot.distributionBoardComponents.map((entry) => [
|
||||||
|
entry.id,
|
||||||
|
entry,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const groupKeys = new Set<string>();
|
||||||
|
const equipmentIdentifiersByList = new Map<string, Set<string>>();
|
||||||
|
|
||||||
for (const board of snapshot.distributionBoards) {
|
for (const board of snapshot.distributionBoards) {
|
||||||
assertProjectOwnership(board.projectId, projectId, "distribution board");
|
assertProjectOwnership(board.projectId, projectId, "distribution board");
|
||||||
@@ -500,6 +689,19 @@ function assertProjectStateSnapshotRelations(
|
|||||||
section.circuitListId,
|
section.circuitListId,
|
||||||
"circuit section list"
|
"circuit section list"
|
||||||
);
|
);
|
||||||
|
if (section.category !== null && section.groupNumber !== null) {
|
||||||
|
const groupKey = [
|
||||||
|
section.circuitListId,
|
||||||
|
section.category,
|
||||||
|
section.groupNumber,
|
||||||
|
].join(":");
|
||||||
|
if (groupKeys.has(groupKey)) {
|
||||||
|
throw new Error(
|
||||||
|
"Snapshot contains a duplicate circuit group number."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
groupKeys.add(groupKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const device of snapshot.projectDevices) {
|
for (const device of snapshot.projectDevices) {
|
||||||
assertProjectOwnership(device.projectId, projectId, "project device");
|
assertProjectOwnership(device.projectId, projectId, "project device");
|
||||||
@@ -553,6 +755,11 @@ function assertProjectStateSnapshotRelations(
|
|||||||
"Snapshot circuit voltage must match project settings."
|
"Snapshot circuit voltage must match project settings."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
registerEquipmentIdentifier(
|
||||||
|
equipmentIdentifiersByList,
|
||||||
|
circuit.circuitListId,
|
||||||
|
circuit.equipmentIdentifier
|
||||||
|
);
|
||||||
for (const row of circuit.deviceRows) {
|
for (const row of circuit.deviceRows) {
|
||||||
if (row.circuitId !== circuit.id) {
|
if (row.circuitId !== circuit.id) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -575,6 +782,148 @@ function assertProjectStateSnapshotRelations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fixedComponentRolesByList = new Set<string>();
|
||||||
|
const groupComponentRolesBySection = new Set<string>();
|
||||||
|
for (const component of snapshot.distributionBoardComponents) {
|
||||||
|
assertReference(
|
||||||
|
circuitListIds,
|
||||||
|
component.circuitListId,
|
||||||
|
"distribution board component circuit list"
|
||||||
|
);
|
||||||
|
if (component.sectionId !== null) {
|
||||||
|
assertReference(
|
||||||
|
sectionIds,
|
||||||
|
component.sectionId,
|
||||||
|
"distribution board component section"
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
sectionById.get(component.sectionId)?.circuitListId !==
|
||||||
|
component.circuitListId
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Snapshot distribution board component section belongs to a different circuit list."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertComponentPlacement(component);
|
||||||
|
registerEquipmentIdentifier(
|
||||||
|
equipmentIdentifiersByList,
|
||||||
|
component.circuitListId,
|
||||||
|
component.equipmentIdentifier
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
component.role === "main_switch" ||
|
||||||
|
component.role === "surge_protective_device"
|
||||||
|
) {
|
||||||
|
assertUniqueKey(
|
||||||
|
fixedComponentRolesByList,
|
||||||
|
`${component.circuitListId}:${component.role}`,
|
||||||
|
"Snapshot contains duplicate fixed distribution board components."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
component.role === "group_upstream_protection" ||
|
||||||
|
component.role === "group_residual_current_protection"
|
||||||
|
) {
|
||||||
|
assertUniqueKey(
|
||||||
|
groupComponentRolesBySection,
|
||||||
|
`${component.sectionId}:${component.role}`,
|
||||||
|
"Snapshot contains duplicate group protection components."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const protectedCircuitIds = new Set<string>();
|
||||||
|
for (const protection of snapshot.circuitProtectionDevices) {
|
||||||
|
assertReference(
|
||||||
|
circuitIds,
|
||||||
|
protection.circuitId,
|
||||||
|
"circuit protection device circuit"
|
||||||
|
);
|
||||||
|
assertUniqueKey(
|
||||||
|
protectedCircuitIds,
|
||||||
|
protection.circuitId,
|
||||||
|
"Snapshot contains duplicate circuit protection devices."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const protectedComponentIds = new Set<string>();
|
||||||
|
for (const protection of snapshot.distributionBoardComponentProtectionDevices) {
|
||||||
|
assertReference(
|
||||||
|
componentIds,
|
||||||
|
protection.componentId,
|
||||||
|
"component protection device component"
|
||||||
|
);
|
||||||
|
assertUniqueKey(
|
||||||
|
protectedComponentIds,
|
||||||
|
protection.componentId,
|
||||||
|
"Snapshot contains duplicate component protection devices."
|
||||||
|
);
|
||||||
|
const component = componentById.get(protection.componentId)!;
|
||||||
|
if (
|
||||||
|
component.role !== "group_upstream_protection" &&
|
||||||
|
component.role !== "group_residual_current_protection"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Snapshot protection data belongs to a non-protection component."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertComponentPlacement(
|
||||||
|
component: ProjectStateSnapshot["distributionBoardComponents"][number]
|
||||||
|
) {
|
||||||
|
const isFixedHeader =
|
||||||
|
component.role === "main_switch" ||
|
||||||
|
component.role === "surge_protective_device";
|
||||||
|
const isGroupProtection =
|
||||||
|
component.role === "group_upstream_protection" ||
|
||||||
|
component.role === "group_residual_current_protection";
|
||||||
|
if (
|
||||||
|
(isFixedHeader &&
|
||||||
|
(component.placement !== "header" ||
|
||||||
|
component.sectionId !== null)) ||
|
||||||
|
(isGroupProtection &&
|
||||||
|
(component.placement !== "group" ||
|
||||||
|
component.sectionId === null)) ||
|
||||||
|
(component.role === "auxiliary" &&
|
||||||
|
(component.placement !== "footer" ||
|
||||||
|
component.sectionId !== null))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Snapshot distribution board component placement is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerEquipmentIdentifier(
|
||||||
|
identifiersByList: Map<string, Set<string>>,
|
||||||
|
circuitListId: string,
|
||||||
|
equipmentIdentifier: string
|
||||||
|
) {
|
||||||
|
const identifiers =
|
||||||
|
identifiersByList.get(circuitListId) ?? new Set<string>();
|
||||||
|
const normalized = equipmentIdentifier.trim().toLocaleLowerCase("de");
|
||||||
|
if (identifiers.has(normalized)) {
|
||||||
|
throw new Error(
|
||||||
|
"Snapshot contains duplicate equipment identifiers in one circuit list."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
identifiers.add(normalized);
|
||||||
|
identifiersByList.set(circuitListId, identifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertUniqueKey(
|
||||||
|
keys: Set<string>,
|
||||||
|
key: string,
|
||||||
|
message: string
|
||||||
|
) {
|
||||||
|
if (keys.has(key)) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
keys.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniqueIds(
|
function uniqueIds(
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ export function remapProjectState(
|
|||||||
const listIds = idMap(source.circuitLists, createId);
|
const listIds = idMap(source.circuitLists, createId);
|
||||||
const sectionIds = idMap(source.circuitSections, createId);
|
const sectionIds = idMap(source.circuitSections, createId);
|
||||||
const circuitIds = idMap(source.circuits, createId);
|
const circuitIds = idMap(source.circuits, createId);
|
||||||
|
const componentIds = idMap(
|
||||||
|
source.distributionBoardComponents,
|
||||||
|
createId
|
||||||
|
);
|
||||||
const deviceIds = idMap(source.projectDevices, createId);
|
const deviceIds = idMap(source.projectDevices, createId);
|
||||||
const floorIds = idMap(source.floors, createId);
|
const floorIds = idMap(source.floors, createId);
|
||||||
const roomIds = idMap(source.rooms, createId);
|
const roomIds = idMap(source.rooms, createId);
|
||||||
@@ -110,6 +114,35 @@ export function remapProjectState(
|
|||||||
id: requiredId(sectionIds, section.id),
|
id: requiredId(sectionIds, section.id),
|
||||||
circuitListId: requiredId(listIds, section.circuitListId),
|
circuitListId: requiredId(listIds, section.circuitListId),
|
||||||
})),
|
})),
|
||||||
|
distributionBoardComponents:
|
||||||
|
source.distributionBoardComponents.map((component) => ({
|
||||||
|
...component,
|
||||||
|
id: requiredId(componentIds, component.id),
|
||||||
|
circuitListId: requiredId(
|
||||||
|
listIds,
|
||||||
|
component.circuitListId
|
||||||
|
),
|
||||||
|
sectionId:
|
||||||
|
component.sectionId === null
|
||||||
|
? null
|
||||||
|
: requiredId(sectionIds, component.sectionId),
|
||||||
|
})),
|
||||||
|
circuitProtectionDevices: source.circuitProtectionDevices.map(
|
||||||
|
(protection) => ({
|
||||||
|
...protection,
|
||||||
|
circuitId: requiredId(circuitIds, protection.circuitId),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
distributionBoardComponentProtectionDevices:
|
||||||
|
source.distributionBoardComponentProtectionDevices.map(
|
||||||
|
(protection) => ({
|
||||||
|
...protection,
|
||||||
|
componentId: requiredId(
|
||||||
|
componentIds,
|
||||||
|
protection.componentId
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
circuits: source.circuits.map((circuit) => ({
|
circuits: source.circuits.map((circuit) => ({
|
||||||
...circuit,
|
...circuit,
|
||||||
id: requiredId(circuitIds, circuit.id),
|
id: requiredId(circuitIds, circuit.id),
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import { ProjectTransferRepository } from "../src/db/repositories/project-transf
|
|||||||
import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js";
|
import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../src/db/schema/circuit-lists.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 { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.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 { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||||
import { floors } from "../src/db/schema/floors.js";
|
import { floors } from "../src/db/schema/floors.js";
|
||||||
import { projectDevices } from "../src/db/schema/project-devices.js";
|
import { projectDevices } from "../src/db/schema/project-devices.js";
|
||||||
@@ -129,9 +132,58 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
cosPhi: 0.9,
|
cosPhi: 0.9,
|
||||||
})
|
})
|
||||||
.run();
|
.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;
|
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", () => {
|
describe("project snapshot repository", () => {
|
||||||
it("captures, hashes and lists a complete named logical snapshot", () => {
|
it("captures, hashes and lists a complete named logical snapshot", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
@@ -190,6 +242,12 @@ describe("project snapshot repository", () => {
|
|||||||
context.db.select().from(circuitSections).all().length
|
context.db.select().from(circuitSections).all().length
|
||||||
);
|
);
|
||||||
assert.equal(state.circuits.length, 1);
|
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.length, 1);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
state.circuits[0].deviceRows[0].linkedProjectDeviceId,
|
state.circuits[0].deviceRows[0].linkedProjectDeviceId,
|
||||||
@@ -291,6 +349,7 @@ describe("project snapshot repository", () => {
|
|||||||
project: Record<string, unknown>;
|
project: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
currentPayload.schemaVersion = 1;
|
currentPayload.schemaVersion = 1;
|
||||||
|
removeVersionSevenFields(currentPayload);
|
||||||
delete currentPayload.project.internalProjectNumber;
|
delete currentPayload.project.internalProjectNumber;
|
||||||
delete currentPayload.project.externalProjectNumber;
|
delete currentPayload.project.externalProjectNumber;
|
||||||
delete currentPayload.project.buildingOwner;
|
delete currentPayload.project.buildingOwner;
|
||||||
@@ -368,6 +427,19 @@ describe("portable project transfer", () => {
|
|||||||
copied.state.circuits[0].id,
|
copied.state.circuits[0].id,
|
||||||
source.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(
|
assert.equal(
|
||||||
copied.state.circuits[0].deviceRows[0].linkedProjectDeviceId,
|
copied.state.circuits[0].deviceRows[0].linkedProjectDeviceId,
|
||||||
copied.state.projectDevices[0].id
|
copied.state.projectDevices[0].id
|
||||||
@@ -438,6 +510,7 @@ describe("portable project transfer", () => {
|
|||||||
distributionBoards: Array<Record<string, unknown>>;
|
distributionBoards: Array<Record<string, unknown>>;
|
||||||
};
|
};
|
||||||
legacyState.schemaVersion = 2;
|
legacyState.schemaVersion = 2;
|
||||||
|
removeVersionSevenFields(legacyState);
|
||||||
delete (
|
delete (
|
||||||
legacyState.project as Record<string, unknown>
|
legacyState.project as Record<string, unknown>
|
||||||
).enabledDistributionBoardSupplyTypes;
|
).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 { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/project-settings-project-command.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../src/db/schema/circuit-lists.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 { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
import { consumers } from "../src/db/schema/consumers.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 { floors } from "../src/db/schema/floors.js";
|
||||||
import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js";
|
import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js";
|
||||||
import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js";
|
import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js";
|
||||||
@@ -132,6 +135,43 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
simultaneityFactor: 0.8,
|
simultaneityFactor: 0.8,
|
||||||
})
|
})
|
||||||
.run();
|
.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;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +213,16 @@ function changeCompleteProjectState(context: DatabaseContext) {
|
|||||||
})
|
})
|
||||||
.where(eq(circuits.id, "circuit-1"))
|
.where(eq(circuits.id, "circuit-1"))
|
||||||
.run();
|
.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
|
context.db
|
||||||
.insert(floors)
|
.insert(floors)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ function minimalSnapshot() {
|
|||||||
distributionBoards: [],
|
distributionBoards: [],
|
||||||
circuitLists: [],
|
circuitLists: [],
|
||||||
circuitSections: [],
|
circuitSections: [],
|
||||||
|
distributionBoardComponents: [],
|
||||||
|
circuitProtectionDevices: [],
|
||||||
|
distributionBoardComponentProtectionDevices: [],
|
||||||
circuits: [],
|
circuits: [],
|
||||||
projectDevices: [],
|
projectDevices: [],
|
||||||
floors: [],
|
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", () => {
|
describe("project state snapshot model", () => {
|
||||||
it("round-trips a versioned logical project state", () => {
|
it("round-trips a versioned logical project state", () => {
|
||||||
const snapshot = parseProjectStateSnapshot(minimalSnapshot());
|
const snapshot = parseProjectStateSnapshot(minimalSnapshot());
|
||||||
@@ -52,7 +160,7 @@ describe("project state snapshot model", () => {
|
|||||||
|
|
||||||
it("upgrades version-one snapshots with empty project metadata", () => {
|
it("upgrades version-one snapshots with empty project metadata", () => {
|
||||||
const legacy = {
|
const legacy = {
|
||||||
...minimalSnapshot(),
|
...legacyMinimalSnapshot(),
|
||||||
schemaVersion: 1 as const,
|
schemaVersion: 1 as const,
|
||||||
project: {
|
project: {
|
||||||
id: "project-1",
|
id: "project-1",
|
||||||
@@ -82,7 +190,7 @@ describe("project state snapshot model", () => {
|
|||||||
|
|
||||||
it("upgrades version-two distribution boards without inferred assignments", () => {
|
it("upgrades version-two distribution boards without inferred assignments", () => {
|
||||||
const previous = {
|
const previous = {
|
||||||
...minimalSnapshot(),
|
...legacyMinimalSnapshot(),
|
||||||
schemaVersion: 2 as const,
|
schemaVersion: 2 as const,
|
||||||
project: {
|
project: {
|
||||||
...minimalSnapshot().project,
|
...minimalSnapshot().project,
|
||||||
@@ -110,7 +218,7 @@ describe("project state snapshot model", () => {
|
|||||||
|
|
||||||
it("upgrades version-three snapshots with all project supply types", () => {
|
it("upgrades version-three snapshots with all project supply types", () => {
|
||||||
const previous = {
|
const previous = {
|
||||||
...minimalSnapshot(),
|
...legacyMinimalSnapshot(),
|
||||||
schemaVersion: 3 as const,
|
schemaVersion: 3 as const,
|
||||||
project: {
|
project: {
|
||||||
...minimalSnapshot().project,
|
...minimalSnapshot().project,
|
||||||
@@ -131,7 +239,7 @@ describe("project state snapshot model", () => {
|
|||||||
|
|
||||||
it("normalizes version-four device voltages to project settings", () => {
|
it("normalizes version-four device voltages to project settings", () => {
|
||||||
const previous = {
|
const previous = {
|
||||||
...minimalSnapshot(),
|
...legacyMinimalSnapshot(),
|
||||||
schemaVersion: 4 as const,
|
schemaVersion: 4 as const,
|
||||||
projectDevices: [
|
projectDevices: [
|
||||||
{
|
{
|
||||||
@@ -161,7 +269,7 @@ describe("project state snapshot model", () => {
|
|||||||
|
|
||||||
it("upgrades version-five distribution boards with a neutral factor", () => {
|
it("upgrades version-five distribution boards with a neutral factor", () => {
|
||||||
const previous = {
|
const previous = {
|
||||||
...minimalSnapshot(),
|
...legacyMinimalSnapshot(),
|
||||||
schemaVersion: 5 as const,
|
schemaVersion: 5 as const,
|
||||||
distributionBoards: [
|
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", () => {
|
it("rejects duplicate ids and cross-project ownership", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user