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`,
`USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board
cannot be disabled.
Distribution-board setup uses `distribution-board.insert` with a complete
stable snapshot of the board, circuit list and four default sections. Its
Distribution-board setup uses `distribution-board.insert` schema version 3
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
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.update`; both values are snapshot/export fields and one
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
be enabled in the project settings
- 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:
`{ "distributionBoard": { ... }, "revision": { ... }, "history": { ... } }`
- persistent Undo removes only the unchanged and still-empty generated
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`
- `PUT /projects/:projectId/distribution-boards/:distributionBoardId`
- 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
Ersetzen eines Projekts bleibt auf dessen Einstellungsmodal und den
projektgebundenen Endpunkt beschränkt.
`distribution-board.insert` versioniert die Anlage einer Verteilung als einen
vollständigen Block aus Verteilung, Stromkreisliste und vier Standardbereichen.
`distribution-board.insert` versioniert die Anlage einer Verteilung mit
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.
`distribution-board.delete` ist die persistierte Inverse und entfernt nur den
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
nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
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
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
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.
- [x] Phase B2: snapshot/transfer schema upgrade and deterministic compatibility
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 E: editor projection and editing.
- [ ] Phase F: documentation and full GUI verification.
@@ -593,11 +593,25 @@ Acceptance:
### 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
- implement component and group CRUD commands
- implement group and footer reordering
- 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:
- new boards contain the agreed fixed structure
@@ -2,14 +2,12 @@ import { and, asc, eq, inArray } from "drizzle-orm";
import {
assertDistributionBoardDeleteProjectCommand,
assertDistributionBoardInsertProjectCommand,
createDistributionBoardDeleteProjectCommand,
createDistributionBoardInsertProjectCommand,
distributionBoardDeleteCommandType,
distributionBoardInsertCommandType,
invertDistributionBoardStructureProjectCommand,
normalizeDistributionBoardStructureProjectCommand,
type DistributionBoardStructureProjectCommand,
type DistributionBoardStructureSnapshot,
legacyDistributionBoardStructureCommandSchemaVersion,
type NormalizedDistributionBoardStructureSnapshot,
} from "../../domain/models/distribution-board-structure-project-command.model.js";
import {
assertDistributionBoardUpdateProjectCommand,
@@ -24,11 +22,11 @@ import type {
DistributionBoardStructureProjectCommandStore,
ExecuteDistributionBoardStructureCommandInput,
} 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 { circuitLists } from "../schema/circuit-lists.js";
import { circuitSections } from "../schema/circuit-sections.js";
import { circuits } from "../schema/circuits.js";
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
import { distributionBoards } from "../schema/distribution-boards.js";
import { floors } from "../schema/floors.js";
import { projects } from "../schema/projects.js";
@@ -69,31 +67,15 @@ export class DistributionBoardStructureProjectCommandRepository
projectId: string,
command: DistributionBoardStructureProjectCommand
): DistributionBoardStructureProjectCommand {
const normalized =
const structure =
normalizeDistributionBoardStructureProjectCommand(command);
if (normalized.type === distributionBoardInsertCommandType) {
assertDistributionBoardInsertProjectCommand(normalized);
const inverse = this.insert(
database,
projectId,
normalized.payload.structure
);
return command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
? toLegacyStructureCommand(inverse)
: inverse;
if (command.type === distributionBoardInsertCommandType) {
this.insert(database, projectId, structure);
return invertDistributionBoardStructureProjectCommand(command);
}
if (normalized.type === distributionBoardDeleteCommandType) {
assertDistributionBoardDeleteProjectCommand(normalized);
const inverse = this.delete(
database,
projectId,
normalized.payload.structure
);
return command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
? toLegacyStructureCommand(inverse)
: inverse;
if (command.type === distributionBoardDeleteCommandType) {
this.delete(database, projectId, structure);
return invertDistributionBoardStructureProjectCommand(command);
}
throw new Error("Unsupported distribution-board structure command.");
}
@@ -101,7 +83,7 @@ export class DistributionBoardStructureProjectCommandRepository
private insert(
database: AppDatabase,
projectId: string,
structure: DistributionBoardStructureSnapshot
structure: NormalizedDistributionBoardStructureSnapshot
) {
if (
structure.distributionBoard.projectId !== projectId ||
@@ -160,7 +142,26 @@ export class DistributionBoardStructureProjectCommandRepository
)
.limit(1)
.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.");
}
@@ -171,15 +172,20 @@ export class DistributionBoardStructureProjectCommandRepository
database.insert(circuitLists).values(structure.circuitList).run();
database
.insert(circuitSections)
.values(structure.sections.map(withInitialGroupIdentity))
.values(structure.sections)
.run();
return createDistributionBoardDeleteProjectCommand(structure);
if (structure.components.length > 0) {
database
.insert(distributionBoardComponents)
.values(structure.components)
.run();
}
}
private delete(
database: AppDatabase,
projectId: string,
structure: DistributionBoardStructureSnapshot
structure: NormalizedDistributionBoardStructureSnapshot
) {
const board = database
.select()
@@ -202,11 +208,30 @@ export class DistributionBoardStructureProjectCommandRepository
asc(circuitSections.id)
)
.all();
const components = database
.select()
.from(distributionBoardComponents)
.where(
eq(
distributionBoardComponents.circuitListId,
structure.circuitList.id
)
)
.orderBy(
asc(distributionBoardComponents.sortOrder),
asc(distributionBoardComponents.id)
)
.all();
if (
!board ||
!list ||
board.projectId !== projectId ||
!structureMatches(structure, { board, list, sections })
!structureMatches(structure, {
board,
list,
sections,
components,
})
) {
throw new Error(
"Distribution-board structure changed before deletion."
@@ -239,7 +264,6 @@ export class DistributionBoardStructureProjectCommandRepository
if (deleted.changes !== 1) {
throw new Error("Distribution board could not be deleted.");
}
return createDistributionBoardInsertProjectCommand(structure);
}
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<
TField extends DistributionBoardUpdateField,
>(
@@ -386,11 +388,12 @@ function getDistributionBoardFieldValue<
}
function structureMatches(
expected: DistributionBoardStructureSnapshot,
expected: NormalizedDistributionBoardStructureSnapshot,
actual: {
board: typeof distributionBoards.$inferSelect;
list: typeof circuitLists.$inferSelect;
sections: Array<typeof circuitSections.$inferSelect>;
components: Array<typeof distributionBoardComponents.$inferSelect>;
}
) {
const {
@@ -404,7 +407,8 @@ function structureMatches(
actualDistributionBoard
) ||
!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;
}
@@ -413,59 +417,32 @@ function structureMatches(
left.sortOrder - right.sortOrder ||
left.id.localeCompare(right.id)
);
return expectedSections.every((section, index) => {
const actualSection = actual.sections[index];
if (
actualSection.category !== initialCategoryBySectionKey(section.key) ||
actualSection.groupNumber !== initialGroupNumberBySectionKey(section.key)
) {
return false;
}
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);
const sectionsMatch = expectedSections.every((section, index) =>
sameRecord(section, actual.sections[index])
);
const expectedComponents = [...expected.components].sort(
(left, right) =>
left.sortOrder - right.sortOrder ||
left.id.localeCompare(right.id)
);
return (
expectedEntries.length === Object.keys(actual).length &&
expectedEntries.every(
([key, value]) => actual[key] === value
sectionsMatch &&
expectedComponents.every((component, index) =>
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,
type DistributionBoardSupplyType,
} 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";
export const distributionBoardInsertCommandType =
@@ -10,9 +19,10 @@ export const distributionBoardInsertCommandType =
export const distributionBoardDeleteCommandType =
"distribution-board.delete" 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",
displayName: "Lighting",
@@ -39,6 +49,33 @@ export const defaultCircuitSectionDefinitions = [
},
] 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 {
distributionBoard: {
id: string;
@@ -46,7 +83,22 @@ interface LegacyDistributionBoardStructureSnapshot {
name: string;
};
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 {
@@ -70,7 +122,31 @@ export interface DistributionBoardStructureSnapshot {
displayName: string;
prefix: string;
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<
@@ -97,6 +173,16 @@ interface LegacyDistributionBoardStructureProjectCommand
| typeof distributionBoardDeleteCommandType;
}
interface PreviousDistributionBoardStructureProjectCommand
extends SerializedProjectCommand<
DistributionBoardStructureCommandPayload<PreviousDistributionBoardStructureSnapshot>
> {
schemaVersion: typeof previousDistributionBoardStructureCommandSchemaVersion;
type:
| typeof distributionBoardInsertCommandType
| typeof distributionBoardDeleteCommandType;
}
export interface DistributionBoardInsertProjectCommand
extends CurrentDistributionBoardStructureProjectCommand {
type: typeof distributionBoardInsertCommandType;
@@ -110,6 +196,7 @@ export interface DistributionBoardDeleteProjectCommand
export type DistributionBoardStructureProjectCommand =
| DistributionBoardInsertProjectCommand
| DistributionBoardDeleteProjectCommand
| PreviousDistributionBoardStructureProjectCommand
| LegacyDistributionBoardStructureProjectCommand;
export function createDistributionBoardStructureSnapshot(
@@ -143,6 +230,22 @@ export function createDistributionBoardStructureSnapshot(
circuitListId: structureId,
...section,
})),
components: [
{
id: crypto.randomUUID(),
circuitListId: structureId,
sectionId: null,
...defaultMainSwitchComponent,
sortOrder: 10,
},
{
id: crypto.randomUUID(),
circuitListId: structureId,
sectionId: null,
...defaultSurgeProtectiveDeviceComponent,
sortOrder: 20,
},
],
};
assertDistributionBoardStructureSnapshot(structure);
return structure;
@@ -192,32 +295,71 @@ export function assertDistributionBoardDeleteProjectCommand(
export function normalizeDistributionBoardStructureProjectCommand(
command: DistributionBoardStructureProjectCommand
): DistributionBoardInsertProjectCommand | DistributionBoardDeleteProjectCommand {
): NormalizedDistributionBoardStructureSnapshot {
if (
command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
) {
const structure: DistributionBoardStructureSnapshot = {
...command.payload.structure,
return {
distributionBoard: {
...command.payload.structure.distributionBoard,
floorId: 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
| DistributionBoardInsertProjectCommand
| DistributionBoardDeleteProjectCommand;
if (
command.schemaVersion ===
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(
structure: unknown
): asserts structure is DistributionBoardStructureSnapshot {
assertDistributionBoardStructureSnapshotVersion(structure, false);
assertDistributionBoardStructureSnapshotVersion(structure, "current");
}
function assertDistributionBoardStructureProjectCommand(
@@ -239,36 +381,57 @@ function assertDistributionBoardStructureProjectCommand(
) {
assertDistributionBoardStructureSnapshotVersion(
command.payload.structure,
true
"legacy"
);
return;
}
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.");
}
assertDistributionBoardStructureSnapshotVersion(
command.payload.structure,
false
"current"
);
}
function assertDistributionBoardStructureSnapshotVersion(
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.");
}
const { distributionBoard, circuitList, sections } = structure;
const { distributionBoard, circuitList, sections, components } =
structure;
const expectedSections = current
? defaultCircuitSectionDefinitions
: legacyDefaultCircuitSectionDefinitions;
if (
!isPlainObject(distributionBoard) ||
Object.keys(distributionBoard).length !== (legacy ? 3 : 5) ||
Object.keys(distributionBoard).length !==
(version === "legacy" ? 3 : 5) ||
!isPlainObject(circuitList) ||
Object.keys(circuitList).length !== 4 ||
!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.");
}
@@ -278,7 +441,7 @@ function assertDistributionBoardStructureSnapshotVersion(
`distributionBoard.${field}`
);
}
if (!legacy) {
if (version !== "legacy") {
assertNullableId(distributionBoard.floorId, "distributionBoard.floorId");
assertNullableSupplyType(distributionBoard.supplyType);
}
@@ -296,8 +459,11 @@ function assertDistributionBoardStructureSnapshotVersion(
const sectionIds = new Set<string>();
for (let index = 0; index < sections.length; index += 1) {
const section = sections[index];
const expected = defaultCircuitSectionDefinitions[index];
if (!isPlainObject(section) || Object.keys(section).length !== 6) {
const expected = expectedSections[index];
if (
!isPlainObject(section) ||
Object.keys(section).length !== (current ? 8 : 6)
) {
throw new Error("Default circuit section is invalid.");
}
assertNonEmptyString(section.id, "section.id");
@@ -310,13 +476,64 @@ function assertDistributionBoardStructureSnapshotVersion(
section.key !== expected.key ||
section.displayName !== expected.displayName ||
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(
"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) {
@@ -12,6 +12,7 @@ import { ProjectHistoryRepository } from "../src/db/repositories/project-history
import { circuitLists } from "../src/db/schema/circuit-lists.js";
import { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js";
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
import { floors } from "../src/db/schema/floors.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,
lists: context.db.select().from(circuitLists).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,
};
}
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", () => {
it("builds the fixed setup and sends the expected frontend revision", async () => {
const structure = createDistributionBoardStructureSnapshot(
@@ -74,17 +116,26 @@ describe("distribution-board structure project command", () => {
section.prefix,
]),
[
["lighting", "-1F"],
["single_phase", "-2F"],
["three_phase", "-3F"],
["unassigned", "-UF"],
["lighting", "-1F1."],
["single_phase", "-2F1."],
["three_phase", "-3F1."],
]
);
assert.deepEqual(
structure.components.map((component) => [
component.equipmentIdentifier,
component.name,
]),
[
["-Q0", "Hauptschalter"],
["-FA", "Überspannungsableiter"],
]
);
assert.throws(
() =>
createDistributionBoardInsertProjectCommand({
...structure,
sections: structure.sections.slice(0, 3),
sections: structure.sections.slice(0, 2),
}),
/incomplete/
);
@@ -150,7 +201,8 @@ describe("distribution-board structure project command", () => {
assert.deepEqual(getStructureCounts(context), {
boards: 1,
lists: 1,
sections: 4,
sections: 3,
components: 2,
revisions: 1,
});
@@ -167,6 +219,7 @@ describe("distribution-board structure project command", () => {
boards: 0,
lists: 0,
sections: 0,
components: 0,
revisions: 2,
});
@@ -182,7 +235,8 @@ describe("distribution-board structure project command", () => {
assert.deepEqual(getStructureCounts(context), {
boards: 1,
lists: 1,
sections: 4,
sections: 3,
components: 2,
revisions: 3,
});
assert.ok(
@@ -274,8 +328,9 @@ describe("distribution-board structure project command", () => {
type: "distribution-board.insert",
payload: {
structure: {
...current,
distributionBoard: legacyBoard,
circuitList: current.circuitList,
sections: legacySections(current.circuitList.id),
},
},
} as unknown as DistributionBoardStructureProjectCommand;
@@ -291,6 +346,59 @@ describe("distribution-board structure project command", () => {
.get();
assert.equal(inserted?.floorId, 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 {
context.close();
}
@@ -545,6 +653,7 @@ describe("distribution-board structure project command", () => {
boards: 0,
lists: 0,
sections: 0,
components: 0,
revisions: 0,
});
@@ -661,6 +770,7 @@ describe("distribution-board structure project command", () => {
boards: 0,
lists: 0,
sections: 0,
components: 0,
revisions: 0,
});
} finally {
+20 -6
View File
@@ -243,7 +243,13 @@ describe("project snapshot repository", () => {
);
assert.equal(state.circuits.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(
state.distributionBoardComponentProtectionDevices.length,
1
@@ -427,18 +433,26 @@ describe("portable project transfer", () => {
copied.state.circuits[0].id,
source.state.circuits[0].id
);
assert.notEqual(
copied.state.distributionBoardComponents[0].id,
source.state.distributionBoardComponents[0].id
);
assert.equal(copied.state.distributionBoardComponents.length, 3);
assert.equal(
copied.state.circuitProtectionDevices[0].circuitId,
copied.state.circuits[0].id
);
assert.equal(
copied.state.distributionBoardComponents.some(
(component) =>
component.id ===
copied.state
.distributionBoardComponentProtectionDevices[0]
.componentId
),
true
);
assert.notEqual(
copied.state.distributionBoardComponentProtectionDevices[0]
.componentId,
copied.state.distributionBoardComponents[0].id
source.state.distributionBoardComponentProtectionDevices[0]
.componentId
);
assert.equal(
copied.state.circuits[0].deviceRows[0].linkedProjectDeviceId,
@@ -1,6 +1,7 @@
import type { AppDatabase } from "../../src/db/database-context.js";
import { circuitLists } from "../../src/db/schema/circuit-lists.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 { createDistributionBoardStructureSnapshot } from "../../src/domain/models/distribution-board-structure-project-command.model.js";
@@ -21,6 +22,9 @@ export class DistributionBoardFixtureRepository {
.run();
tx.insert(circuitLists).values(structure.circuitList).run();
tx.insert(circuitSections).values(structure.sections).run();
tx.insert(distributionBoardComponents)
.values(structure.components)
.run();
});
return structure.distributionBoard;
}