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
@@ -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) {