From 7a9530a914a6b8cae0feefb7dc0eeff3df8cb834 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Thu, 30 Jul 2026 20:14:45 +0200 Subject: [PATCH] Expose protected tree structure --- AGENTS.md | 4 + docs/circuit-list-editor-api.md | 50 ++++- docs/current-architecture.md | 7 + docs/spec/08-current-product-backlog.md | 2 + .../circuit-protection-device.repository.ts | 17 ++ ...distribution-board-component.repository.ts | 36 ++++ src/domain/models/circuit-tree.model.ts | 38 ++++ src/frontend/types.ts | 38 ++++ .../composition/application-repositories.ts | 6 + .../controllers/circuit-tree.controller.ts | 81 ++++++- tests/circuit-tree.controller.test.ts | 200 +++++++++++++++++- 11 files changed, 474 insertions(+), 5 deletions(-) create mode 100644 src/db/repositories/circuit-protection-device.repository.ts create mode 100644 src/db/repositories/distribution-board-component.repository.ts diff --git a/AGENTS.md b/AGENTS.md index e023890..fdee43a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -332,6 +332,10 @@ snapshot contains the group, optional component protections, complete circuits, circuit protections and all device-row link/override metadata. Delete re-captures and compares the subtree before cascading; restore preserves every UUID in foreign-key-safe order. +The circuit-tree read model exposes fixed `headerComponents`, group-owned +`components`, `footerComponents`, optional group category/number and optional +one-to-one protection DTOs for circuits and group components. These are +additive to the retained flat circuit protection fields during the transition. 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 f3d9a49..2062d1b 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -230,7 +230,11 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`. ### Tree Endpoint - `GET /projects/:projectId/circuit-lists/:circuitListId/tree` -- Purpose: returns section/circuit/device-row tree with calculated row and circuit totals. +- Purpose: returns fixed header components, grouped sections with optional + protection components, complete circuit/device-row blocks and auxiliary + footer components. Circuit and component protection devices use their + separate one-to-one DTOs. Calculated row, circuit, section and board totals + remain included. Response sketch: @@ -238,16 +242,47 @@ Response sketch: { "circuitListId": "cl_1", "currentRevision": 12, + "headerComponents": [ + { + "id": "cmp_main", + "equipmentIdentifier": "-Q0", + "name": "Hauptschalter", + "role": "main_switch", + "placement": "header" + } + ], "sections": [ { "id": "sec_1", "key": "lighting", - "prefix": "-1F", + "prefix": "-1F1.", + "category": "lighting", + "groupNumber": 1, + "components": [ + { + "id": "cmp_rcd", + "equipmentIdentifier": "-1Q1.0", + "name": "Gruppen-FI", + "role": "group_residual_current_protection", + "placement": "group", + "protectionDevice": { + "type": "FI", + "ratedCurrentA": 40, + "rcdType": "A", + "ratedResidualCurrentMa": 30 + } + } + ], "circuits": [ { "id": "cir_1", - "equipmentIdentifier": "-1F1", + "equipmentIdentifier": "-1F1.1", "circuitTotalPower": 4.2, + "protectionDevice": { + "type": "LS", + "ratedCurrentA": 10, + "tripCharacteristic": "B" + }, "deviceRows": [ { "id": "row_1", @@ -258,6 +293,15 @@ Response sketch: } ] } + ], + "footerComponents": [ + { + "id": "cmp_actor", + "equipmentIdentifier": "-K1", + "name": "KNX Schaltaktor", + "role": "auxiliary", + "placement": "footer" + } ] } ``` diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 9b989c8..a53bbe6 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -343,6 +343,13 @@ Snapshot. `circuit-group.restore-subtree` schreibt ihn in FK-sicherer Reihenfolg mit denselben UUIDs zurück. Beide Richtungen laufen als vollständige inverse Projekt-Commands; späte Fehler hinterlassen weder Teillöschungen noch Teilwiederherstellungen. +Der Circuit-Tree liefert zusätzlich `headerComponents`, +gruppenbezogene `components` und `footerComponents`. Abschnitte enthalten +optionale Gruppenkategorie und -nummer; Stromkreise und Gruppenkomponenten +enthalten ihre optionale getrennte Schutzgerätekonfiguration. Die bisherigen +flachen Stromkreis-Schutzfelder bleiben während der Übergangsphase additiv +erhalten. Separate aktive Read-Repositories lesen Komponenten- und beide +Schutzgerätetabellen; Schreibzugriffe bleiben ausschließlich in Commands. `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 b718704..f4849c6 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -31,6 +31,8 @@ requirements and intended sequencing, not proof of implementation. - [x] Phase D2b: persistent same-category circuit-move command. - [x] Phase D3a: complete validated group-subtree snapshot and warning summary. - [x] Phase D3b: persistent populated-group delete/restore command. +- [x] Phase E1: tree read model for components, groups and protection devices. +- [ ] Phase E2: header/group/footer editor projection. - [ ] Phase E: editor projection and editing. - [ ] Phase F: documentation and full GUI verification. - [ ] Keep full electrical sizing and cable-dimensioning rules separate until diff --git a/src/db/repositories/circuit-protection-device.repository.ts b/src/db/repositories/circuit-protection-device.repository.ts new file mode 100644 index 0000000..06b00f5 --- /dev/null +++ b/src/db/repositories/circuit-protection-device.repository.ts @@ -0,0 +1,17 @@ +import { inArray } from "drizzle-orm"; +import type { AppDatabase } from "../database-context.js"; +import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; + +export class CircuitProtectionDeviceRepository { + constructor(private readonly database: AppDatabase) {} + + async listByCircuitIds(circuitIds: string[]) { + if (circuitIds.length === 0) { + return []; + } + return this.database + .select() + .from(circuitProtectionDevices) + .where(inArray(circuitProtectionDevices.circuitId, circuitIds)); + } +} diff --git a/src/db/repositories/distribution-board-component.repository.ts b/src/db/repositories/distribution-board-component.repository.ts new file mode 100644 index 0000000..ab60603 --- /dev/null +++ b/src/db/repositories/distribution-board-component.repository.ts @@ -0,0 +1,36 @@ +import { asc, eq, inArray } from "drizzle-orm"; +import type { AppDatabase } from "../database-context.js"; +import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; +import { distributionBoardComponents } from "../schema/distribution-board-components.js"; + +export class DistributionBoardComponentRepository { + constructor(private readonly database: AppDatabase) {} + + async listByCircuitList(circuitListId: string) { + return this.database + .select() + .from(distributionBoardComponents) + .where( + eq(distributionBoardComponents.circuitListId, circuitListId) + ) + .orderBy( + asc(distributionBoardComponents.sortOrder), + asc(distributionBoardComponents.id) + ); + } + + async listProtectionByComponentIds(componentIds: string[]) { + if (componentIds.length === 0) { + return []; + } + return this.database + .select() + .from(distributionBoardComponentProtectionDevices) + .where( + inArray( + distributionBoardComponentProtectionDevices.componentId, + componentIds + ) + ); + } +} diff --git a/src/domain/models/circuit-tree.model.ts b/src/domain/models/circuit-tree.model.ts index 7e0449f..dee49ce 100644 --- a/src/domain/models/circuit-tree.model.ts +++ b/src/domain/models/circuit-tree.model.ts @@ -44,6 +44,7 @@ export interface CircuitTreeCircuit { remark?: string; circuitTotalPower: number; deviceRows: CircuitTreeDeviceRow[]; + protectionDevice?: CircuitTreeProtectionDevice; } export interface CircuitTreeSectionBlock { @@ -52,7 +53,10 @@ export interface CircuitTreeSectionBlock { displayName: string; prefix: string; sortOrder: number; + category?: CircuitGroupCategory; + groupNumber?: number; sectionTotalPower: number; + components: CircuitTreeComponent[]; circuits: CircuitTreeCircuit[]; } @@ -64,7 +68,9 @@ export interface CircuitTreeResponse { distributionBoardSimultaneityFactor: number; distributionBoardTotalPower: number; distributionBoardTotalPowerWithSimultaneityFactor: number; + headerComponents: CircuitTreeComponent[]; sections: CircuitTreeSectionBlock[]; + footerComponents: CircuitTreeComponent[]; } export interface LegacyMigrationReport { @@ -78,3 +84,35 @@ export interface LegacyMigrationReport { warnings: string[]; } +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; +import type { + DistributionBoardComponentPlacement, + DistributionBoardComponentRole, +} from "../../shared/constants/distribution-board-component.js"; +import type { + BreakerTripCharacteristic, + FuseUtilizationCategory, + ProtectionDeviceType, + RcdType, +} from "../../shared/constants/protection-device.js"; + +export interface CircuitTreeProtectionDevice { + type: ProtectionDeviceType; + ratedCurrentA: number; + fuseUtilizationCategory?: FuseUtilizationCategory; + tripCharacteristic?: BreakerTripCharacteristic; + rcdType?: RcdType; + ratedResidualCurrentMa?: number; +} + +export interface CircuitTreeComponent { + id: string; + circuitListId: string; + sectionId?: string; + equipmentIdentifier: string; + name: string; + role: DistributionBoardComponentRole; + placement: DistributionBoardComponentPlacement; + sortOrder: number; + protectionDevice?: CircuitTreeProtectionDevice; +} diff --git a/src/frontend/types.ts b/src/frontend/types.ts index f5e30e4..b0e9887 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -1,4 +1,15 @@ import type { DistributionBoardSupplyType } from "../shared/constants/distribution-board"; +import type { CircuitGroupCategory } from "../shared/constants/circuit-group"; +import type { + DistributionBoardComponentPlacement, + DistributionBoardComponentRole, +} from "../shared/constants/distribution-board-component"; +import type { + BreakerTripCharacteristic, + FuseUtilizationCategory, + ProtectionDeviceType, + RcdType, +} from "../shared/constants/protection-device"; export interface ProjectDto { id: string; @@ -267,6 +278,27 @@ export interface CircuitTreeDeviceRowDto { rowTotalPower: number; } +export interface CircuitTreeProtectionDeviceDto { + type: ProtectionDeviceType; + ratedCurrentA: number; + fuseUtilizationCategory?: FuseUtilizationCategory; + tripCharacteristic?: BreakerTripCharacteristic; + rcdType?: RcdType; + ratedResidualCurrentMa?: number; +} + +export interface CircuitTreeComponentDto { + id: string; + circuitListId: string; + sectionId?: string; + equipmentIdentifier: string; + name: string; + role: DistributionBoardComponentRole; + placement: DistributionBoardComponentPlacement; + sortOrder: number; + protectionDevice?: CircuitTreeProtectionDeviceDto; +} + export interface CircuitTreeCircuitDto { id: string; circuitListId: string; @@ -289,6 +321,7 @@ export interface CircuitTreeCircuitDto { remark?: string; circuitTotalPower: number; deviceRows: CircuitTreeDeviceRowDto[]; + protectionDevice?: CircuitTreeProtectionDeviceDto; } export interface CircuitTreeSectionDto { @@ -297,7 +330,10 @@ export interface CircuitTreeSectionDto { displayName: string; prefix: string; sortOrder: number; + category?: CircuitGroupCategory; + groupNumber?: number; sectionTotalPower: number; + components: CircuitTreeComponentDto[]; circuits: CircuitTreeCircuitDto[]; } @@ -320,7 +356,9 @@ export interface CircuitTreeResponseDto { distributionBoardSimultaneityFactor: number; distributionBoardTotalPower: number; distributionBoardTotalPowerWithSimultaneityFactor: number; + headerComponents: CircuitTreeComponentDto[]; sections: CircuitTreeSectionDto[]; + footerComponents: CircuitTreeComponentDto[]; migrationReport?: CircuitTreeMigrationReportDto; } diff --git a/src/server/composition/application-repositories.ts b/src/server/composition/application-repositories.ts index 8812141..a880b41 100644 --- a/src/server/composition/application-repositories.ts +++ b/src/server/composition/application-repositories.ts @@ -3,6 +3,8 @@ import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js"; import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js"; import { CircuitRepository } from "../../db/repositories/circuit.repository.js"; +import { CircuitProtectionDeviceRepository } from "../../db/repositories/circuit-protection-device.repository.js"; +import { DistributionBoardComponentRepository } from "../../db/repositories/distribution-board-component.repository.js"; import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js"; import { FloorRepository } from "../../db/repositories/floor.repository.js"; import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js"; @@ -15,6 +17,10 @@ export const circuitDeviceRowRepository = export const circuitListRepository = new CircuitListRepository(db); export const circuitSectionRepository = new CircuitSectionRepository(db); export const circuitRepository = new CircuitRepository(db); +export const circuitProtectionDeviceRepository = + new CircuitProtectionDeviceRepository(db); +export const distributionBoardComponentRepository = + new DistributionBoardComponentRepository(db); export const distributionBoardRepository = new DistributionBoardRepository(db); export const floorRepository = new FloorRepository(db); diff --git a/src/server/controllers/circuit-tree.controller.ts b/src/server/controllers/circuit-tree.controller.ts index 724e20d..4957e2e 100644 --- a/src/server/controllers/circuit-tree.controller.ts +++ b/src/server/controllers/circuit-tree.controller.ts @@ -7,11 +7,14 @@ import { calculateSectionTotalPower, } from "../../domain/calculations/circuit-power-calculation.js"; import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js"; +import type { CircuitTreeProtectionDevice } from "../../domain/models/circuit-tree.model.js"; import { circuitDeviceRowRepository, circuitListRepository, + circuitProtectionDeviceRepository, circuitRepository, circuitSectionRepository, + distributionBoardComponentRepository, distributionBoardRepository, projectRepository, } from "../composition/application-repositories.js"; @@ -23,7 +26,9 @@ export function isMissingCircuitTreeSchemaError(error: unknown): boolean { return ( error.message.includes("no such table: circuit_sections") || error.message.includes("no such table: circuits") || - error.message.includes("no such table: circuit_device_rows") + error.message.includes("no such table: circuit_device_rows") || + error.message.includes("no such table: distribution_board_components") || + error.message.includes("no such table: circuit_protection_devices") ); } @@ -56,9 +61,28 @@ export async function getCircuitTree(req: Request, res: Response) { const sections = await circuitSectionRepository.listByCircuitList(circuitListId); const circuits = await circuitRepository.listByCircuitList(circuitListId); const rows = await circuitDeviceRowRepository.listByCircuitList(circuits.map((entry) => entry.id)); + const components = + await distributionBoardComponentRepository.listByCircuitList( + circuitListId + ); + const [circuitProtections, componentProtections] = + await Promise.all([ + circuitProtectionDeviceRepository.listByCircuitIds( + circuits.map((entry) => entry.id) + ), + distributionBoardComponentRepository.listProtectionByComponentIds( + components.map((entry) => entry.id) + ), + ]); const sectionById = new Map(sections.map((section) => [section.id, section])); const rowsByCircuitId = new Map(); + const circuitProtectionById = new Map( + circuitProtections.map((entry) => [entry.circuitId, entry]) + ); + const componentProtectionById = new Map( + componentProtections.map((entry) => [entry.componentId, entry]) + ); for (const row of rows) { if (!rowsByCircuitId.has(row.circuitId)) { rowsByCircuitId.set(row.circuitId, []); @@ -75,17 +99,46 @@ export async function getCircuitTree(req: Request, res: Response) { distributionBoard.simultaneityFactor, distributionBoardTotalPower: 0, distributionBoardTotalPowerWithSimultaneityFactor: 0, + headerComponents: [], sections: sections.map((section) => ({ id: section.id, key: section.key, displayName: section.displayName, prefix: section.prefix, sortOrder: section.sortOrder, + category: section.category ?? undefined, + groupNumber: section.groupNumber ?? undefined, sectionTotalPower: 0, + components: [], circuits: [], })), + footerComponents: [], }; const sectionBlocks = new Map(tree.sections.map((section) => [section.id, section])); + for (const component of components) { + const protection = + componentProtectionById.get(component.id); + const dto = { + id: component.id, + circuitListId: component.circuitListId, + sectionId: component.sectionId ?? undefined, + equipmentIdentifier: component.equipmentIdentifier, + name: component.name, + role: component.role, + placement: component.placement, + sortOrder: component.sortOrder, + protectionDevice: protection + ? toProtectionDeviceDto(protection) + : undefined, + }; + if (component.placement === "header") { + tree.headerComponents.push(dto); + } else if (component.placement === "footer") { + tree.footerComponents.push(dto); + } else if (component.sectionId !== null) { + sectionBlocks.get(component.sectionId)?.components.push(dto); + } + } for (const circuit of circuits) { const section = sectionById.get(circuit.sectionId); @@ -116,6 +169,7 @@ export async function getCircuitTree(req: Request, res: Response) { rowTotalPower: calculateRowTotalPower(row.quantity, row.powerPerUnit, row.simultaneityFactor), })); const circuitTotalPower = calculateCircuitTotalPower(deviceRows); + const protection = circuitProtectionById.get(circuit.id); sectionBlocks.get(section.id)?.circuits.push({ id: circuit.id, @@ -139,6 +193,9 @@ export async function getCircuitTree(req: Request, res: Response) { remark: circuit.remark ?? undefined, circuitTotalPower, deviceRows, + protectionDevice: protection + ? toProtectionDeviceDto(protection) + : undefined, }); } @@ -167,7 +224,9 @@ export async function getCircuitTree(req: Request, res: Response) { distributionBoard.simultaneityFactor, distributionBoardTotalPower: 0, distributionBoardTotalPowerWithSimultaneityFactor: 0, + headerComponents: [], sections: [], + footerComponents: [], warning: "Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).", }); @@ -175,3 +234,23 @@ export async function getCircuitTree(req: Request, res: Response) { throw error; } } + +function toProtectionDeviceDto(value: { + type: CircuitTreeProtectionDevice["type"]; + ratedCurrentA: number; + fuseUtilizationCategory: CircuitTreeProtectionDevice["fuseUtilizationCategory"] | null; + tripCharacteristic: CircuitTreeProtectionDevice["tripCharacteristic"] | null; + rcdType: CircuitTreeProtectionDevice["rcdType"] | null; + ratedResidualCurrentMa: number | null; +}): CircuitTreeProtectionDevice { + return { + type: value.type, + ratedCurrentA: value.ratedCurrentA, + fuseUtilizationCategory: + value.fuseUtilizationCategory ?? undefined, + tripCharacteristic: value.tripCharacteristic ?? undefined, + rcdType: value.rcdType ?? undefined, + ratedResidualCurrentMa: + value.ratedResidualCurrentMa ?? undefined, + }; +} diff --git a/tests/circuit-tree.controller.test.ts b/tests/circuit-tree.controller.test.ts index d649c3a..7fcf884 100644 --- a/tests/circuit-tree.controller.test.ts +++ b/tests/circuit-tree.controller.test.ts @@ -1,6 +1,19 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import { isMissingCircuitTreeSchemaError } from "../src/server/controllers/circuit-tree.controller.js"; +import { + getCircuitTree, + isMissingCircuitTreeSchemaError, +} from "../src/server/controllers/circuit-tree.controller.js"; +import { + circuitDeviceRowRepository, + circuitListRepository, + circuitProtectionDeviceRepository, + circuitRepository, + circuitSectionRepository, + distributionBoardComponentRepository, + distributionBoardRepository, + projectRepository, +} from "../src/server/composition/application-repositories.js"; describe("circuit tree controller", () => { it("detects missing circuit-first schema errors", () => { @@ -13,7 +26,192 @@ describe("circuit tree controller", () => { isMissingCircuitTreeSchemaError(new Error("SqliteError: no such table: circuit_device_rows")), true ); + assert.equal( + isMissingCircuitTreeSchemaError( + new Error("SqliteError: no such table: distribution_board_components") + ), + true + ); assert.equal(isMissingCircuitTreeSchemaError(new Error("Some other error")), false); }); + + it("returns header, grouped and footer components with both protection layers", async () => { + const originals = { + findList: circuitListRepository.findById, + findProject: projectRepository.findById, + findBoard: distributionBoardRepository.findById, + listSections: circuitSectionRepository.listByCircuitList, + listCircuits: circuitRepository.listByCircuitList, + listRows: circuitDeviceRowRepository.listByCircuitList, + listComponents: + distributionBoardComponentRepository.listByCircuitList, + listComponentProtection: + distributionBoardComponentRepository.listProtectionByComponentIds, + listCircuitProtection: + circuitProtectionDeviceRepository.listByCircuitIds, + }; + circuitListRepository.findById = async () => + ({ + id: "list-1", + projectId: "project-1", + distributionBoardId: "board-1", + name: "UV-01", + }) as never; + projectRepository.findById = async () => + ({ + id: "project-1", + name: "Projekt", + currentRevision: 4, + singlePhaseVoltageV: 230, + threePhaseVoltageV: 400, + }) as never; + distributionBoardRepository.findById = async () => + ({ id: "board-1", simultaneityFactor: 0.8 }) as never; + circuitSectionRepository.listByCircuitList = async () => + [ + { + id: "group-1", + circuitListId: "list-1", + key: "lighting", + displayName: "Beleuchtung 1", + prefix: "-1F1.", + sortOrder: 10, + category: "lighting", + groupNumber: 1, + }, + ] as never; + circuitRepository.listByCircuitList = async () => + [ + { + id: "circuit-1", + circuitListId: "list-1", + sectionId: "group-1", + equipmentIdentifier: "-1F1.1", + displayName: "Licht", + sortOrder: 10, + isReserve: 0, + }, + ] as never; + circuitDeviceRowRepository.listByCircuitList = async () => []; + distributionBoardComponentRepository.listByCircuitList = + async () => + [ + { + id: "main", + circuitListId: "list-1", + sectionId: null, + equipmentIdentifier: "-Q0", + name: "Hauptschalter", + role: "main_switch", + placement: "header", + sortOrder: 10, + }, + { + id: "rcd", + circuitListId: "list-1", + sectionId: "group-1", + equipmentIdentifier: "-1Q1.0", + name: "Gruppen-FI", + role: "group_residual_current_protection", + placement: "group", + sortOrder: 10, + }, + { + id: "actor", + circuitListId: "list-1", + sectionId: null, + equipmentIdentifier: "-K1", + name: "KNX Aktor", + role: "auxiliary", + placement: "footer", + sortOrder: 10, + }, + ] as never; + distributionBoardComponentRepository.listProtectionByComponentIds = + async () => + [ + { + componentId: "rcd", + type: "FI", + ratedCurrentA: 40, + fuseUtilizationCategory: null, + tripCharacteristic: null, + rcdType: "A", + ratedResidualCurrentMa: 30, + }, + ] as never; + circuitProtectionDeviceRepository.listByCircuitIds = async () => + [ + { + circuitId: "circuit-1", + type: "LS", + ratedCurrentA: 10, + fuseUtilizationCategory: null, + tripCharacteristic: "B", + rcdType: null, + ratedResidualCurrentMa: null, + }, + ] as never; + + let responseBody: unknown; + const response = { + status() { + return this; + }, + json(value: unknown) { + responseBody = value; + return this; + }, + }; + try { + await getCircuitTree( + { params: { projectId: "project-1", circuitListId: "list-1" } } as never, + response as never + ); + } finally { + circuitListRepository.findById = originals.findList; + projectRepository.findById = originals.findProject; + distributionBoardRepository.findById = originals.findBoard; + circuitSectionRepository.listByCircuitList = + originals.listSections; + circuitRepository.listByCircuitList = originals.listCircuits; + circuitDeviceRowRepository.listByCircuitList = originals.listRows; + distributionBoardComponentRepository.listByCircuitList = + originals.listComponents; + distributionBoardComponentRepository.listProtectionByComponentIds = + originals.listComponentProtection; + circuitProtectionDeviceRepository.listByCircuitIds = + originals.listCircuitProtection; + } + + const tree = responseBody as { + headerComponents: Array<{ equipmentIdentifier: string }>; + footerComponents: Array<{ equipmentIdentifier: string }>; + sections: Array<{ + category: string; + groupNumber: number; + components: Array<{ + protectionDevice: { ratedResidualCurrentMa: number }; + }>; + circuits: Array<{ + protectionDevice: { tripCharacteristic: string }; + }>; + }>; + }; + assert.equal(tree.headerComponents[0].equipmentIdentifier, "-Q0"); + assert.equal(tree.footerComponents[0].equipmentIdentifier, "-K1"); + assert.equal(tree.sections[0].category, "lighting"); + assert.equal(tree.sections[0].groupNumber, 1); + assert.equal( + tree.sections[0].components[0].protectionDevice + .ratedResidualCurrentMa, + 30 + ); + assert.equal( + tree.sections[0].circuits[0].protectionDevice + .tripCharacteristic, + "B" + ); + }); });