Create protected board defaults

This commit is contained in:
2026-07-30 19:30:53 +02:00
parent 012308984d
commit 9d4d4082fe
10 changed files with 504 additions and 156 deletions
+5 -2
View File
@@ -294,10 +294,13 @@ change in one revision and Undo/Redo restores them together; the project PUT
route requires `expectedRevision`. The system catalog is `AV`, `SV`, `EV`, route requires `expectedRevision`. The system catalog is `AV`, `SV`, `EV`,
`USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board `USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board
cannot be disabled. cannot be disabled.
Distribution-board setup uses `distribution-board.insert` with a complete Distribution-board setup uses `distribution-board.insert` schema version 3
stable snapshot of the board, circuit list and four default sections. Its with a complete stable snapshot of the board, circuit list, three default
groups, main switch `-Q0` and surge protective device `-FA`. Its
inverse removes only the same unchanged and still-empty structure; the POST inverse removes only the same unchanged and still-empty structure; the POST
route requires `expectedRevision` and returns the updated history state. route requires `expectedRevision` and returns the updated history state.
Stored schema-version 1 and 2 setup commands remain executable with their four
legacy sections and without invented components.
Distribution-board floor assignment and a project-enabled supply type use Distribution-board floor assignment and a project-enabled supply type use
`distribution-board.update`; both values are snapshot/export fields and one `distribution-board.update`; both values are snapshot/export fields and one
persistent undo step. persistent undo step.
+4 -1
View File
@@ -194,11 +194,14 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
- `supplyType` is one of `AV`, `SV`, `EV`, `USV`, `MSR` or `SiBe` and must - `supplyType` is one of `AV`, `SV`, `EV`, `USV`, `MSR` or `SiBe` and must
be enabled in the project settings be enabled in the project settings
- executes `distribution-board.insert` with stable ids for the distribution - executes `distribution-board.insert` with stable ids for the distribution
board, its circuit list and all four default circuit sections board, its circuit list, the three default circuit groups, main switch
`-Q0` and surge protective device `-FA`
- response: - response:
`{ "distributionBoard": { ... }, "revision": { ... }, "history": { ... } }` `{ "distributionBoard": { ... }, "revision": { ... }, "history": { ... } }`
- persistent Undo removes only the unchanged and still-empty generated - persistent Undo removes only the unchanged and still-empty generated
structure; Redo restores the same ids structure; Redo restores the same ids
- stored command schema versions 1 and 2 remain executable with their four
legacy sections and without generated components
- stale revisions return `409 PROJECT_REVISION_CONFLICT` - stale revisions return `409 PROJECT_REVISION_CONFLICT`
- `PUT /projects/:projectId/distribution-boards/:distributionBoardId` - `PUT /projects/:projectId/distribution-boards/:distributionBoardId`
- body: - body:
+6 -2
View File
@@ -271,8 +271,10 @@ Projektübersicht verwendet dafür den separaten Collection-Endpunkt
deshalb weder eine bestehende Projekt-ID noch `expectedRevision` annimmt. Das deshalb weder eine bestehende Projekt-ID noch `expectedRevision` annimmt. Das
Ersetzen eines Projekts bleibt auf dessen Einstellungsmodal und den Ersetzen eines Projekts bleibt auf dessen Einstellungsmodal und den
projektgebundenen Endpunkt beschränkt. projektgebundenen Endpunkt beschränkt.
`distribution-board.insert` versioniert die Anlage einer Verteilung als einen `distribution-board.insert` versioniert die Anlage einer Verteilung mit
vollständigen Block aus Verteilung, Stromkreisliste und vier Standardbereichen. Command-Schema 3 als vollständigen Block aus Verteilung, Stromkreisliste, den
drei Gruppen `Beleuchtung 1`, `1-phasig 1`, `3-phasig 1`, Hauptschalter `-Q0`
und Überspannungsableiter `-FA`.
Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil. Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil.
`distribution-board.delete` ist die persistierte Inverse und entfernt nur den `distribution-board.delete` ist die persistierte Inverse und entfernt nur den
vollständig unveränderten, weiterhin stromkreislosen Block. Controller und vollständig unveränderten, weiterhin stromkreislosen Block. Controller und
@@ -281,6 +283,8 @@ Controller-Schreibweg ist entfernt. Verteilungen besitzen eine optionale
Etagenreferenz sowie eine Netzart aus dem Projektkatalog. Anlage und Etagenreferenz sowie eine Netzart aus dem Projektkatalog. Anlage und
nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
Freigabe der Netzart in den Projekteinstellungen. Freigabe der Netzart in den Projekteinstellungen.
Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier
Legacy-Abschnitten und ohne nachträglich erfundene Komponenten ausführbar.
`distribution-board.update` versioniert Etage, Netzart und den `distribution-board.update` versioniert Etage, Netzart und den
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
+3 -1
View File
@@ -18,7 +18,9 @@ requirements and intended sequencing, not proof of implementation.
tables and shared BMK uniqueness. tables and shared BMK uniqueness.
- [x] 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. - [x] Phase C1: versioned new-board command with three groups and fixed header
components.
- [ ] Phase C2: component and group CRUD/reorder commands.
- [ ] Phase D: group numbering, moves and destructive operations. - [ ] Phase D: group numbering, moves and destructive operations.
- [ ] Phase E: editor projection and editing. - [ ] Phase E: editor projection and editing.
- [ ] Phase F: documentation and full GUI verification. - [ ] Phase F: documentation and full GUI verification.
@@ -593,11 +593,25 @@ Acceptance:
### C. Persistent Commands and Board Defaults ### C. Persistent Commands and Board Defaults
Status: In progress. New-board defaults C1 are complete; component and group
management commands C2 are pending.
- extend `distribution-board.insert` with fixed components and three groups - extend `distribution-board.insert` with fixed components and three groups
- implement component and group CRUD commands - implement component and group CRUD commands
- implement group and footer reordering - implement group and footer reordering
- add complete inverse snapshots and rollback tests - add complete inverse snapshots and rollback tests
Implemented in C1:
- `distribution-board.insert` schema version 3 carries stable UUIDs for three
initial groups, main switch `-Q0` and surge protective device `-FA`
- its inverse verifies and removes the unchanged complete structure; Redo
restores the same UUIDs
- a late revision/history failure rolls back board, list, groups and components
together
- stored schema versions 1 and 2 retain four legacy sections and receive no
invented components
Acceptance: Acceptance:
- new boards contain the agreed fixed structure - new boards contain the agreed fixed structure
@@ -2,14 +2,12 @@ import { and, asc, eq, inArray } from "drizzle-orm";
import { import {
assertDistributionBoardDeleteProjectCommand, assertDistributionBoardDeleteProjectCommand,
assertDistributionBoardInsertProjectCommand, assertDistributionBoardInsertProjectCommand,
createDistributionBoardDeleteProjectCommand,
createDistributionBoardInsertProjectCommand,
distributionBoardDeleteCommandType, distributionBoardDeleteCommandType,
distributionBoardInsertCommandType, distributionBoardInsertCommandType,
invertDistributionBoardStructureProjectCommand,
normalizeDistributionBoardStructureProjectCommand, normalizeDistributionBoardStructureProjectCommand,
type DistributionBoardStructureProjectCommand, type DistributionBoardStructureProjectCommand,
type DistributionBoardStructureSnapshot, type NormalizedDistributionBoardStructureSnapshot,
legacyDistributionBoardStructureCommandSchemaVersion,
} from "../../domain/models/distribution-board-structure-project-command.model.js"; } from "../../domain/models/distribution-board-structure-project-command.model.js";
import { import {
assertDistributionBoardUpdateProjectCommand, assertDistributionBoardUpdateProjectCommand,
@@ -24,11 +22,11 @@ import type {
DistributionBoardStructureProjectCommandStore, DistributionBoardStructureProjectCommandStore,
ExecuteDistributionBoardStructureCommandInput, ExecuteDistributionBoardStructureCommandInput,
} from "../../domain/ports/distribution-board-structure-project-command.store.js"; } from "../../domain/ports/distribution-board-structure-project-command.store.js";
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
import type { AppDatabase } from "../database-context.js"; import type { AppDatabase } from "../database-context.js";
import { circuitLists } from "../schema/circuit-lists.js"; import { circuitLists } from "../schema/circuit-lists.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 { 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 { projects } from "../schema/projects.js"; import { projects } from "../schema/projects.js";
@@ -69,31 +67,15 @@ export class DistributionBoardStructureProjectCommandRepository
projectId: string, projectId: string,
command: DistributionBoardStructureProjectCommand command: DistributionBoardStructureProjectCommand
): DistributionBoardStructureProjectCommand { ): DistributionBoardStructureProjectCommand {
const normalized = const structure =
normalizeDistributionBoardStructureProjectCommand(command); normalizeDistributionBoardStructureProjectCommand(command);
if (normalized.type === distributionBoardInsertCommandType) { if (command.type === distributionBoardInsertCommandType) {
assertDistributionBoardInsertProjectCommand(normalized); this.insert(database, projectId, structure);
const inverse = this.insert( return invertDistributionBoardStructureProjectCommand(command);
database,
projectId,
normalized.payload.structure
);
return command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
? toLegacyStructureCommand(inverse)
: inverse;
} }
if (normalized.type === distributionBoardDeleteCommandType) { if (command.type === distributionBoardDeleteCommandType) {
assertDistributionBoardDeleteProjectCommand(normalized); this.delete(database, projectId, structure);
const inverse = this.delete( return invertDistributionBoardStructureProjectCommand(command);
database,
projectId,
normalized.payload.structure
);
return command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
? toLegacyStructureCommand(inverse)
: inverse;
} }
throw new Error("Unsupported distribution-board structure command."); throw new Error("Unsupported distribution-board structure command.");
} }
@@ -101,7 +83,7 @@ export class DistributionBoardStructureProjectCommandRepository
private insert( private insert(
database: AppDatabase, database: AppDatabase,
projectId: string, projectId: string,
structure: DistributionBoardStructureSnapshot structure: NormalizedDistributionBoardStructureSnapshot
) { ) {
if ( if (
structure.distributionBoard.projectId !== projectId || structure.distributionBoard.projectId !== projectId ||
@@ -160,7 +142,26 @@ export class DistributionBoardStructureProjectCommandRepository
) )
.limit(1) .limit(1)
.get(); .get();
if (existingBoard || existingList || existingSection) { const existingComponent =
structure.components.length === 0
? undefined
: database
.select({ id: distributionBoardComponents.id })
.from(distributionBoardComponents)
.where(
inArray(
distributionBoardComponents.id,
structure.components.map((component) => component.id)
)
)
.limit(1)
.get();
if (
existingBoard ||
existingList ||
existingSection ||
existingComponent
) {
throw new Error("Distribution-board structure id already exists."); throw new Error("Distribution-board structure id already exists.");
} }
@@ -171,15 +172,20 @@ export class DistributionBoardStructureProjectCommandRepository
database.insert(circuitLists).values(structure.circuitList).run(); database.insert(circuitLists).values(structure.circuitList).run();
database database
.insert(circuitSections) .insert(circuitSections)
.values(structure.sections.map(withInitialGroupIdentity)) .values(structure.sections)
.run(); .run();
return createDistributionBoardDeleteProjectCommand(structure); if (structure.components.length > 0) {
database
.insert(distributionBoardComponents)
.values(structure.components)
.run();
}
} }
private delete( private delete(
database: AppDatabase, database: AppDatabase,
projectId: string, projectId: string,
structure: DistributionBoardStructureSnapshot structure: NormalizedDistributionBoardStructureSnapshot
) { ) {
const board = database const board = database
.select() .select()
@@ -202,11 +208,30 @@ export class DistributionBoardStructureProjectCommandRepository
asc(circuitSections.id) asc(circuitSections.id)
) )
.all(); .all();
const components = database
.select()
.from(distributionBoardComponents)
.where(
eq(
distributionBoardComponents.circuitListId,
structure.circuitList.id
)
)
.orderBy(
asc(distributionBoardComponents.sortOrder),
asc(distributionBoardComponents.id)
)
.all();
if ( if (
!board || !board ||
!list || !list ||
board.projectId !== projectId || board.projectId !== projectId ||
!structureMatches(structure, { board, list, sections }) !structureMatches(structure, {
board,
list,
sections,
components,
})
) { ) {
throw new Error( throw new Error(
"Distribution-board structure changed before deletion." "Distribution-board structure changed before deletion."
@@ -239,7 +264,6 @@ export class DistributionBoardStructureProjectCommandRepository
if (deleted.changes !== 1) { if (deleted.changes !== 1) {
throw new Error("Distribution board could not be deleted."); throw new Error("Distribution board could not be deleted.");
} }
return createDistributionBoardInsertProjectCommand(structure);
} }
private update( private update(
@@ -354,28 +378,6 @@ function assertSupplyTypeEnabled(
} }
} }
function toLegacyStructureCommand(
command:
| ReturnType<typeof createDistributionBoardInsertProjectCommand>
| ReturnType<typeof createDistributionBoardDeleteProjectCommand>
): DistributionBoardStructureProjectCommand {
const {
floorId: _floorId,
supplyType: _supplyType,
...distributionBoard
} = command.payload.structure.distributionBoard;
return {
schemaVersion: legacyDistributionBoardStructureCommandSchemaVersion,
type: command.type,
payload: {
structure: {
...command.payload.structure,
distributionBoard,
},
},
} as DistributionBoardStructureProjectCommand;
}
function getDistributionBoardFieldValue< function getDistributionBoardFieldValue<
TField extends DistributionBoardUpdateField, TField extends DistributionBoardUpdateField,
>( >(
@@ -386,11 +388,12 @@ function getDistributionBoardFieldValue<
} }
function structureMatches( function structureMatches(
expected: DistributionBoardStructureSnapshot, expected: NormalizedDistributionBoardStructureSnapshot,
actual: { actual: {
board: typeof distributionBoards.$inferSelect; board: typeof distributionBoards.$inferSelect;
list: typeof circuitLists.$inferSelect; list: typeof circuitLists.$inferSelect;
sections: Array<typeof circuitSections.$inferSelect>; sections: Array<typeof circuitSections.$inferSelect>;
components: Array<typeof distributionBoardComponents.$inferSelect>;
} }
) { ) {
const { const {
@@ -404,7 +407,8 @@ function structureMatches(
actualDistributionBoard actualDistributionBoard
) || ) ||
!sameRecord(expected.circuitList, actual.list) || !sameRecord(expected.circuitList, actual.list) ||
expected.sections.length !== actual.sections.length expected.sections.length !== actual.sections.length ||
expected.components.length !== actual.components.length
) { ) {
return false; return false;
} }
@@ -413,59 +417,32 @@ function structureMatches(
left.sortOrder - right.sortOrder || left.sortOrder - right.sortOrder ||
left.id.localeCompare(right.id) left.id.localeCompare(right.id)
); );
return expectedSections.every((section, index) => { const sectionsMatch = expectedSections.every((section, index) =>
const actualSection = actual.sections[index]; sameRecord(section, actual.sections[index])
if ( );
actualSection.category !== initialCategoryBySectionKey(section.key) || const expectedComponents = [...expected.components].sort(
actualSection.groupNumber !== initialGroupNumberBySectionKey(section.key) (left, right) =>
) { left.sortOrder - right.sortOrder ||
return false; left.id.localeCompare(right.id)
} );
const {
category: _category,
groupNumber: _groupNumber,
...comparableActualSection
} = actualSection;
return sameRecord(section, comparableActualSection);
});
}
function withInitialGroupIdentity(
section: DistributionBoardStructureSnapshot["sections"][number]
): typeof circuitSections.$inferInsert {
return {
...section,
category: initialCategoryBySectionKey(section.key),
groupNumber: initialGroupNumberBySectionKey(section.key),
};
}
function initialCategoryBySectionKey(
key: string
): CircuitGroupCategory | null {
if (
key === "lighting" ||
key === "single_phase" ||
key === "three_phase"
) {
return key;
}
return null;
}
function initialGroupNumberBySectionKey(key: string): number | null {
return initialCategoryBySectionKey(key) === null ? null : 1;
}
function sameRecord(
expected: Record<string, unknown>,
actual: Record<string, unknown>
) {
const expectedEntries = Object.entries(expected);
return ( return (
expectedEntries.length === Object.keys(actual).length && sectionsMatch &&
expectedEntries.every( expectedComponents.every((component, index) =>
([key, value]) => actual[key] === value sameRecord(component, actual.components[index])
)
);
}
function sameRecord(
expected: object,
actual: object
) {
const expectedEntries = Object.entries(expected);
const actualRecord = actual as Record<string, unknown>;
return (
expectedEntries.length === Object.keys(actual).length &&
expectedEntries.every(
([key, value]) => actualRecord[key] === value
) )
); );
} }
@@ -3,6 +3,15 @@ import {
distributionBoardSupplyTypes, distributionBoardSupplyTypes,
type DistributionBoardSupplyType, type DistributionBoardSupplyType,
} from "../../shared/constants/distribution-board.js"; } from "../../shared/constants/distribution-board.js";
import {
defaultMainSwitchComponent,
defaultSurgeProtectiveDeviceComponent,
} from "../../shared/constants/distribution-board-component.js";
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
import type {
DistributionBoardComponentPlacement,
DistributionBoardComponentRole,
} from "../../shared/constants/distribution-board-component.js";
import type { SerializedProjectCommand } from "./project-command.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js";
export const distributionBoardInsertCommandType = export const distributionBoardInsertCommandType =
@@ -10,9 +19,10 @@ export const distributionBoardInsertCommandType =
export const distributionBoardDeleteCommandType = export const distributionBoardDeleteCommandType =
"distribution-board.delete" as const; "distribution-board.delete" as const;
export const legacyDistributionBoardStructureCommandSchemaVersion = 1 as const; export const legacyDistributionBoardStructureCommandSchemaVersion = 1 as const;
export const distributionBoardStructureCommandSchemaVersion = 2 as const; export const previousDistributionBoardStructureCommandSchemaVersion = 2 as const;
export const distributionBoardStructureCommandSchemaVersion = 3 as const;
export const defaultCircuitSectionDefinitions = [ const legacyDefaultCircuitSectionDefinitions = [
{ {
key: "lighting", key: "lighting",
displayName: "Lighting", displayName: "Lighting",
@@ -39,6 +49,33 @@ export const defaultCircuitSectionDefinitions = [
}, },
] as const; ] as const;
export const defaultCircuitSectionDefinitions = [
{
key: "lighting",
displayName: "Beleuchtung 1",
prefix: "-1F1.",
sortOrder: 10,
category: "lighting",
groupNumber: 1,
},
{
key: "single_phase",
displayName: "1-phasig 1",
prefix: "-2F1.",
sortOrder: 20,
category: "single_phase",
groupNumber: 1,
},
{
key: "three_phase",
displayName: "3-phasig 1",
prefix: "-3F1.",
sortOrder: 30,
category: "three_phase",
groupNumber: 1,
},
] as const;
interface LegacyDistributionBoardStructureSnapshot { interface LegacyDistributionBoardStructureSnapshot {
distributionBoard: { distributionBoard: {
id: string; id: string;
@@ -46,7 +83,22 @@ interface LegacyDistributionBoardStructureSnapshot {
name: string; name: string;
}; };
circuitList: DistributionBoardStructureSnapshot["circuitList"]; circuitList: DistributionBoardStructureSnapshot["circuitList"];
sections: DistributionBoardStructureSnapshot["sections"]; sections: LegacyCircuitSectionSnapshot[];
}
interface PreviousDistributionBoardStructureSnapshot {
distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"];
circuitList: DistributionBoardStructureSnapshot["circuitList"];
sections: LegacyCircuitSectionSnapshot[];
}
interface LegacyCircuitSectionSnapshot {
id: string;
circuitListId: string;
key: string;
displayName: string;
prefix: string;
sortOrder: number;
} }
export interface DistributionBoardStructureSnapshot { export interface DistributionBoardStructureSnapshot {
@@ -70,7 +122,31 @@ export interface DistributionBoardStructureSnapshot {
displayName: string; displayName: string;
prefix: string; prefix: string;
sortOrder: number; sortOrder: number;
category: CircuitGroupCategory;
groupNumber: number;
}>; }>;
components: Array<{
id: string;
circuitListId: string;
sectionId: null;
equipmentIdentifier: string;
name: string;
role: DistributionBoardComponentRole;
placement: DistributionBoardComponentPlacement;
sortOrder: number;
}>;
}
export interface NormalizedDistributionBoardStructureSnapshot {
distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"];
circuitList: DistributionBoardStructureSnapshot["circuitList"];
sections: Array<
LegacyCircuitSectionSnapshot & {
category: CircuitGroupCategory | null;
groupNumber: number | null;
}
>;
components: DistributionBoardStructureSnapshot["components"];
} }
interface DistributionBoardStructureCommandPayload< interface DistributionBoardStructureCommandPayload<
@@ -97,6 +173,16 @@ interface LegacyDistributionBoardStructureProjectCommand
| typeof distributionBoardDeleteCommandType; | typeof distributionBoardDeleteCommandType;
} }
interface PreviousDistributionBoardStructureProjectCommand
extends SerializedProjectCommand<
DistributionBoardStructureCommandPayload<PreviousDistributionBoardStructureSnapshot>
> {
schemaVersion: typeof previousDistributionBoardStructureCommandSchemaVersion;
type:
| typeof distributionBoardInsertCommandType
| typeof distributionBoardDeleteCommandType;
}
export interface DistributionBoardInsertProjectCommand export interface DistributionBoardInsertProjectCommand
extends CurrentDistributionBoardStructureProjectCommand { extends CurrentDistributionBoardStructureProjectCommand {
type: typeof distributionBoardInsertCommandType; type: typeof distributionBoardInsertCommandType;
@@ -110,6 +196,7 @@ export interface DistributionBoardDeleteProjectCommand
export type DistributionBoardStructureProjectCommand = export type DistributionBoardStructureProjectCommand =
| DistributionBoardInsertProjectCommand | DistributionBoardInsertProjectCommand
| DistributionBoardDeleteProjectCommand | DistributionBoardDeleteProjectCommand
| PreviousDistributionBoardStructureProjectCommand
| LegacyDistributionBoardStructureProjectCommand; | LegacyDistributionBoardStructureProjectCommand;
export function createDistributionBoardStructureSnapshot( export function createDistributionBoardStructureSnapshot(
@@ -143,6 +230,22 @@ export function createDistributionBoardStructureSnapshot(
circuitListId: structureId, circuitListId: structureId,
...section, ...section,
})), })),
components: [
{
id: crypto.randomUUID(),
circuitListId: structureId,
sectionId: null,
...defaultMainSwitchComponent,
sortOrder: 10,
},
{
id: crypto.randomUUID(),
circuitListId: structureId,
sectionId: null,
...defaultSurgeProtectiveDeviceComponent,
sortOrder: 20,
},
],
}; };
assertDistributionBoardStructureSnapshot(structure); assertDistributionBoardStructureSnapshot(structure);
return structure; return structure;
@@ -192,32 +295,71 @@ export function assertDistributionBoardDeleteProjectCommand(
export function normalizeDistributionBoardStructureProjectCommand( export function normalizeDistributionBoardStructureProjectCommand(
command: DistributionBoardStructureProjectCommand command: DistributionBoardStructureProjectCommand
): DistributionBoardInsertProjectCommand | DistributionBoardDeleteProjectCommand { ): NormalizedDistributionBoardStructureSnapshot {
if ( if (
command.schemaVersion === command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion legacyDistributionBoardStructureCommandSchemaVersion
) { ) {
const structure: DistributionBoardStructureSnapshot = { return {
...command.payload.structure,
distributionBoard: { distributionBoard: {
...command.payload.structure.distributionBoard, ...command.payload.structure.distributionBoard,
floorId: null, floorId: null,
supplyType: null, supplyType: null,
}, },
circuitList: command.payload.structure.circuitList,
sections: command.payload.structure.sections.map(
normalizeLegacySection
),
components: [],
}; };
return command.type === distributionBoardInsertCommandType
? createDistributionBoardInsertProjectCommand(structure)
: createDistributionBoardDeleteProjectCommand(structure);
} }
return command as if (
| DistributionBoardInsertProjectCommand command.schemaVersion ===
| DistributionBoardDeleteProjectCommand; previousDistributionBoardStructureCommandSchemaVersion
) {
return {
...command.payload.structure,
sections: command.payload.structure.sections.map(
normalizeLegacySection
),
components: [],
};
}
return command.payload.structure;
}
export function invertDistributionBoardStructureProjectCommand(
command: DistributionBoardStructureProjectCommand
): DistributionBoardStructureProjectCommand {
return {
...command,
type:
command.type === distributionBoardInsertCommandType
? distributionBoardDeleteCommandType
: distributionBoardInsertCommandType,
} as DistributionBoardStructureProjectCommand;
}
function normalizeLegacySection(
section: LegacyCircuitSectionSnapshot
): NormalizedDistributionBoardStructureSnapshot["sections"][number] {
const category =
section.key === "lighting" ||
section.key === "single_phase" ||
section.key === "three_phase"
? section.key
: null;
return {
...section,
category,
groupNumber: category === null ? null : 1,
};
} }
export function assertDistributionBoardStructureSnapshot( export function assertDistributionBoardStructureSnapshot(
structure: unknown structure: unknown
): asserts structure is DistributionBoardStructureSnapshot { ): asserts structure is DistributionBoardStructureSnapshot {
assertDistributionBoardStructureSnapshotVersion(structure, false); assertDistributionBoardStructureSnapshotVersion(structure, "current");
} }
function assertDistributionBoardStructureProjectCommand( function assertDistributionBoardStructureProjectCommand(
@@ -239,36 +381,57 @@ function assertDistributionBoardStructureProjectCommand(
) { ) {
assertDistributionBoardStructureSnapshotVersion( assertDistributionBoardStructureSnapshotVersion(
command.payload.structure, command.payload.structure,
true "legacy"
); );
return; return;
} }
if ( if (
command.schemaVersion !== distributionBoardStructureCommandSchemaVersion command.schemaVersion ===
previousDistributionBoardStructureCommandSchemaVersion
) {
assertDistributionBoardStructureSnapshotVersion(
command.payload.structure,
"previous"
);
return;
}
if (
command.schemaVersion !==
distributionBoardStructureCommandSchemaVersion
) { ) {
throw new Error("Unsupported distribution-board structure command."); throw new Error("Unsupported distribution-board structure command.");
} }
assertDistributionBoardStructureSnapshotVersion( assertDistributionBoardStructureSnapshotVersion(
command.payload.structure, command.payload.structure,
false "current"
); );
} }
function assertDistributionBoardStructureSnapshotVersion( function assertDistributionBoardStructureSnapshotVersion(
structure: unknown, structure: unknown,
legacy: boolean version: "legacy" | "previous" | "current"
) { ) {
if (!isPlainObject(structure) || Object.keys(structure).length !== 3) { const current = version === "current";
if (
!isPlainObject(structure) ||
Object.keys(structure).length !== (current ? 4 : 3)
) {
throw new Error("Distribution-board structure is invalid."); throw new Error("Distribution-board structure is invalid.");
} }
const { distributionBoard, circuitList, sections } = structure; const { distributionBoard, circuitList, sections, components } =
structure;
const expectedSections = current
? defaultCircuitSectionDefinitions
: legacyDefaultCircuitSectionDefinitions;
if ( if (
!isPlainObject(distributionBoard) || !isPlainObject(distributionBoard) ||
Object.keys(distributionBoard).length !== (legacy ? 3 : 5) || Object.keys(distributionBoard).length !==
(version === "legacy" ? 3 : 5) ||
!isPlainObject(circuitList) || !isPlainObject(circuitList) ||
Object.keys(circuitList).length !== 4 || Object.keys(circuitList).length !== 4 ||
!Array.isArray(sections) || !Array.isArray(sections) ||
sections.length !== defaultCircuitSectionDefinitions.length sections.length !== expectedSections.length ||
(current && (!Array.isArray(components) || components.length !== 2))
) { ) {
throw new Error("Distribution-board structure is incomplete."); throw new Error("Distribution-board structure is incomplete.");
} }
@@ -278,7 +441,7 @@ function assertDistributionBoardStructureSnapshotVersion(
`distributionBoard.${field}` `distributionBoard.${field}`
); );
} }
if (!legacy) { if (version !== "legacy") {
assertNullableId(distributionBoard.floorId, "distributionBoard.floorId"); assertNullableId(distributionBoard.floorId, "distributionBoard.floorId");
assertNullableSupplyType(distributionBoard.supplyType); assertNullableSupplyType(distributionBoard.supplyType);
} }
@@ -296,8 +459,11 @@ function assertDistributionBoardStructureSnapshotVersion(
const sectionIds = new Set<string>(); const sectionIds = new Set<string>();
for (let index = 0; index < sections.length; index += 1) { for (let index = 0; index < sections.length; index += 1) {
const section = sections[index]; const section = sections[index];
const expected = defaultCircuitSectionDefinitions[index]; const expected = expectedSections[index];
if (!isPlainObject(section) || Object.keys(section).length !== 6) { if (
!isPlainObject(section) ||
Object.keys(section).length !== (current ? 8 : 6)
) {
throw new Error("Default circuit section is invalid."); throw new Error("Default circuit section is invalid.");
} }
assertNonEmptyString(section.id, "section.id"); assertNonEmptyString(section.id, "section.id");
@@ -310,13 +476,64 @@ function assertDistributionBoardStructureSnapshotVersion(
section.key !== expected.key || section.key !== expected.key ||
section.displayName !== expected.displayName || section.displayName !== expected.displayName ||
section.prefix !== expected.prefix || section.prefix !== expected.prefix ||
section.sortOrder !== expected.sortOrder section.sortOrder !== expected.sortOrder ||
(current &&
(!("category" in expected) ||
section.category !== expected.category ||
section.groupNumber !== expected.groupNumber))
) { ) {
throw new Error( throw new Error(
"Distribution-board structure has invalid default sections." "Distribution-board structure has invalid default sections."
); );
} }
} }
if (current) {
assertDefaultComponents(components, circuitList.id);
}
}
function assertDefaultComponents(
components: unknown,
circuitListId: unknown
) {
if (!Array.isArray(components)) {
throw new Error("Default distribution-board components are invalid.");
}
const expectedComponents = [
{ ...defaultMainSwitchComponent, sortOrder: 10 },
{ ...defaultSurgeProtectiveDeviceComponent, sortOrder: 20 },
];
const componentIds = new Set<string>();
for (let index = 0; index < expectedComponents.length; index += 1) {
const component = components[index];
const expected = expectedComponents[index];
if (!isPlainObject(component) || Object.keys(component).length !== 8) {
throw new Error(
"Default distribution-board component is invalid."
);
}
assertNonEmptyString(component.id, "component.id");
if (componentIds.has(component.id)) {
throw new Error(
"Default distribution-board component ids must be unique."
);
}
componentIds.add(component.id);
if (
component.circuitListId !== circuitListId ||
component.sectionId !== null ||
component.equipmentIdentifier !== expected.equipmentIdentifier ||
component.name !== expected.name ||
component.role !== expected.role ||
component.placement !== expected.placement ||
component.sortOrder !== expected.sortOrder
) {
throw new Error(
"Distribution-board structure has invalid default components."
);
}
}
} }
function assertNullableId(value: unknown, field: string) { function assertNullableId(value: unknown, field: string) {
@@ -12,6 +12,7 @@ import { ProjectHistoryRepository } from "../src/db/repositories/project-history
import { circuitLists } from "../src/db/schema/circuit-lists.js"; import { circuitLists } from "../src/db/schema/circuit-lists.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 { 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 { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js";
@@ -51,10 +52,51 @@ function getStructureCounts(context: DatabaseContext) {
boards: context.db.select().from(distributionBoards).all().length, boards: context.db.select().from(distributionBoards).all().length,
lists: context.db.select().from(circuitLists).all().length, lists: context.db.select().from(circuitLists).all().length,
sections: context.db.select().from(circuitSections).all().length, sections: context.db.select().from(circuitSections).all().length,
components: context.db
.select()
.from(distributionBoardComponents)
.all().length,
revisions: context.db.select().from(projectRevisions).all().length, revisions: context.db.select().from(projectRevisions).all().length,
}; };
} }
function legacySections(circuitListId: string) {
return [
{
id: "legacy-lighting",
circuitListId,
key: "lighting",
displayName: "Lighting",
prefix: "-1F",
sortOrder: 10,
},
{
id: "legacy-single-phase",
circuitListId,
key: "single_phase",
displayName: "Single-phase circuits",
prefix: "-2F",
sortOrder: 20,
},
{
id: "legacy-three-phase",
circuitListId,
key: "three_phase",
displayName: "Three-phase circuits",
prefix: "-3F",
sortOrder: 30,
},
{
id: "legacy-unassigned",
circuitListId,
key: "unassigned",
displayName: "Unassigned",
prefix: "-UF",
sortOrder: 90,
},
];
}
describe("distribution-board structure project command", () => { describe("distribution-board structure project command", () => {
it("builds the fixed setup and sends the expected frontend revision", async () => { it("builds the fixed setup and sends the expected frontend revision", async () => {
const structure = createDistributionBoardStructureSnapshot( const structure = createDistributionBoardStructureSnapshot(
@@ -74,17 +116,26 @@ describe("distribution-board structure project command", () => {
section.prefix, section.prefix,
]), ]),
[ [
["lighting", "-1F"], ["lighting", "-1F1."],
["single_phase", "-2F"], ["single_phase", "-2F1."],
["three_phase", "-3F"], ["three_phase", "-3F1."],
["unassigned", "-UF"], ]
);
assert.deepEqual(
structure.components.map((component) => [
component.equipmentIdentifier,
component.name,
]),
[
["-Q0", "Hauptschalter"],
["-FA", "Überspannungsableiter"],
] ]
); );
assert.throws( assert.throws(
() => () =>
createDistributionBoardInsertProjectCommand({ createDistributionBoardInsertProjectCommand({
...structure, ...structure,
sections: structure.sections.slice(0, 3), sections: structure.sections.slice(0, 2),
}), }),
/incomplete/ /incomplete/
); );
@@ -150,7 +201,8 @@ describe("distribution-board structure project command", () => {
assert.deepEqual(getStructureCounts(context), { assert.deepEqual(getStructureCounts(context), {
boards: 1, boards: 1,
lists: 1, lists: 1,
sections: 4, sections: 3,
components: 2,
revisions: 1, revisions: 1,
}); });
@@ -167,6 +219,7 @@ describe("distribution-board structure project command", () => {
boards: 0, boards: 0,
lists: 0, lists: 0,
sections: 0, sections: 0,
components: 0,
revisions: 2, revisions: 2,
}); });
@@ -182,7 +235,8 @@ describe("distribution-board structure project command", () => {
assert.deepEqual(getStructureCounts(context), { assert.deepEqual(getStructureCounts(context), {
boards: 1, boards: 1,
lists: 1, lists: 1,
sections: 4, sections: 3,
components: 2,
revisions: 3, revisions: 3,
}); });
assert.ok( assert.ok(
@@ -274,8 +328,9 @@ describe("distribution-board structure project command", () => {
type: "distribution-board.insert", type: "distribution-board.insert",
payload: { payload: {
structure: { structure: {
...current,
distributionBoard: legacyBoard, distributionBoard: legacyBoard,
circuitList: current.circuitList,
sections: legacySections(current.circuitList.id),
}, },
}, },
} as unknown as DistributionBoardStructureProjectCommand; } as unknown as DistributionBoardStructureProjectCommand;
@@ -291,6 +346,59 @@ describe("distribution-board structure project command", () => {
.get(); .get();
assert.equal(inserted?.floorId, null); assert.equal(inserted?.floorId, null);
assert.equal(inserted?.supplyType, null); assert.equal(inserted?.supplyType, null);
assert.equal(
context.db.select().from(circuitSections).all().length,
4
);
assert.equal(
context.db.select().from(distributionBoardComponents).all()
.length,
0
);
} finally {
context.close();
}
});
it("keeps stored version-two setup commands component-free", () => {
const context = createTestDatabase();
try {
const repository =
new DistributionBoardStructureProjectCommandRepository(
context.db
);
const current = createDistributionBoardStructureSnapshot(
"project-1",
"UV Version 2"
);
const command = {
schemaVersion: 2,
type: "distribution-board.insert",
payload: {
structure: {
distributionBoard: current.distributionBoard,
circuitList: current.circuitList,
sections: legacySections(current.circuitList.id),
},
},
} as DistributionBoardStructureProjectCommand;
const inserted = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
assert.equal(inserted.inverse.schemaVersion, 2);
assert.equal(
context.db.select().from(circuitSections).all().length,
4
);
assert.equal(
context.db.select().from(distributionBoardComponents).all()
.length,
0
);
} finally { } finally {
context.close(); context.close();
} }
@@ -545,6 +653,7 @@ describe("distribution-board structure project command", () => {
boards: 0, boards: 0,
lists: 0, lists: 0,
sections: 0, sections: 0,
components: 0,
revisions: 0, revisions: 0,
}); });
@@ -661,6 +770,7 @@ describe("distribution-board structure project command", () => {
boards: 0, boards: 0,
lists: 0, lists: 0,
sections: 0, sections: 0,
components: 0,
revisions: 0, revisions: 0,
}); });
} finally { } finally {
+20 -6
View File
@@ -243,7 +243,13 @@ describe("project snapshot repository", () => {
); );
assert.equal(state.circuits.length, 1); assert.equal(state.circuits.length, 1);
assert.equal(state.circuitProtectionDevices.length, 1); assert.equal(state.circuitProtectionDevices.length, 1);
assert.equal(state.distributionBoardComponents.length, 1); assert.equal(
state.distributionBoardComponents.length,
context.db
.select()
.from(distributionBoardComponents)
.all().length
);
assert.equal( assert.equal(
state.distributionBoardComponentProtectionDevices.length, state.distributionBoardComponentProtectionDevices.length,
1 1
@@ -427,18 +433,26 @@ 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( assert.equal(copied.state.distributionBoardComponents.length, 3);
copied.state.distributionBoardComponents[0].id,
source.state.distributionBoardComponents[0].id
);
assert.equal( assert.equal(
copied.state.circuitProtectionDevices[0].circuitId, copied.state.circuitProtectionDevices[0].circuitId,
copied.state.circuits[0].id copied.state.circuits[0].id
); );
assert.equal( assert.equal(
copied.state.distributionBoardComponents.some(
(component) =>
component.id ===
copied.state
.distributionBoardComponentProtectionDevices[0]
.componentId
),
true
);
assert.notEqual(
copied.state.distributionBoardComponentProtectionDevices[0] copied.state.distributionBoardComponentProtectionDevices[0]
.componentId, .componentId,
copied.state.distributionBoardComponents[0].id source.state.distributionBoardComponentProtectionDevices[0]
.componentId
); );
assert.equal( assert.equal(
copied.state.circuits[0].deviceRows[0].linkedProjectDeviceId, copied.state.circuits[0].deviceRows[0].linkedProjectDeviceId,
@@ -1,6 +1,7 @@
import type { AppDatabase } from "../../src/db/database-context.js"; import type { AppDatabase } from "../../src/db/database-context.js";
import { circuitLists } from "../../src/db/schema/circuit-lists.js"; import { circuitLists } from "../../src/db/schema/circuit-lists.js";
import { circuitSections } from "../../src/db/schema/circuit-sections.js"; import { circuitSections } from "../../src/db/schema/circuit-sections.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 { createDistributionBoardStructureSnapshot } from "../../src/domain/models/distribution-board-structure-project-command.model.js"; import { createDistributionBoardStructureSnapshot } from "../../src/domain/models/distribution-board-structure-project-command.model.js";
@@ -21,6 +22,9 @@ export class DistributionBoardFixtureRepository {
.run(); .run();
tx.insert(circuitLists).values(structure.circuitList).run(); tx.insert(circuitLists).values(structure.circuitList).run();
tx.insert(circuitSections).values(structure.sections).run(); tx.insert(circuitSections).values(structure.sections).run();
tx.insert(distributionBoardComponents)
.values(structure.components)
.run();
}); });
return structure.distributionBoard; return structure.distributionBoard;
} }