Create protected board defaults
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user