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
+2
View File
@@ -44,6 +44,8 @@ Response sketch:
- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits` - `POST /projects/:projectId/circuit-lists/:circuitListId/circuits`
- create circuit in list/section - 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` - `PATCH /circuits/:circuitId`
- update circuit-level fields (BMK, protection, cable, reserve flag, etc.) - update circuit-level fields (BMK, protection, cable, reserve flag, etc.)
- `DELETE /circuits/:circuitId` - `DELETE /circuits/:circuitId`
+2
View File
@@ -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 - 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 - 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 ## Add Circuit Behavior
@@ -68,6 +69,7 @@ Intent is separated by drag source type:
- project device drag: - project device drag:
- drop to section/placeholder -> create new circuit with linked row - 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 - drop to existing circuit row -> append row to that circuit
- lighting devices are accepted only by the lighting section - lighting devices are accepted only by the lighting section
- other devices are accepted only by the section matching their phase type - other devices are accepted only by the section matching their phase type
@@ -5,6 +5,10 @@ import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuitLists } from "../schema/circuit-lists.js"; import { circuitLists } from "../schema/circuit-lists.js";
import { circuits } from "../schema/circuits.js"; import { circuits } from "../schema/circuits.js";
import { distributionBoards } from "../schema/distribution-boards.js"; import { distributionBoards } from "../schema/distribution-boards.js";
import {
toCircuitCreateValues,
type CircuitCreatePersistenceInput,
} from "./circuit.repository.js";
export interface CircuitDeviceRowUpdateInput { export interface CircuitDeviceRowUpdateInput {
linkedProjectDeviceId?: string; 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) { function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
return { return {
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null, linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
@@ -199,6 +210,42 @@ export class CircuitDeviceRowRepository {
return id; 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( async update(
rowId: string, rowId: string,
input: CircuitDeviceRowUpdateInput input: CircuitDeviceRowUpdateInput
+25 -19
View File
@@ -3,21 +3,7 @@ import { and, asc, eq, inArray, ne } from "drizzle-orm";
import { db } from "../client.js"; import { db } from "../client.js";
import { circuits } from "../schema/circuits.js"; import { circuits } from "../schema/circuits.js";
export class CircuitRepository { export interface CircuitCreatePersistenceInput {
async findById(circuitId: string) {
const [row] = await db.select().from(circuits).where(eq(circuits.id, circuitId)).limit(1);
return row ?? null;
}
async listByCircuitList(circuitListId: string) {
return db
.select()
.from(circuits)
.where(eq(circuits.circuitListId, circuitListId))
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
}
async create(input: {
circuitListId: string; circuitListId: string;
sectionId: string; sectionId: string;
equipmentIdentifier: string; equipmentIdentifier: string;
@@ -36,9 +22,10 @@ export class CircuitRepository {
terminalDesignation?: string; terminalDesignation?: string;
status?: string; status?: string;
isReserve?: boolean; isReserve?: boolean;
}) { }
const id = crypto.randomUUID();
await db.insert(circuits).values({ export function toCircuitCreateValues(id: string, input: CircuitCreatePersistenceInput) {
return {
id, id,
circuitListId: input.circuitListId, circuitListId: input.circuitListId,
sectionId: input.sectionId, sectionId: input.sectionId,
@@ -58,7 +45,26 @@ export class CircuitRepository {
status: input.status ?? null, status: input.status ?? null,
isReserve: input.isReserve ? 1 : 0, isReserve: input.isReserve ? 1 : 0,
remark: input.remark ?? null, 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);
return row ?? null;
}
async listByCircuitList(circuitListId: string) {
return db
.select()
.from(circuits)
.where(eq(circuits.circuitListId, circuitListId))
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
}
async create(input: CircuitCreatePersistenceInput) {
const id = crypto.randomUUID();
await db.insert(circuits).values(toCircuitCreateValues(id, input));
return id; return id;
} }
+57 -4
View File
@@ -6,6 +6,7 @@ import { ProjectDeviceRepository } from "../../db/repositories/project-device.re
import type { import type {
CreateCircuitDeviceRowInput, CreateCircuitDeviceRowInput,
CreateCircuitInput, CreateCircuitInput,
CreateCircuitWithDeviceRowsInput,
MoveCircuitDeviceRowInput, MoveCircuitDeviceRowInput,
MoveCircuitDeviceRowsBulkInput, MoveCircuitDeviceRowsBulkInput,
ReorderSectionCircuitsInput, ReorderSectionCircuitsInput,
@@ -73,6 +74,19 @@ export class CircuitWriteService {
} }
// Validates linked project-device id against owning project of the circuit list. // 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) { private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
if (!linkedProjectDeviceId) { if (!linkedProjectDeviceId) {
return; return;
@@ -85,10 +99,7 @@ export class CircuitWriteService {
if (!list) { if (!list) {
throw new Error("Circuit list not found."); throw new Error("Circuit list not found.");
} }
const device = await this.projectDeviceRepository.findById(list.projectId, linkedProjectDeviceId); await this.assertValidLinkedProjectDeviceForProject(list.projectId, linkedProjectDeviceId);
if (!device) {
throw new Error("Invalid linked project device id.");
}
} }
async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) { async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) {
@@ -121,6 +132,48 @@ export class CircuitWriteService {
return this.circuitRepository.findById(id); 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) { async updateCircuit(circuitId: string, input: UpdateCircuitInput) {
const current = await this.circuitRepository.findById(circuitId); const current = await this.circuitRepository.findById(circuitId);
if (!current) { if (!current) {
+54 -21
View File
@@ -38,6 +38,7 @@ import {
import type { VisibleGridRow } from "../utils/circuit-grid-projection"; import type { VisibleGridRow } from "../utils/circuit-grid-projection";
import { import {
createCircuit, createCircuit,
createCircuitWithDeviceRows,
createCircuitDeviceRow, createCircuitDeviceRow,
deleteCircuitById, deleteCircuitById,
deleteCircuitDeviceRowById, deleteCircuitDeviceRowById,
@@ -1201,16 +1202,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const sortOrder = const sortOrder =
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
const isDeviceField = deviceFieldKeys.has(key); const isDeviceField = deviceFieldKeys.has(key);
const createdCircuit = (await createCircuit(projectId, circuitListId, {
if (isDeviceField) {
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
circuit: {
sectionId, sectionId,
equipmentIdentifier: next.nextIdentifier, equipmentIdentifier: next.nextIdentifier,
displayName: "Neuer Stromkreis", displayName: "Neuer Stromkreis",
sortOrder, sortOrder,
isReserve: !isDeviceField, isReserve: false,
})) as CircuitTreeCircuitDto; },
deviceRows: [
if (isDeviceField) { {
const createdRow = (await createCircuitDeviceRow(createdCircuit.id, {
name: "Manuelles Gerät", name: "Manuelles Gerät",
displayName: "Manuelles Gerät", displayName: "Manuelles Gerät",
phaseType: "single_phase", phaseType: "single_phase",
@@ -1218,11 +1221,22 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
powerPerUnit: 0, powerPerUnit: 0,
simultaneityFactor: 1, simultaneityFactor: 1,
cosPhi: 1, cosPhi: 1,
})) as { id: string }; },
],
});
const createdCircuit = created.circuit;
const createdRow = created.deviceRows[0];
await patchDeviceRow(createdRow.id, key, draft); await patchDeviceRow(createdRow.id, key, draft);
return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key }; return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key };
} }
const createdCircuit = (await createCircuit(projectId, circuitListId, {
sectionId,
equipmentIdentifier: next.nextIdentifier,
displayName: "Neuer Stromkreis",
sortOrder,
isReserve: true,
})) as CircuitTreeCircuitDto;
await patchCircuit(createdCircuit.id, key, draft); await patchCircuit(createdCircuit.id, key, draft);
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key }; return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
} }
@@ -1654,15 +1668,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const next = await getNextCircuitIdentifier(sectionId); const next = await getNextCircuitIdentifier(sectionId);
const sortOrder = const sortOrder =
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
const createdCircuit = (await createCircuit(projectId, circuitListId, { const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
circuit: {
sectionId, sectionId,
equipmentIdentifier: next.nextIdentifier, equipmentIdentifier: next.nextIdentifier,
displayName: device.displayName || device.name, displayName: device.displayName || device.name,
sortOrder, sortOrder,
isReserve: false, isReserve: false,
})) as CircuitTreeCircuitDto; },
deviceRows: [
const createdRow = (await createCircuitDeviceRow(createdCircuit.id, { {
linkedProjectDeviceId: device.id, linkedProjectDeviceId: device.id,
name: device.name, name: device.name,
displayName: device.displayName, displayName: device.displayName,
@@ -1675,7 +1690,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
cosPhi: device.cosPhi ?? undefined, cosPhi: device.cosPhi ?? undefined,
category: device.category ?? undefined, category: device.category ?? undefined,
remark: device.remark ?? undefined, remark: device.remark ?? undefined,
})) as { id: string }; },
],
});
const createdCircuit = created.circuit;
const createdRow = created.deviceRows[0];
setActiveSectionId(sectionId); setActiveSectionId(sectionId);
setTargetCircuitId(createdCircuit.id); setTargetCircuitId(createdCircuit.id);
@@ -1732,7 +1751,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
} }
async function recreateCircuit(snapshot: CircuitSnapshot) { async function recreateCircuit(snapshot: CircuitSnapshot) {
const createdCircuit = (await createCircuit(projectId, circuitListId, { const circuitInput = {
sectionId: snapshot.sectionId, sectionId: snapshot.sectionId,
equipmentIdentifier: snapshot.equipmentIdentifier, equipmentIdentifier: snapshot.equipmentIdentifier,
displayName: snapshot.displayName, displayName: snapshot.displayName,
@@ -1750,11 +1769,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
status: snapshot.status, status: snapshot.status,
isReserve: snapshot.isReserve, isReserve: snapshot.isReserve,
remark: snapshot.remark, remark: snapshot.remark,
})) as CircuitTreeCircuitDto; };
const createdRowIds: string[] = []; if (snapshot.deviceRows.length === 0) {
for (const row of snapshot.deviceRows) { const createdCircuit = (await createCircuit(
const created = (await createCircuitDeviceRow(createdCircuit.id, { 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, linkedProjectDeviceId: row.linkedProjectDeviceId,
name: row.name, name: row.name,
displayName: row.displayName, displayName: row.displayName,
@@ -1773,10 +1804,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
remark: row.remark, remark: row.remark,
overriddenFields: row.overriddenFields, overriddenFields: row.overriddenFields,
sortOrder: row.sortOrder, sortOrder: row.sortOrder,
})) as { id: string }; })),
createdRowIds.push(created.id); });
} return {
return { circuitId: createdCircuit.id, rowIds: createdRowIds }; circuitId: created.circuit.id,
rowIds: created.deviceRows.map((row) => row.id),
};
} }
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) { function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
+22
View File
@@ -18,6 +18,8 @@ import type {
RoomDto, RoomDto,
UpdateConsumerInput, UpdateConsumerInput,
CircuitTreeResponseDto, CircuitTreeResponseDto,
CircuitTreeCircuitDto,
CircuitTreeDeviceRowDto,
CreateCircuitInputDto, CreateCircuitInputDto,
UpdateCircuitInputDto, UpdateCircuitInputDto,
CreateCircuitDeviceRowInputDto, 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) { export function updateCircuitById(circuitId: string, input: UpdateCircuitInputDto) {
return request(`/api/circuits/${circuitId}`, { return request(`/api/circuits/${circuitId}`, {
method: "PATCH", method: "PATCH",
+33 -1
View File
@@ -1,6 +1,10 @@
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js"; 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(); 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) { export async function updateCircuit(req: Request, res: Response) {
const { circuitId } = req.params; const { circuitId } = req.params;
if (typeof circuitId !== "string") { if (typeof circuitId !== "string") {
+5
View File
@@ -1,6 +1,7 @@
import { Router } from "express"; import { Router } from "express";
import { import {
createCircuit, createCircuit,
createCircuitWithDeviceRows,
deleteCircuit, deleteCircuit,
getNextCircuitIdentifier, getNextCircuitIdentifier,
updateCircuit, updateCircuit,
@@ -10,6 +11,10 @@ import { createCircuitDeviceRow } from "../controllers/circuit-device-row.contro
export const circuitRouter = Router(); export const circuitRouter = Router();
circuitRouter.post("/projects/:projectId/circuit-lists/:circuitListId/circuits", createCircuit); 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.patch("/circuits/:circuitId", updateCircuit);
circuitRouter.delete("/circuits/:circuitId", deleteCircuit); circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier); circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
+6
View File
@@ -48,6 +48,11 @@ export const createCircuitDeviceRowSchema = z.object({
sortOrder: z.number().optional(), sortOrder: z.number().optional(),
}); });
export const createCircuitWithDeviceRowsSchema = z.object({
circuit: createCircuitSchema,
deviceRows: z.array(createCircuitDeviceRowSchema).min(1),
});
export const updateCircuitDeviceRowSchema = createCircuitDeviceRowSchema.partial(); export const updateCircuitDeviceRowSchema = createCircuitDeviceRowSchema.partial();
export const moveCircuitDeviceRowSchema = z export const moveCircuitDeviceRowSchema = z
@@ -95,6 +100,7 @@ export const updateSectionEquipmentIdentifiersSchema = z.object({
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>; export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>; export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>; export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
export type CreateCircuitWithDeviceRowsInput = z.infer<typeof createCircuitWithDeviceRowsSchema>;
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>; export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>; export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>; export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;
+149
View File
@@ -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 () => { it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = []; let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
const service = new CircuitWriteService({ 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<Record<string, unknown>> = [];
(db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => {
const fakeTx = {
insert() {
return {
values(values: Record<string, unknown>) {
insertedValues.push(values);
return {
run() {
return { changes: 1 };
},
};
},
};
},
};
const callbackResult = cb(fakeTx);
callbackReturnedPromise = Boolean(
callbackResult && typeof (callbackResult as Promise<unknown>).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 () => { it("tracks local edits on linked device rows as overridden fields", async () => {
let savedOverrides: string | undefined; let savedOverrides: string | undefined;
const current = { const current = {