diff --git a/AGENTS.md b/AGENTS.md index 25785c8..c25de10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 29e44c4..f3d9a49 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -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: diff --git a/docs/current-architecture.md b/docs/current-architecture.md index eca8dd4..bb30469 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -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 diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 0ee044c..7a5fb03 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -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. diff --git a/docs/spec/09-distribution-board-components-and-protection-groups.md b/docs/spec/09-distribution-board-components-and-protection-groups.md index 5cd077d..ad04919 100644 --- a/docs/spec/09-distribution-board-components-and-protection-groups.md +++ b/docs/spec/09-distribution-board-components-and-protection-groups.md @@ -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 diff --git a/src/db/repositories/distribution-board-structure-project-command.repository.ts b/src/db/repositories/distribution-board-structure-project-command.repository.ts index 8cded53..9e7ae6f 100644 --- a/src/db/repositories/distribution-board-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-structure-project-command.repository.ts @@ -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 - | ReturnType -): 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; + components: Array; } ) { 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, - actual: Record -) { - 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; + return ( + expectedEntries.length === Object.keys(actual).length && + expectedEntries.every( + ([key, value]) => actualRecord[key] === value ) ); } diff --git a/src/domain/models/distribution-board-structure-project-command.model.ts b/src/domain/models/distribution-board-structure-project-command.model.ts index 1ac0896..6553399 100644 --- a/src/domain/models/distribution-board-structure-project-command.model.ts +++ b/src/domain/models/distribution-board-structure-project-command.model.ts @@ -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 + > { + 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(); 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(); + 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) { diff --git a/tests/distribution-board-structure-project-command.repository.test.ts b/tests/distribution-board-structure-project-command.repository.test.ts index e73b54d..570d6b9 100644 --- a/tests/distribution-board-structure-project-command.repository.test.ts +++ b/tests/distribution-board-structure-project-command.repository.test.ts @@ -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 { diff --git a/tests/project-snapshot.repository.test.ts b/tests/project-snapshot.repository.test.ts index b59998f..d70dace 100644 --- a/tests/project-snapshot.repository.test.ts +++ b/tests/project-snapshot.repository.test.ts @@ -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, diff --git a/tests/support/distribution-board-fixture.ts b/tests/support/distribution-board-fixture.ts index 9e51a22..b752e4a 100644 --- a/tests/support/distribution-board-fixture.ts +++ b/tests/support/distribution-board-fixture.ts @@ -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; }