diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 82fb9a9..191fb7a 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -6,6 +6,11 @@ The circuit-first editor uses tree + circuit + row endpoints. Legacy consumer en ## Circuit-First Endpoints +### Distribution Board Setup + +- `POST /projects/:projectId/distribution-boards` + - atomically creates the distribution board, its circuit list and all default circuit sections + ### Tree Endpoint - `GET /projects/:projectId/circuit-lists/:circuitListId/tree` diff --git a/package.json b/package.json index 6dc0dc6..82b6ea7 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "build:api": "tsc -p tsconfig.json", "build:web": "next build", "start": "node dist/server/index.js", - "test": "tsx --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts tests/project-device-schema.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-grid-projection.test.ts", - "test:watch": "tsx --watch --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts tests/project-device-schema.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-grid-projection.test.ts", + "test": "tsx --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts tests/project-device-schema.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts", + "test:watch": "tsx --watch --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts tests/project-device-schema.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:backup": "node scripts/db-backup.js", diff --git a/src/db/repositories/circuit-section.repository.ts b/src/db/repositories/circuit-section.repository.ts index 2d672dc..7c3f1f0 100644 --- a/src/db/repositories/circuit-section.repository.ts +++ b/src/db/repositories/circuit-section.repository.ts @@ -3,6 +3,19 @@ import { and, asc, eq } from "drizzle-orm"; import { db } from "../client.js"; import { circuitSections } from "../schema/circuit-sections.js"; +export function createDefaultCircuitSectionValues(circuitListId: string) { + return [ + { key: "lighting", displayName: "Lighting", prefix: "-1F", sortOrder: 10 }, + { key: "single_phase", displayName: "Single-phase circuits", prefix: "-2F", sortOrder: 20 }, + { key: "three_phase", displayName: "Three-phase circuits", prefix: "-3F", sortOrder: 30 }, + { key: "unassigned", displayName: "Unassigned", prefix: "-UF", sortOrder: 90 }, + ].map((entry) => ({ + id: crypto.randomUUID(), + circuitListId, + ...entry, + })); +} + export class CircuitSectionRepository { async findById(sectionId: string) { const [row] = await db.select().from(circuitSections).where(eq(circuitSections.id, sectionId)).limit(1); @@ -18,14 +31,7 @@ export class CircuitSectionRepository { } async createDefaults(circuitListId: string) { - const defaults = [ - { key: "lighting", displayName: "Lighting", prefix: "-1F", sortOrder: 10 }, - { key: "single_phase", displayName: "Single-phase circuits", prefix: "-2F", sortOrder: 20 }, - { key: "three_phase", displayName: "Three-phase circuits", prefix: "-3F", sortOrder: 30 }, - { key: "unassigned", displayName: "Unassigned", prefix: "-UF", sortOrder: 90 }, - ]; - - for (const entry of defaults) { + for (const entry of createDefaultCircuitSectionValues(circuitListId)) { const existing = await db .select({ id: circuitSections.id }) .from(circuitSections) @@ -36,11 +42,7 @@ export class CircuitSectionRepository { if (existing.length) { continue; } - await db.insert(circuitSections).values({ - id: crypto.randomUUID(), - circuitListId, - ...entry, - }); + await db.insert(circuitSections).values(entry); } } } diff --git a/src/db/repositories/distribution-board.repository.ts b/src/db/repositories/distribution-board.repository.ts index 9db2cdb..9a04a44 100644 --- a/src/db/repositories/distribution-board.repository.ts +++ b/src/db/repositories/distribution-board.repository.ts @@ -1,7 +1,10 @@ import crypto from "node:crypto"; import { and, eq } from "drizzle-orm"; import { db } from "../client.js"; +import { circuitLists } from "../schema/circuit-lists.js"; +import { circuitSections } from "../schema/circuit-sections.js"; import { distributionBoards } from "../schema/distribution-boards.js"; +import { createDefaultCircuitSectionValues } from "./circuit-section.repository.js"; export class DistributionBoardRepository { async listByProject(projectId: string) { @@ -15,6 +18,26 @@ export class DistributionBoardRepository { return board; } + createWithCircuitListAndDefaultSections(projectId: string, name: string) { + const id = crypto.randomUUID(); + const board = { id, projectId, name }; + const circuitList = { + id, + projectId, + distributionBoardId: id, + name: `${name} Stromkreisliste`, + }; + const sections = createDefaultCircuitSectionValues(id); + + db.transaction((tx) => { + tx.insert(distributionBoards).values(board).run(); + tx.insert(circuitLists).values(circuitList).run(); + tx.insert(circuitSections).values(sections).run(); + }); + + return board; + } + async existsInProject(projectId: string, distributionBoardId: string) { const [row] = await db .select({ id: distributionBoards.id }) diff --git a/src/server/controllers/distribution-board.controller.ts b/src/server/controllers/distribution-board.controller.ts index cc340db..2d39919 100644 --- a/src/server/controllers/distribution-board.controller.ts +++ b/src/server/controllers/distribution-board.controller.ts @@ -1,11 +1,7 @@ import type { Request, Response } from "express"; -import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js"; -import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js"; import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js"; import { createDistributionBoardSchema } from "../../shared/validation/consumer.schemas.js"; -const circuitListRepository = new CircuitListRepository(); -const circuitSectionRepository = new CircuitSectionRepository(); const distributionBoardRepository = new DistributionBoardRepository(); export async function listDistributionBoardsByProject(req: Request, res: Response) { @@ -29,12 +25,9 @@ export async function createDistributionBoard(req: Request, res: Response) { return res.status(400).json({ error: parsed.error.flatten() }); } - const board = await distributionBoardRepository.create(projectId, parsed.data.name); - const list = await circuitListRepository.createForDistributionBoard({ + const board = distributionBoardRepository.createWithCircuitListAndDefaultSections( projectId, - distributionBoardId: board.id, - name: `${board.name} Stromkreisliste`, - }); - await circuitSectionRepository.createDefaults(list.id); + parsed.data.name + ); return res.status(201).json(board); } diff --git a/tests/distribution-board.repository.test.ts b/tests/distribution-board.repository.test.ts new file mode 100644 index 0000000..0ab4f23 --- /dev/null +++ b/tests/distribution-board.repository.test.ts @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { db } from "../src/db/client.js"; +import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js"; +import { circuitLists } from "../src/db/schema/circuit-lists.js"; +import { circuitSections } from "../src/db/schema/circuit-sections.js"; +import { distributionBoards } from "../src/db/schema/distribution-boards.js"; + +describe("distribution board repository", () => { + it("creates board, circuit list and default sections in one synchronous transaction", () => { + const originalTransaction = db.transaction; + const insertedValues: Array<{ table: unknown; values: unknown }> = []; + let transactionCalls = 0; + + (db as unknown as { transaction: (callback: (tx: unknown) => unknown) => unknown }).transaction = + (callback) => { + transactionCalls += 1; + return callback({ + insert(table: unknown) { + return { + values(values: unknown) { + insertedValues.push({ table, values }); + return { + run() { + return { changes: Array.isArray(values) ? values.length : 1 }; + }, + }; + }, + }; + }, + }); + }; + + try { + const repository = new DistributionBoardRepository(); + const board = repository.createWithCircuitListAndDefaultSections("p1", "UV-01"); + + assert.equal(transactionCalls, 1); + assert.equal(insertedValues.length, 3); + assert.equal(insertedValues[0].table, distributionBoards); + assert.deepEqual(insertedValues[0].values, board); + assert.equal(insertedValues[1].table, circuitLists); + assert.deepEqual(insertedValues[1].values, { + id: board.id, + projectId: "p1", + distributionBoardId: board.id, + name: "UV-01 Stromkreisliste", + }); + assert.equal(insertedValues[2].table, circuitSections); + + const sections = insertedValues[2].values as Array<{ + circuitListId: string; + key: string; + prefix: string; + }>; + assert.equal(sections.length, 4); + assert.ok(sections.every((section) => section.circuitListId === board.id)); + assert.deepEqual( + sections.map((section) => [section.key, section.prefix]), + [ + ["lighting", "-1F"], + ["single_phase", "-2F"], + ["three_phase", "-3F"], + ["unassigned", "-UF"], + ] + ); + } finally { + (db as unknown as { transaction: unknown }).transaction = originalTransaction; + } + }); +});