diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index ee2580c..82fb9a9 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -44,6 +44,8 @@ Response sketch: - `POST /projects/:projectId/circuit-lists/:circuitListId/circuits` - create circuit in list/section +- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows` + - atomically create one circuit and one or more initial device rows - `PATCH /circuits/:circuitId` - update circuit-level fields (BMK, protection, cable, reserve flag, etc.) - `DELETE /circuits/:circuitId` diff --git a/docs/circuit-list-editor-interactions.md b/docs/circuit-list-editor-interactions.md index 1acdfd8..cc8001e 100644 --- a/docs/circuit-list-editor-interactions.md +++ b/docs/circuit-list-editor-interactions.md @@ -56,6 +56,7 @@ Each section has a trailing placeholder row showing `-frei-` in BMK column: - serves as drop target for creating new circuits from project devices or moved rows - editable placeholder cells can create a new circuit + first row through the same edit command flow +- a new circuit and its initial row(s) are created in one SQLite transaction ## Add Circuit Behavior @@ -68,6 +69,7 @@ Intent is separated by drag source type: - project device drag: - drop to section/placeholder -> create new circuit with linked row + - the new circuit and linked row are committed atomically - drop to existing circuit row -> append row to that circuit - lighting devices are accepted only by the lighting section - other devices are accepted only by the section matching their phase type diff --git a/src/db/repositories/circuit-device-row.repository.ts b/src/db/repositories/circuit-device-row.repository.ts index ba58f23..034fe5a 100644 --- a/src/db/repositories/circuit-device-row.repository.ts +++ b/src/db/repositories/circuit-device-row.repository.ts @@ -5,6 +5,10 @@ import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuits } from "../schema/circuits.js"; import { distributionBoards } from "../schema/distribution-boards.js"; +import { + toCircuitCreateValues, + type CircuitCreatePersistenceInput, +} from "./circuit.repository.js"; export interface CircuitDeviceRowUpdateInput { linkedProjectDeviceId?: string; @@ -45,6 +49,13 @@ export interface CircuitDeviceRowsBulkMoveInput { }; } +export interface CircuitWithDeviceRowsCreateInput { + circuit: CircuitCreatePersistenceInput; + deviceRows: Array< + Omit & { sortOrder?: number } + >; +} + function toUpdateValues(input: CircuitDeviceRowUpdateInput) { return { linkedProjectDeviceId: input.linkedProjectDeviceId ?? null, @@ -199,6 +210,42 @@ export class CircuitDeviceRowRepository { return id; } + createCircuitWithDeviceRowsTransactional(input: CircuitWithDeviceRowsCreateInput) { + if (input.deviceRows.length === 0) { + throw new Error("Mindestens eine Gerätezeile ist erforderlich."); + } + + const circuitId = crypto.randomUUID(); + const rowIds = input.deviceRows.map(() => crypto.randomUUID()); + db.transaction((tx) => { + tx + .insert(circuits) + .values( + toCircuitCreateValues(circuitId, { + ...input.circuit, + isReserve: false, + }) + ) + .run(); + + for (let index = 0; index < input.deviceRows.length; index += 1) { + const row = input.deviceRows[index]; + tx + .insert(circuitDeviceRows) + .values( + toCreateValues(rowIds[index], { + ...row, + circuitId, + sortOrder: row.sortOrder ?? (index + 1) * 10, + }) + ) + .run(); + } + }); + + return { circuitId, rowIds }; + } + async update( rowId: string, input: CircuitDeviceRowUpdateInput diff --git a/src/db/repositories/circuit.repository.ts b/src/db/repositories/circuit.repository.ts index c3026dd..9382c39 100644 --- a/src/db/repositories/circuit.repository.ts +++ b/src/db/repositories/circuit.repository.ts @@ -3,6 +3,51 @@ import { and, asc, eq, inArray, ne } from "drizzle-orm"; import { db } from "../client.js"; import { circuits } from "../schema/circuits.js"; +export interface CircuitCreatePersistenceInput { + circuitListId: string; + sectionId: string; + equipmentIdentifier: string; + displayName?: string; + sortOrder: number; + protectionType?: string; + protectionRatedCurrent?: number; + protectionCharacteristic?: string; + cableType?: string; + cableCrossSection?: string; + cableLength?: number; + voltage?: number; + controlRequirement?: string; + remark?: string; + rcdAssignment?: string; + terminalDesignation?: string; + status?: string; + isReserve?: boolean; +} + +export function toCircuitCreateValues(id: string, input: CircuitCreatePersistenceInput) { + return { + id, + circuitListId: input.circuitListId, + sectionId: input.sectionId, + equipmentIdentifier: input.equipmentIdentifier, + displayName: input.displayName ?? null, + sortOrder: input.sortOrder, + protectionType: input.protectionType ?? null, + protectionRatedCurrent: input.protectionRatedCurrent ?? null, + protectionCharacteristic: input.protectionCharacteristic ?? null, + cableType: input.cableType ?? null, + cableCrossSection: input.cableCrossSection ?? null, + cableLength: input.cableLength ?? null, + rcdAssignment: input.rcdAssignment ?? null, + terminalDesignation: input.terminalDesignation ?? null, + voltage: input.voltage ?? null, + controlRequirement: input.controlRequirement ?? null, + status: input.status ?? null, + isReserve: input.isReserve ? 1 : 0, + remark: input.remark ?? null, + }; +} + export class CircuitRepository { async findById(circuitId: string) { const [row] = await db.select().from(circuits).where(eq(circuits.id, circuitId)).limit(1); @@ -17,48 +62,9 @@ export class CircuitRepository { .orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier)); } - async create(input: { - circuitListId: string; - sectionId: string; - equipmentIdentifier: string; - displayName?: string; - sortOrder: number; - protectionType?: string; - protectionRatedCurrent?: number; - protectionCharacteristic?: string; - cableType?: string; - cableCrossSection?: string; - cableLength?: number; - voltage?: number; - controlRequirement?: string; - remark?: string; - rcdAssignment?: string; - terminalDesignation?: string; - status?: string; - isReserve?: boolean; - }) { + async create(input: CircuitCreatePersistenceInput) { const id = crypto.randomUUID(); - await db.insert(circuits).values({ - id, - circuitListId: input.circuitListId, - sectionId: input.sectionId, - equipmentIdentifier: input.equipmentIdentifier, - displayName: input.displayName ?? null, - sortOrder: input.sortOrder, - protectionType: input.protectionType ?? null, - protectionRatedCurrent: input.protectionRatedCurrent ?? null, - protectionCharacteristic: input.protectionCharacteristic ?? null, - cableType: input.cableType ?? null, - cableCrossSection: input.cableCrossSection ?? null, - cableLength: input.cableLength ?? null, - rcdAssignment: input.rcdAssignment ?? null, - terminalDesignation: input.terminalDesignation ?? null, - voltage: input.voltage ?? null, - controlRequirement: input.controlRequirement ?? null, - status: input.status ?? null, - isReserve: input.isReserve ? 1 : 0, - remark: input.remark ?? null, - }); + await db.insert(circuits).values(toCircuitCreateValues(id, input)); return id; } diff --git a/src/domain/services/circuit-write.service.ts b/src/domain/services/circuit-write.service.ts index 4da5cd6..789e815 100644 --- a/src/domain/services/circuit-write.service.ts +++ b/src/domain/services/circuit-write.service.ts @@ -6,6 +6,7 @@ import { ProjectDeviceRepository } from "../../db/repositories/project-device.re import type { CreateCircuitDeviceRowInput, CreateCircuitInput, + CreateCircuitWithDeviceRowsInput, MoveCircuitDeviceRowInput, MoveCircuitDeviceRowsBulkInput, ReorderSectionCircuitsInput, @@ -73,6 +74,19 @@ export class CircuitWriteService { } // Validates linked project-device id against owning project of the circuit list. + private async assertValidLinkedProjectDeviceForProject( + projectId: string, + linkedProjectDeviceId?: string + ) { + if (!linkedProjectDeviceId) { + return; + } + const device = await this.projectDeviceRepository.findById(projectId, linkedProjectDeviceId); + if (!device) { + throw new Error("Invalid linked project device id."); + } + } + private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) { if (!linkedProjectDeviceId) { return; @@ -85,10 +99,7 @@ export class CircuitWriteService { if (!list) { throw new Error("Circuit list not found."); } - const device = await this.projectDeviceRepository.findById(list.projectId, linkedProjectDeviceId); - if (!device) { - throw new Error("Invalid linked project device id."); - } + await this.assertValidLinkedProjectDeviceForProject(list.projectId, linkedProjectDeviceId); } async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) { @@ -121,6 +132,48 @@ export class CircuitWriteService { return this.circuitRepository.findById(id); } + async createCircuitWithDeviceRows( + projectId: string, + circuitListId: string, + input: CreateCircuitWithDeviceRowsInput + ) { + const list = await this.circuitListRepository.findById(projectId, circuitListId); + if (!list) { + throw new Error("Circuit list not found in project."); + } + await this.assertSectionInList(input.circuit.sectionId, circuitListId); + await this.assertUniqueEquipmentIdentifier( + circuitListId, + input.circuit.equipmentIdentifier + ); + for (const row of input.deviceRows) { + await this.assertValidLinkedProjectDeviceForProject( + list.projectId, + row.linkedProjectDeviceId + ); + } + + const created = this.deviceRowRepository.createCircuitWithDeviceRowsTransactional({ + circuit: { + circuitListId, + ...input.circuit, + isReserve: false, + }, + deviceRows: input.deviceRows, + }); + const circuit = await this.circuitRepository.findById(created.circuitId); + const deviceRows = await Promise.all( + created.rowIds.map((rowId) => this.deviceRowRepository.findById(rowId)) + ); + if (!circuit || deviceRows.some((row) => !row)) { + throw new Error("Der angelegte Stromkreis konnte nicht geladen werden."); + } + return { + circuit, + deviceRows, + }; + } + async updateCircuit(circuitId: string, input: UpdateCircuitInput) { const current = await this.circuitRepository.findById(circuitId); if (!current) { diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index efe3af7..cb05281 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -38,6 +38,7 @@ import { import type { VisibleGridRow } from "../utils/circuit-grid-projection"; import { createCircuit, + createCircuitWithDeviceRows, createCircuitDeviceRow, deleteCircuitById, deleteCircuitDeviceRowById, @@ -1201,28 +1202,41 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const sortOrder = section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; const isDeviceField = deviceFieldKeys.has(key); + + if (isDeviceField) { + const created = await createCircuitWithDeviceRows(projectId, circuitListId, { + circuit: { + sectionId, + equipmentIdentifier: next.nextIdentifier, + displayName: "Neuer Stromkreis", + sortOrder, + isReserve: false, + }, + deviceRows: [ + { + name: "Manuelles Gerät", + displayName: "Manuelles Gerät", + phaseType: "single_phase", + quantity: 1, + powerPerUnit: 0, + simultaneityFactor: 1, + cosPhi: 1, + }, + ], + }); + const createdCircuit = created.circuit; + const createdRow = created.deviceRows[0]; + await patchDeviceRow(createdRow.id, key, draft); + return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key }; + } + const createdCircuit = (await createCircuit(projectId, circuitListId, { sectionId, equipmentIdentifier: next.nextIdentifier, displayName: "Neuer Stromkreis", sortOrder, - isReserve: !isDeviceField, + isReserve: true, })) as CircuitTreeCircuitDto; - - if (isDeviceField) { - const createdRow = (await createCircuitDeviceRow(createdCircuit.id, { - name: "Manuelles Gerät", - displayName: "Manuelles Gerät", - phaseType: "single_phase", - quantity: 1, - powerPerUnit: 0, - simultaneityFactor: 1, - cosPhi: 1, - })) as { id: string }; - await patchDeviceRow(createdRow.id, key, draft); - return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key }; - } - await patchCircuit(createdCircuit.id, key, draft); return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key }; } @@ -1654,28 +1668,33 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const next = await getNextCircuitIdentifier(sectionId); const sortOrder = section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; - const createdCircuit = (await createCircuit(projectId, circuitListId, { - sectionId, - equipmentIdentifier: next.nextIdentifier, - displayName: device.displayName || device.name, - sortOrder, - isReserve: false, - })) as CircuitTreeCircuitDto; - - const createdRow = (await createCircuitDeviceRow(createdCircuit.id, { - linkedProjectDeviceId: device.id, - name: device.name, - displayName: device.displayName, - phaseType: resolvePhaseType(device), - connectionKind: device.connectionKind ?? undefined, - costGroup: device.costGroup ?? undefined, - quantity: device.quantity, - powerPerUnit: device.powerPerUnit, - simultaneityFactor: device.simultaneityFactor, - cosPhi: device.cosPhi ?? undefined, - category: device.category ?? undefined, - remark: device.remark ?? undefined, - })) as { id: string }; + const created = await createCircuitWithDeviceRows(projectId, circuitListId, { + circuit: { + sectionId, + equipmentIdentifier: next.nextIdentifier, + displayName: device.displayName || device.name, + sortOrder, + isReserve: false, + }, + deviceRows: [ + { + linkedProjectDeviceId: device.id, + name: device.name, + displayName: device.displayName, + phaseType: resolvePhaseType(device), + connectionKind: device.connectionKind ?? undefined, + costGroup: device.costGroup ?? undefined, + quantity: device.quantity, + powerPerUnit: device.powerPerUnit, + simultaneityFactor: device.simultaneityFactor, + cosPhi: device.cosPhi ?? undefined, + category: device.category ?? undefined, + remark: device.remark ?? undefined, + }, + ], + }); + const createdCircuit = created.circuit; + const createdRow = created.deviceRows[0]; setActiveSectionId(sectionId); setTargetCircuitId(createdCircuit.id); @@ -1732,7 +1751,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } async function recreateCircuit(snapshot: CircuitSnapshot) { - const createdCircuit = (await createCircuit(projectId, circuitListId, { + const circuitInput = { sectionId: snapshot.sectionId, equipmentIdentifier: snapshot.equipmentIdentifier, displayName: snapshot.displayName, @@ -1750,11 +1769,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str status: snapshot.status, isReserve: snapshot.isReserve, remark: snapshot.remark, - })) as CircuitTreeCircuitDto; + }; - const createdRowIds: string[] = []; - for (const row of snapshot.deviceRows) { - const created = (await createCircuitDeviceRow(createdCircuit.id, { + if (snapshot.deviceRows.length === 0) { + const createdCircuit = (await createCircuit( + projectId, + circuitListId, + circuitInput + )) as CircuitTreeCircuitDto; + return { circuitId: createdCircuit.id, rowIds: [] }; + } + + const created = await createCircuitWithDeviceRows(projectId, circuitListId, { + circuit: { + ...circuitInput, + isReserve: false, + }, + deviceRows: snapshot.deviceRows.map((row) => ({ linkedProjectDeviceId: row.linkedProjectDeviceId, name: row.name, displayName: row.displayName, @@ -1773,10 +1804,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str remark: row.remark, overriddenFields: row.overriddenFields, sortOrder: row.sortOrder, - })) as { id: string }; - createdRowIds.push(created.id); - } - return { circuitId: createdCircuit.id, rowIds: createdRowIds }; + })), + }); + return { + circuitId: created.circuit.id, + rowIds: created.deviceRows.map((row) => row.id), + }; } function parseDraggedProjectDeviceId(event: DragEvent) { diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index 7736373..fca4a8d 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -18,6 +18,8 @@ import type { RoomDto, UpdateConsumerInput, CircuitTreeResponseDto, + CircuitTreeCircuitDto, + CircuitTreeDeviceRowDto, CreateCircuitInputDto, UpdateCircuitInputDto, CreateCircuitDeviceRowInputDto, @@ -98,6 +100,26 @@ export function createCircuit(projectId: string, circuitListId: string, input: C }); } +export function createCircuitWithDeviceRows( + projectId: string, + circuitListId: string, + input: { + circuit: CreateCircuitInputDto; + deviceRows: CreateCircuitDeviceRowInputDto[]; + } +) { + return request<{ + circuit: CircuitTreeCircuitDto; + deviceRows: CircuitTreeDeviceRowDto[]; + }>( + `/api/projects/${projectId}/circuit-lists/${circuitListId}/circuits-with-device-rows`, + { + method: "POST", + body: JSON.stringify(input), + } + ); +} + export function updateCircuitById(circuitId: string, input: UpdateCircuitInputDto) { return request(`/api/circuits/${circuitId}`, { method: "PATCH", diff --git a/src/server/controllers/circuit.controller.ts b/src/server/controllers/circuit.controller.ts index 415059f..8ab692d 100644 --- a/src/server/controllers/circuit.controller.ts +++ b/src/server/controllers/circuit.controller.ts @@ -1,6 +1,10 @@ import type { Request, Response } from "express"; import { CircuitWriteService } from "../../domain/services/circuit-write.service.js"; -import { createCircuitSchema, updateCircuitSchema } from "../../shared/validation/circuit.schemas.js"; +import { + createCircuitSchema, + createCircuitWithDeviceRowsSchema, + updateCircuitSchema, +} from "../../shared/validation/circuit.schemas.js"; const circuitWriteService = new CircuitWriteService(); @@ -23,6 +27,34 @@ export async function createCircuit(req: Request, res: Response) { } } +export async function createCircuitWithDeviceRows(req: Request, res: Response) { + const { projectId, circuitListId } = req.params; + if (typeof projectId !== "string" || typeof circuitListId !== "string") { + return res.status(400).json({ error: "Ungültige Parameter." }); + } + + const parsed = createCircuitWithDeviceRowsSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + + try { + const created = await circuitWriteService.createCircuitWithDeviceRows( + projectId, + circuitListId, + parsed.data + ); + return res.status(201).json(created); + } catch (error) { + return res.status(400).json({ + error: + error instanceof Error + ? error.message + : "Stromkreis und Gerätezeilen konnten nicht erstellt werden.", + }); + } +} + export async function updateCircuit(req: Request, res: Response) { const { circuitId } = req.params; if (typeof circuitId !== "string") { diff --git a/src/server/routes/circuit.routes.ts b/src/server/routes/circuit.routes.ts index 12702c4..bff9a8c 100644 --- a/src/server/routes/circuit.routes.ts +++ b/src/server/routes/circuit.routes.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import { createCircuit, + createCircuitWithDeviceRows, deleteCircuit, getNextCircuitIdentifier, updateCircuit, @@ -10,6 +11,10 @@ import { createCircuitDeviceRow } from "../controllers/circuit-device-row.contro export const circuitRouter = Router(); circuitRouter.post("/projects/:projectId/circuit-lists/:circuitListId/circuits", createCircuit); +circuitRouter.post( + "/projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows", + createCircuitWithDeviceRows +); circuitRouter.patch("/circuits/:circuitId", updateCircuit); circuitRouter.delete("/circuits/:circuitId", deleteCircuit); circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier); diff --git a/src/shared/validation/circuit.schemas.ts b/src/shared/validation/circuit.schemas.ts index b248a37..a0976f8 100644 --- a/src/shared/validation/circuit.schemas.ts +++ b/src/shared/validation/circuit.schemas.ts @@ -48,6 +48,11 @@ export const createCircuitDeviceRowSchema = z.object({ sortOrder: z.number().optional(), }); +export const createCircuitWithDeviceRowsSchema = z.object({ + circuit: createCircuitSchema, + deviceRows: z.array(createCircuitDeviceRowSchema).min(1), +}); + export const updateCircuitDeviceRowSchema = createCircuitDeviceRowSchema.partial(); export const moveCircuitDeviceRowSchema = z @@ -95,6 +100,7 @@ export const updateSectionEquipmentIdentifiersSchema = z.object({ export type CreateCircuitInput = z.infer; export type UpdateCircuitInput = z.infer; export type CreateCircuitDeviceRowInput = z.infer; +export type CreateCircuitWithDeviceRowsInput = z.infer; export type UpdateCircuitDeviceRowInput = z.infer; export type MoveCircuitDeviceRowInput = z.infer; export type MoveCircuitDeviceRowsBulkInput = z.infer; diff --git a/tests/circuit-write.rules.test.ts b/tests/circuit-write.rules.test.ts index 45d5dc5..5ee18b6 100644 --- a/tests/circuit-write.rules.test.ts +++ b/tests/circuit-write.rules.test.ts @@ -215,6 +215,85 @@ describe("circuit write service rules", () => { }); }); + it("creates a circuit and all first device rows through one repository command", async () => { + let transactionalCreate: { + circuit: { circuitListId: string; sectionId: string; equipmentIdentifier: string }; + deviceRows: Array<{ linkedProjectDeviceId?: string; name: string }>; + } | undefined; + const service = new CircuitWriteService({ + circuitListRepository: { + async findById() { + return { id: "l1", projectId: "p1" } as never; + }, + } as never, + circuitSectionRepository: { + async findById() { + return { id: "s1", circuitListId: "l1" } as never; + }, + } as never, + circuitRepository: { + async existsByEquipmentIdentifier() { + return false; + }, + async findById() { + return { id: "c-new", circuitListId: "l1", sectionId: "s1" } as never; + }, + } as never, + projectDeviceRepository: { + async findById(projectId: string, deviceId: string) { + return projectId === "p1" && deviceId === "pd1" ? ({ id: "pd1" } as never) : null; + }, + } as never, + deviceRowRepository: { + createCircuitWithDeviceRowsTransactional(input: typeof transactionalCreate) { + transactionalCreate = input; + return { circuitId: "c-new", rowIds: ["r1", "r2"] }; + }, + async findById(rowId: string) { + return { id: rowId, circuitId: "c-new" } as never; + }, + } as never, + }); + + const created = await service.createCircuitWithDeviceRows("p1", "l1", { + circuit: { + sectionId: "s1", + equipmentIdentifier: "-2F1", + displayName: "Steckdosen", + sortOrder: 10, + }, + deviceRows: [ + { + linkedProjectDeviceId: "pd1", + name: "Socket", + displayName: "Steckdose", + quantity: 1, + powerPerUnit: 0.2, + simultaneityFactor: 1, + }, + { + name: "Manual", + displayName: "Manuell", + quantity: 2, + powerPerUnit: 0.1, + simultaneityFactor: 0.8, + }, + ], + }); + + assert.equal(transactionalCreate?.circuit.circuitListId, "l1"); + assert.equal(transactionalCreate?.circuit.sectionId, "s1"); + assert.equal(transactionalCreate?.circuit.equipmentIdentifier, "-2F1"); + assert.deepEqual( + transactionalCreate?.deviceRows.map((row) => row.name), + ["Socket", "Manual"] + ); + assert.deepEqual( + created.deviceRows.map((row) => row?.id), + ["r1", "r2"] + ); + }); + it("renumber uses safe bulk identifier update for swapped identifiers", async () => { let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = []; const service = new CircuitWriteService({ @@ -997,6 +1076,76 @@ describe("circuit write service rules", () => { } }); + it("new circuit and initial device rows share one synchronous transaction", () => { + const repository = new CircuitDeviceRowRepository(); + const originalTransaction = (db as unknown as { transaction: unknown }).transaction; + + let callbackReturnedPromise = false; + const insertedValues: Array> = []; + (db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => { + const fakeTx = { + insert() { + return { + values(values: Record) { + insertedValues.push(values); + return { + run() { + return { changes: 1 }; + }, + }; + }, + }; + }, + }; + const callbackResult = cb(fakeTx); + callbackReturnedPromise = Boolean( + callbackResult && typeof (callbackResult as Promise).then === "function" + ); + }; + + try { + const created = repository.createCircuitWithDeviceRowsTransactional({ + circuit: { + circuitListId: "l1", + sectionId: "s1", + equipmentIdentifier: "-2F1", + displayName: "Steckdosen", + sortOrder: 10, + }, + deviceRows: [ + { + name: "Socket", + displayName: "Steckdose", + quantity: 1, + powerPerUnit: 0.2, + simultaneityFactor: 1, + sortOrder: 15, + }, + { + name: "Manual", + displayName: "Manuell", + quantity: 2, + powerPerUnit: 0.1, + simultaneityFactor: 0.8, + }, + ], + }); + + assert.equal(callbackReturnedPromise, false); + assert.equal(insertedValues.length, 3); + assert.equal(insertedValues[0].id, created.circuitId); + assert.equal(insertedValues[0].isReserve, 0); + assert.equal(insertedValues[1].id, created.rowIds[0]); + assert.equal(insertedValues[1].circuitId, created.circuitId); + assert.equal(insertedValues[1].sortOrder, 15); + assert.equal(insertedValues[2].id, created.rowIds[1]); + assert.equal(insertedValues[2].circuitId, created.circuitId); + assert.equal(insertedValues[2].sortOrder, 20); + } finally { + (db as unknown as { transaction: unknown }).transaction = originalTransaction; + } + }); + it("tracks local edits on linked device rows as overridden fields", async () => { let savedOverrides: string | undefined; const current = {