Create circuits with rows atomically

This commit is contained in:
2026-07-23 17:36:38 +02:00
parent b0fc2b4fbf
commit 0800f984bb
11 changed files with 450 additions and 93 deletions
@@ -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<CircuitDeviceRowCreateInput, "circuitId" | "sortOrder"> & { 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
+47 -41
View File
@@ -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;
}
+57 -4
View File
@@ -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) {
+80 -47
View File
@@ -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<HTMLElement>) {
+22
View File
@@ -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",
+33 -1
View File
@@ -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") {
+5
View File
@@ -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);
+6
View File
@@ -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<typeof createCircuitSchema>;
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
export type CreateCircuitWithDeviceRowsInput = z.infer<typeof createCircuitWithDeviceRowsSchema>;
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;