Isolate device row transactions

This commit is contained in:
2026-07-23 18:33:22 +02:00
parent d7fda37576
commit 30c7555c68
15 changed files with 458 additions and 352 deletions
@@ -0,0 +1,93 @@
import crypto from "node:crypto";
import { and, eq } from "drizzle-orm";
import type {
CircuitDeviceRowTransactionStore,
CreateCircuitDeviceRowTransactionInput,
} from "../../domain/ports/circuit-device-row-transaction.store.js";
import type { AppDatabase } from "../database-context.js";
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuits } from "../schema/circuits.js";
import {
toCircuitDeviceRowCreateValues,
} from "./circuit-device-row.persistence.js";
export class CircuitDeviceRowTransactionRepository
implements CircuitDeviceRowTransactionStore
{
constructor(private readonly database: AppDatabase) {}
createInCircuit(input: CreateCircuitDeviceRowTransactionInput) {
const id = crypto.randomUUID();
this.database.transaction((tx) => {
const [circuit] = tx
.select({ id: circuits.id })
.from(circuits)
.where(eq(circuits.id, input.circuitId))
.limit(1)
.all();
if (!circuit) {
throw new Error("Der Stromkreis ist ungültig.");
}
const existingRows = tx
.select({ sortOrder: circuitDeviceRows.sortOrder })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, input.circuitId))
.all();
const lastSortOrder = existingRows.reduce(
(highest, row) => Math.max(highest, row.sortOrder),
0
);
const sortOrder = input.sortOrder ?? lastSortOrder + 10;
tx
.insert(circuitDeviceRows)
.values(toCircuitDeviceRowCreateValues(id, { ...input, sortOrder }))
.run();
tx
.update(circuits)
.set({ isReserve: 0 })
.where(eq(circuits.id, input.circuitId))
.run();
});
return id;
}
deleteFromCircuit(rowId: string, expectedCircuitId: string) {
this.database.transaction((tx) => {
const [row] = tx
.select({ id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, rowId))
.limit(1)
.all();
if (!row || row.circuitId !== expectedCircuitId) {
throw new Error("Die Gerätezeile wurde vor dem Löschen verändert.");
}
const result = tx
.delete(circuitDeviceRows)
.where(
and(
eq(circuitDeviceRows.id, rowId),
eq(circuitDeviceRows.circuitId, expectedCircuitId)
)
)
.run();
if (result.changes !== 1) {
throw new Error("Die Gerätezeile konnte nicht gelöscht werden.");
}
const remainingRows = tx
.select({ id: circuitDeviceRows.id })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, expectedCircuitId))
.all();
tx
.update(circuits)
.set({ isReserve: remainingRows.length === 0 ? 1 : 0 })
.where(eq(circuits.id, expectedCircuitId))
.run();
});
}
}
@@ -0,0 +1,100 @@
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
export interface CircuitDeviceRowUpdateInput {
linkedProjectDeviceId?: string;
name: string;
displayName: string;
phaseType?: string;
connectionKind?: string;
costGroup?: string;
category?: string;
level?: string;
roomId?: string;
roomNumberSnapshot?: string;
roomNameSnapshot?: string;
quantity: number;
powerPerUnit: number;
simultaneityFactor: number;
cosPhi?: number;
remark?: string;
overriddenFields?: string;
}
export type CircuitDeviceRowPatchInput = Partial<CircuitDeviceRowUpdateInput> & {
sortOrder?: number;
};
export interface CircuitDeviceRowCreateInput extends CircuitDeviceRowUpdateInput {
circuitId: string;
linkedProjectDeviceId?: string;
legacyConsumerId?: string;
sortOrder: number;
}
export function toCircuitDeviceRowUpdateValues(input: CircuitDeviceRowUpdateInput) {
return {
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
name: input.name,
displayName: input.displayName,
phaseType: input.phaseType ?? null,
connectionKind: input.connectionKind ?? null,
costGroup: input.costGroup ?? null,
category: input.category ?? null,
level: input.level ?? null,
roomId: input.roomId ?? null,
roomNumberSnapshot: input.roomNumberSnapshot ?? null,
roomNameSnapshot: input.roomNameSnapshot ?? null,
quantity: input.quantity,
powerPerUnit: input.powerPerUnit,
simultaneityFactor: input.simultaneityFactor,
cosPhi: input.cosPhi ?? null,
remark: input.remark ?? null,
overriddenFields: input.overriddenFields ?? null,
};
}
export function toCircuitDeviceRowPatchValues(input: CircuitDeviceRowPatchInput) {
const values: Partial<typeof circuitDeviceRows.$inferInsert> = {};
const has = (field: keyof CircuitDeviceRowPatchInput) =>
Object.prototype.hasOwnProperty.call(input, field);
if (has("linkedProjectDeviceId")) {
values.linkedProjectDeviceId = input.linkedProjectDeviceId ?? null;
}
if (input.name !== undefined) values.name = input.name;
if (input.displayName !== undefined) values.displayName = input.displayName;
if (has("phaseType")) values.phaseType = input.phaseType ?? null;
if (has("connectionKind")) values.connectionKind = input.connectionKind ?? null;
if (has("costGroup")) values.costGroup = input.costGroup ?? null;
if (has("category")) values.category = input.category ?? null;
if (has("level")) values.level = input.level ?? null;
if (has("roomId")) values.roomId = input.roomId ?? null;
if (has("roomNumberSnapshot")) {
values.roomNumberSnapshot = input.roomNumberSnapshot ?? null;
}
if (has("roomNameSnapshot")) values.roomNameSnapshot = input.roomNameSnapshot ?? null;
if (input.quantity !== undefined) values.quantity = input.quantity;
if (input.powerPerUnit !== undefined) values.powerPerUnit = input.powerPerUnit;
if (input.simultaneityFactor !== undefined) {
values.simultaneityFactor = input.simultaneityFactor;
}
if (has("cosPhi")) values.cosPhi = input.cosPhi ?? null;
if (has("remark")) values.remark = input.remark ?? null;
if (has("overriddenFields")) values.overriddenFields = input.overriddenFields ?? null;
if (input.sortOrder !== undefined) values.sortOrder = input.sortOrder;
return values;
}
export function toCircuitDeviceRowCreateValues(
id: string,
input: CircuitDeviceRowCreateInput
) {
return {
id,
circuitId: input.circuitId,
legacyConsumerId: input.legacyConsumerId ?? null,
sortOrder: input.sortOrder,
...toCircuitDeviceRowUpdateValues(input),
};
}
@@ -5,41 +5,25 @@ 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 {
toCircuitDeviceRowCreateValues,
toCircuitDeviceRowPatchValues,
toCircuitDeviceRowUpdateValues,
type CircuitDeviceRowCreateInput,
type CircuitDeviceRowPatchInput,
type CircuitDeviceRowUpdateInput,
} from "./circuit-device-row.persistence.js";
import {
toCircuitCreateValues,
type CircuitCreatePersistenceInput,
} from "./circuit.repository.js";
export interface CircuitDeviceRowUpdateInput {
linkedProjectDeviceId?: string;
name: string;
displayName: string;
phaseType?: string;
connectionKind?: string;
costGroup?: string;
category?: string;
level?: string;
roomId?: string;
roomNumberSnapshot?: string;
roomNameSnapshot?: string;
quantity: number;
powerPerUnit: number;
simultaneityFactor: number;
cosPhi?: number;
remark?: string;
overriddenFields?: string;
}
export type CircuitDeviceRowPatchInput = Partial<CircuitDeviceRowUpdateInput> & {
sortOrder?: number;
};
export interface CircuitDeviceRowCreateInput extends CircuitDeviceRowUpdateInput {
circuitId: string;
linkedProjectDeviceId?: string;
legacyConsumerId?: string;
sortOrder: number;
}
export type {
CircuitDeviceRowCreateInput,
CircuitDeviceRowPatchInput,
CircuitDeviceRowUpdateInput,
} from "./circuit-device-row.persistence.js";
export { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
export interface CircuitDeviceRowsBulkMoveInput {
rows: Array<{ id: string; expectedCircuitId: string }>;
@@ -60,71 +44,6 @@ export interface CircuitWithDeviceRowsCreateInput {
>;
}
function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
return {
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
name: input.name,
displayName: input.displayName,
phaseType: input.phaseType ?? null,
connectionKind: input.connectionKind ?? null,
costGroup: input.costGroup ?? null,
category: input.category ?? null,
level: input.level ?? null,
roomId: input.roomId ?? null,
roomNumberSnapshot: input.roomNumberSnapshot ?? null,
roomNameSnapshot: input.roomNameSnapshot ?? null,
quantity: input.quantity,
powerPerUnit: input.powerPerUnit,
simultaneityFactor: input.simultaneityFactor,
cosPhi: input.cosPhi ?? null,
remark: input.remark ?? null,
overriddenFields: input.overriddenFields ?? null,
};
}
function toPatchValues(input: CircuitDeviceRowPatchInput) {
const values: Partial<typeof circuitDeviceRows.$inferInsert> = {};
const has = (field: keyof CircuitDeviceRowPatchInput) =>
Object.prototype.hasOwnProperty.call(input, field);
if (has("linkedProjectDeviceId")) {
values.linkedProjectDeviceId = input.linkedProjectDeviceId ?? null;
}
if (input.name !== undefined) values.name = input.name;
if (input.displayName !== undefined) values.displayName = input.displayName;
if (has("phaseType")) values.phaseType = input.phaseType ?? null;
if (has("connectionKind")) values.connectionKind = input.connectionKind ?? null;
if (has("costGroup")) values.costGroup = input.costGroup ?? null;
if (has("category")) values.category = input.category ?? null;
if (has("level")) values.level = input.level ?? null;
if (has("roomId")) values.roomId = input.roomId ?? null;
if (has("roomNumberSnapshot")) {
values.roomNumberSnapshot = input.roomNumberSnapshot ?? null;
}
if (has("roomNameSnapshot")) values.roomNameSnapshot = input.roomNameSnapshot ?? null;
if (input.quantity !== undefined) values.quantity = input.quantity;
if (input.powerPerUnit !== undefined) values.powerPerUnit = input.powerPerUnit;
if (input.simultaneityFactor !== undefined) {
values.simultaneityFactor = input.simultaneityFactor;
}
if (has("cosPhi")) values.cosPhi = input.cosPhi ?? null;
if (has("remark")) values.remark = input.remark ?? null;
if (has("overriddenFields")) values.overriddenFields = input.overriddenFields ?? null;
if (input.sortOrder !== undefined) values.sortOrder = input.sortOrder;
return values;
}
export function toCircuitDeviceRowCreateValues(id: string, input: CircuitDeviceRowCreateInput) {
return {
id,
circuitId: input.circuitId,
legacyConsumerId: input.legacyConsumerId ?? null,
sortOrder: input.sortOrder,
...toUpdateValues(input),
};
}
export class CircuitDeviceRowRepository {
async findById(rowId: string) {
const [row] = await db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId)).limit(1);
@@ -212,41 +131,6 @@ export class CircuitDeviceRowRepository {
return id;
}
createInCircuitTransactional(
input: Omit<CircuitDeviceRowCreateInput, "sortOrder"> & { sortOrder?: number }
) {
const id = crypto.randomUUID();
db.transaction((tx) => {
const [circuit] = tx
.select({ id: circuits.id })
.from(circuits)
.where(eq(circuits.id, input.circuitId))
.limit(1)
.all();
if (!circuit) {
throw new Error("Der Stromkreis ist ungültig.");
}
const existingRows = tx
.select({ sortOrder: circuitDeviceRows.sortOrder })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, input.circuitId))
.all();
const lastSortOrder = existingRows.reduce(
(highest, row) => Math.max(highest, row.sortOrder),
0
);
const sortOrder = input.sortOrder ?? lastSortOrder + 10;
tx
.insert(circuitDeviceRows)
.values(toCircuitDeviceRowCreateValues(id, { ...input, sortOrder }))
.run();
tx.update(circuits).set({ isReserve: 0 }).where(eq(circuits.id, input.circuitId)).run();
});
return id;
}
createCircuitWithDeviceRowsTransactional(input: CircuitWithDeviceRowsCreateInput) {
if (input.deviceRows.length === 0) {
throw new Error("Mindestens eine Gerätezeile ist erforderlich.");
@@ -289,12 +173,12 @@ export class CircuitDeviceRowRepository {
) {
await db
.update(circuitDeviceRows)
.set(toUpdateValues(input))
.set(toCircuitDeviceRowUpdateValues(input))
.where(eq(circuitDeviceRows.id, rowId));
}
async updateFields(rowId: string, input: CircuitDeviceRowPatchInput) {
const values = toPatchValues(input);
const values = toCircuitDeviceRowPatchValues(input);
if (Object.keys(values).length === 0) {
return;
}
@@ -309,7 +193,7 @@ export class CircuitDeviceRowRepository {
for (const change of changes) {
const result = tx
.update(circuitDeviceRows)
.set(toUpdateValues(change.input))
.set(toCircuitDeviceRowUpdateValues(change.input))
.where(
and(
eq(circuitDeviceRows.id, change.rowId),
@@ -363,44 +247,6 @@ export class CircuitDeviceRowRepository {
await db.delete(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId));
}
deleteFromCircuitTransactional(rowId: string, expectedCircuitId: string) {
db.transaction((tx) => {
const [row] = tx
.select({ id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, rowId))
.limit(1)
.all();
if (!row || row.circuitId !== expectedCircuitId) {
throw new Error("Die Gerätezeile wurde vor dem Löschen verändert.");
}
const result = tx
.delete(circuitDeviceRows)
.where(
and(
eq(circuitDeviceRows.id, rowId),
eq(circuitDeviceRows.circuitId, expectedCircuitId)
)
)
.run();
if (result.changes !== 1) {
throw new Error("Die Gerätezeile konnte nicht gelöscht werden.");
}
const remainingRows = tx
.select({ id: circuitDeviceRows.id })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, expectedCircuitId))
.all();
tx
.update(circuits)
.set({ isReserve: remainingRows.length === 0 ? 1 : 0 })
.where(eq(circuits.id, expectedCircuitId))
.run();
});
}
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
await db
.update(circuitDeviceRows)
@@ -0,0 +1,26 @@
export interface CreateCircuitDeviceRowTransactionInput {
circuitId: string;
linkedProjectDeviceId?: string;
sortOrder?: number;
name: string;
displayName: string;
phaseType?: string;
connectionKind?: string;
costGroup?: string;
category?: string;
level?: string;
roomId?: string;
roomNumberSnapshot?: string;
roomNameSnapshot?: string;
quantity: number;
powerPerUnit: number;
simultaneityFactor: number;
cosPhi?: number;
remark?: string;
overriddenFields?: string;
}
export interface CircuitDeviceRowTransactionStore {
createInCircuit(input: CreateCircuitDeviceRowTransactionInput): string;
deleteFromCircuit(rowId: string, expectedCircuitId: string): void;
}
+13 -2
View File
@@ -1,4 +1,5 @@
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
import type { CircuitDeviceRowTransactionStore } from "../ports/circuit-device-row-transaction.store.js";
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
@@ -26,6 +27,7 @@ export class CircuitWriteService {
private readonly circuitSectionRepository: CircuitSectionRepository;
private readonly circuitListRepository: CircuitListRepository;
private readonly deviceRowRepository: CircuitDeviceRowRepository;
private readonly deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
private readonly projectDeviceRepository: ProjectDeviceRepository;
private readonly numberingService: CircuitNumberingService;
@@ -34,6 +36,7 @@ export class CircuitWriteService {
circuitSectionRepository?: CircuitSectionRepository;
circuitListRepository?: CircuitListRepository;
deviceRowRepository?: CircuitDeviceRowRepository;
deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
projectDeviceRepository?: ProjectDeviceRepository;
numberingService?: CircuitNumberingService;
}) {
@@ -41,6 +44,7 @@ export class CircuitWriteService {
this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository();
this.circuitListRepository = deps?.circuitListRepository ?? new CircuitListRepository();
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
this.deviceRowTransactionStore = deps?.deviceRowTransactionStore;
this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository();
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
}
@@ -87,6 +91,13 @@ export class CircuitWriteService {
}
}
private getDeviceRowTransactionStore() {
if (!this.deviceRowTransactionStore) {
throw new Error("Circuit device-row transactions are not configured.");
}
return this.deviceRowTransactionStore;
}
private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
if (!linkedProjectDeviceId) {
return;
@@ -204,7 +215,7 @@ export class CircuitWriteService {
}
await this.assertValidLinkedProjectDevice(circuitId, input.linkedProjectDeviceId);
const rowId = this.deviceRowRepository.createInCircuitTransactional({
const rowId = this.getDeviceRowTransactionStore().createInCircuit({
circuitId,
linkedProjectDeviceId: input.linkedProjectDeviceId,
sortOrder: input.sortOrder,
@@ -266,7 +277,7 @@ export class CircuitWriteService {
if (!circuit) {
throw new Error("Invalid circuit id.");
}
this.deviceRowRepository.deleteFromCircuitTransactional(rowId, circuit.id);
this.getDeviceRowTransactionStore().deleteFromCircuit(rowId, circuit.id);
}
async moveDeviceRow(rowId: string, input: MoveCircuitDeviceRowInput) {
@@ -0,0 +1,7 @@
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
import { db } from "../../db/client.js";
import { CircuitDeviceRowTransactionRepository } from "../../db/repositories/circuit-device-row-transaction.repository.js";
export const circuitWriteService = new CircuitWriteService({
deviceRowTransactionStore: new CircuitDeviceRowTransactionRepository(db),
});
@@ -1,5 +1,5 @@
import type { Request, Response } from "express";
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
import { circuitWriteService } from "../composition/circuit-write-service.js";
import {
createCircuitDeviceRowSchema,
moveCircuitDeviceRowsBulkSchema,
@@ -7,8 +7,6 @@ import {
updateCircuitDeviceRowSchema,
} from "../../shared/validation/circuit.schemas.js";
const circuitWriteService = new CircuitWriteService();
export async function createCircuitDeviceRow(req: Request, res: Response) {
const { circuitId } = req.params;
if (typeof circuitId !== "string") {
@@ -1,12 +1,10 @@
import type { Request, Response } from "express";
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
import { circuitWriteService } from "../composition/circuit-write-service.js";
import {
reorderSectionCircuitsSchema,
updateSectionEquipmentIdentifiersSchema,
} from "../../shared/validation/circuit.schemas.js";
const circuitWriteService = new CircuitWriteService();
export async function renumberCircuitSection(req: Request, res: Response) {
const { sectionId } = req.params;
if (typeof sectionId !== "string") {
+1 -3
View File
@@ -1,13 +1,11 @@
import type { Request, Response } from "express";
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
import { circuitWriteService } from "../composition/circuit-write-service.js";
import {
createCircuitSchema,
createCircuitWithDeviceRowsSchema,
updateCircuitSchema,
} from "../../shared/validation/circuit.schemas.js";
const circuitWriteService = new CircuitWriteService();
export async function createCircuit(req: Request, res: Response) {
const { projectId, circuitListId } = req.params;
if (typeof projectId !== "string" || typeof circuitListId !== "string") {