Compare commits
2 Commits
e4fd7e0df6
...
eb945a9622
| Author | SHA1 | Date | |
|---|---|---|---|
| eb945a9622 | |||
| 9cc3b7c08c |
@@ -75,6 +75,7 @@ Intent is separated by drag source type:
|
|||||||
- drop to placeholder -> create new target circuit and move row(s)
|
- drop to placeholder -> create new target circuit and move row(s)
|
||||||
- crossing a section boundary requires explicit confirmation
|
- crossing a section boundary requires explicit confirmation
|
||||||
- phase type, category, linked project device and local row values remain unchanged
|
- phase type, category, linked project device and local row values remain unchanged
|
||||||
|
- target creation, row assignments and reserve-state updates use one SQLite transaction
|
||||||
- circuit drag (BMK handle):
|
- circuit drag (BMK handle):
|
||||||
- reorder circuits inside same section only
|
- reorder circuits inside same section only
|
||||||
- cross-section reorder is rejected
|
- cross-section reorder is rejected
|
||||||
@@ -125,4 +126,3 @@ Current limitations:
|
|||||||
|
|
||||||
- session-local only
|
- session-local only
|
||||||
- no persisted history across browser reload
|
- no persisted history across browser reload
|
||||||
- some multi-step backend flows are not fully transaction-hardened end-to-end
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
- No global cross-project device library workflow is implemented yet.
|
- No global cross-project device library workflow is implemented yet.
|
||||||
- Final electrical sizing logic is not implemented yet.
|
- Final electrical sizing logic is not implemented yet.
|
||||||
- No full norm-compliant voltage-drop and protection-dimensioning calculation flow yet.
|
- No full norm-compliant voltage-drop and protection-dimensioning calculation flow yet.
|
||||||
- Bulk device-row move flow is command-based but not fully transaction-hardened end-to-end across all affected circuits.
|
|
||||||
- Sorting is view-only until users explicitly apply sorted order.
|
- Sorting is view-only until users explicitly apply sorted order.
|
||||||
- Cross-section circuit drag-reorder is intentionally blocked.
|
- Cross-section circuit drag-reorder is intentionally blocked.
|
||||||
- Legacy consumer and circuit-first paths coexist; migration is transitional and still requires operational discipline.
|
- Legacy consumer and circuit-first paths coexist; migration is transitional and still requires operational discipline.
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ export interface CircuitDeviceRowUpdateInput {
|
|||||||
overriddenFields?: string;
|
overriddenFields?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CircuitDeviceRowsBulkMoveInput {
|
||||||
|
rows: Array<{ id: string; expectedCircuitId: string }>;
|
||||||
|
targetCircuitId?: string;
|
||||||
|
createTargetCircuit?: {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName: string;
|
||||||
|
sortOrder: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
|
function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
|
||||||
return {
|
return {
|
||||||
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
|
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
|
||||||
@@ -259,4 +271,138 @@ export class CircuitDeviceRowRepository {
|
|||||||
})
|
})
|
||||||
.where(eq(circuitDeviceRows.id, rowId));
|
.where(eq(circuitDeviceRows.id, rowId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveRowsTransactional(input: CircuitDeviceRowsBulkMoveInput) {
|
||||||
|
if (input.rows.length === 0) {
|
||||||
|
throw new Error("Keine Gerätezeilen zum Verschieben angegeben.");
|
||||||
|
}
|
||||||
|
if (Boolean(input.targetCircuitId) === Boolean(input.createTargetCircuit)) {
|
||||||
|
throw new Error("Für die Mehrfachverschiebung muss genau ein Ziel angegeben werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowIds = input.rows.map((row) => row.id);
|
||||||
|
if (new Set(rowIds).size !== rowIds.length) {
|
||||||
|
throw new Error("Gerätezeilen dürfen nicht mehrfach ausgewählt sein.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdCircuitId = input.createTargetCircuit ? crypto.randomUUID() : undefined;
|
||||||
|
const targetCircuitId = input.targetCircuitId ?? createdCircuitId!;
|
||||||
|
|
||||||
|
// better-sqlite3 transactions must remain synchronous. Every row assignment,
|
||||||
|
// reserve-state update and optional target creation is committed or rolled back
|
||||||
|
// as one operation.
|
||||||
|
db.transaction((tx) => {
|
||||||
|
const persistedRows = tx
|
||||||
|
.select({ id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId })
|
||||||
|
.from(circuitDeviceRows)
|
||||||
|
.where(inArray(circuitDeviceRows.id, rowIds))
|
||||||
|
.all();
|
||||||
|
if (persistedRows.length !== input.rows.length) {
|
||||||
|
throw new Error("Eine oder mehrere Gerätezeilen sind ungültig.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistedRowsById = new Map(persistedRows.map((row) => [row.id, row]));
|
||||||
|
for (const expectedRow of input.rows) {
|
||||||
|
if (persistedRowsById.get(expectedRow.id)?.circuitId !== expectedRow.expectedCircuitId) {
|
||||||
|
throw new Error("Eine Gerätezeile wurde vor Abschluss der Verschiebung verändert.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetCircuit: { id: string; circuitListId: string };
|
||||||
|
if (input.createTargetCircuit) {
|
||||||
|
tx
|
||||||
|
.insert(circuits)
|
||||||
|
.values({
|
||||||
|
id: targetCircuitId,
|
||||||
|
circuitListId: input.createTargetCircuit.circuitListId,
|
||||||
|
sectionId: input.createTargetCircuit.sectionId,
|
||||||
|
equipmentIdentifier: input.createTargetCircuit.equipmentIdentifier,
|
||||||
|
displayName: input.createTargetCircuit.displayName,
|
||||||
|
sortOrder: input.createTargetCircuit.sortOrder,
|
||||||
|
isReserve: 0,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
targetCircuit = {
|
||||||
|
id: targetCircuitId,
|
||||||
|
circuitListId: input.createTargetCircuit.circuitListId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const [persistedTargetCircuit] = tx
|
||||||
|
.select({ id: circuits.id, circuitListId: circuits.circuitListId })
|
||||||
|
.from(circuits)
|
||||||
|
.where(eq(circuits.id, targetCircuitId))
|
||||||
|
.limit(1)
|
||||||
|
.all();
|
||||||
|
if (!persistedTargetCircuit) {
|
||||||
|
throw new Error("Der Zielstromkreis ist ungültig.");
|
||||||
|
}
|
||||||
|
targetCircuit = persistedTargetCircuit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCircuitIds = [...new Set(input.rows.map((row) => row.expectedCircuitId))];
|
||||||
|
const sourceCircuits = tx
|
||||||
|
.select({ id: circuits.id, circuitListId: circuits.circuitListId })
|
||||||
|
.from(circuits)
|
||||||
|
.where(inArray(circuits.id, sourceCircuitIds))
|
||||||
|
.all();
|
||||||
|
if (sourceCircuits.length !== sourceCircuitIds.length) {
|
||||||
|
throw new Error("Einer oder mehrere Quellstromkreise sind ungültig.");
|
||||||
|
}
|
||||||
|
if (sourceCircuits.some((circuit) => circuit.circuitListId !== targetCircuit.circuitListId)) {
|
||||||
|
throw new Error("Alle verschobenen Zeilen müssen zur Stromkreisliste des Ziels gehören.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const movedRowIdSet = new Set(rowIds);
|
||||||
|
const retainedTargetRows = tx
|
||||||
|
.select({ id: circuitDeviceRows.id, sortOrder: circuitDeviceRows.sortOrder })
|
||||||
|
.from(circuitDeviceRows)
|
||||||
|
.where(eq(circuitDeviceRows.circuitId, targetCircuitId))
|
||||||
|
.all()
|
||||||
|
.filter((row) => !movedRowIdSet.has(row.id));
|
||||||
|
const lastTargetSortOrder = retainedTargetRows.reduce(
|
||||||
|
(highest, row) => Math.max(highest, row.sortOrder),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let index = 0; index < input.rows.length; index += 1) {
|
||||||
|
const row = input.rows[index];
|
||||||
|
const result = tx
|
||||||
|
.update(circuitDeviceRows)
|
||||||
|
.set({
|
||||||
|
circuitId: targetCircuitId,
|
||||||
|
sortOrder: lastTargetSortOrder + (index + 1) * 10,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(circuitDeviceRows.id, row.id),
|
||||||
|
eq(circuitDeviceRows.circuitId, row.expectedCircuitId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
if (result.changes !== 1) {
|
||||||
|
throw new Error("Eine Gerätezeile wurde vor Abschluss der Verschiebung verändert.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sourceCircuitId of sourceCircuitIds) {
|
||||||
|
const remainingRows = tx
|
||||||
|
.select({ id: circuitDeviceRows.id })
|
||||||
|
.from(circuitDeviceRows)
|
||||||
|
.where(eq(circuitDeviceRows.circuitId, sourceCircuitId))
|
||||||
|
.all();
|
||||||
|
tx
|
||||||
|
.update(circuits)
|
||||||
|
.set({ isReserve: remainingRows.length === 0 ? 1 : 0 })
|
||||||
|
.where(eq(circuits.id, sourceCircuitId))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
tx.update(circuits).set({ isReserve: 0 }).where(eq(circuits.id, targetCircuitId)).run();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
movedRowIds: rowIds,
|
||||||
|
targetCircuitId,
|
||||||
|
createdCircuitId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,7 +316,18 @@ export class CircuitWriteService {
|
|||||||
throw new Error("Invalid target circuit id.");
|
throw new Error("Invalid target circuit id.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder-target move creates a new circuit explicitly; no implicit renumbering of others.
|
let createTargetCircuit:
|
||||||
|
| {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
// Placeholder-target move prepares a new circuit explicitly. Its creation and
|
||||||
|
// the row assignment are committed together without renumbering other circuits.
|
||||||
if (!targetCircuit) {
|
if (!targetCircuit) {
|
||||||
if (!input.targetSectionId || !input.createNewCircuit) {
|
if (!input.targetSectionId || !input.createNewCircuit) {
|
||||||
throw new Error("Invalid move target.");
|
throw new Error("Invalid move target.");
|
||||||
@@ -326,76 +337,27 @@ export class CircuitWriteService {
|
|||||||
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
||||||
const nextSortOrder =
|
const nextSortOrder =
|
||||||
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||||
const createdId = await this.circuitRepository.create({
|
createTargetCircuit = {
|
||||||
circuitListId: sourceCircuit.circuitListId,
|
circuitListId: sourceCircuit.circuitListId,
|
||||||
sectionId: section.id,
|
sectionId: section.id,
|
||||||
equipmentIdentifier: nextIdentifier,
|
equipmentIdentifier: nextIdentifier,
|
||||||
displayName: "New circuit",
|
displayName: "Neuer Stromkreis",
|
||||||
sortOrder: nextSortOrder,
|
sortOrder: nextSortOrder,
|
||||||
isReserve: false,
|
};
|
||||||
});
|
|
||||||
targetCircuit = await this.circuitRepository.findById(createdId);
|
|
||||||
if (!targetCircuit) {
|
|
||||||
throw new Error("Failed to create target circuit.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetCircuit.circuitListId !== sourceCircuit.circuitListId) {
|
if (targetCircuit && targetCircuit.circuitListId !== sourceCircuit.circuitListId) {
|
||||||
throw new Error("Target circuit does not belong to same circuit list.");
|
throw new Error("Target circuit does not belong to same circuit list.");
|
||||||
}
|
}
|
||||||
if (targetCircuit.id === sourceCircuit.id) {
|
if (targetCircuit?.id === sourceCircuit.id) {
|
||||||
return this.deviceRowRepository.findById(rowId);
|
return this.deviceRowRepository.findById(rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetCount = await this.deviceRowRepository.countByCircuit(targetCircuit.id);
|
this.deviceRowRepository.moveRowsTransactional({
|
||||||
await this.deviceRowRepository.moveToCircuit(rowId, targetCircuit.id, (targetCount + 1) * 10);
|
rows: [{ id: row.id, expectedCircuitId: row.circuitId }],
|
||||||
|
targetCircuitId: targetCircuit?.id,
|
||||||
const sourceRemaining = await this.deviceRowRepository.countByCircuit(sourceCircuit.id);
|
createTargetCircuit,
|
||||||
// Source circuit becomes reserve when all rows are moved away.
|
});
|
||||||
if (sourceRemaining === 0) {
|
|
||||||
await this.circuitRepository.update(sourceCircuit.id, {
|
|
||||||
sectionId: sourceCircuit.sectionId,
|
|
||||||
equipmentIdentifier: sourceCircuit.equipmentIdentifier,
|
|
||||||
displayName: sourceCircuit.displayName ?? undefined,
|
|
||||||
sortOrder: sourceCircuit.sortOrder,
|
|
||||||
protectionType: sourceCircuit.protectionType ?? undefined,
|
|
||||||
protectionRatedCurrent: sourceCircuit.protectionRatedCurrent ?? undefined,
|
|
||||||
protectionCharacteristic: sourceCircuit.protectionCharacteristic ?? undefined,
|
|
||||||
cableType: sourceCircuit.cableType ?? undefined,
|
|
||||||
cableCrossSection: sourceCircuit.cableCrossSection ?? undefined,
|
|
||||||
cableLength: sourceCircuit.cableLength ?? undefined,
|
|
||||||
rcdAssignment: sourceCircuit.rcdAssignment ?? undefined,
|
|
||||||
terminalDesignation: sourceCircuit.terminalDesignation ?? undefined,
|
|
||||||
voltage: sourceCircuit.voltage ?? undefined,
|
|
||||||
controlRequirement: sourceCircuit.controlRequirement ?? undefined,
|
|
||||||
status: sourceCircuit.status ?? undefined,
|
|
||||||
isReserve: true,
|
|
||||||
remark: sourceCircuit.remark ?? undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Target circuit is no longer reserve once it receives moved rows.
|
|
||||||
if (Boolean(targetCircuit.isReserve)) {
|
|
||||||
await this.circuitRepository.update(targetCircuit.id, {
|
|
||||||
sectionId: targetCircuit.sectionId,
|
|
||||||
equipmentIdentifier: targetCircuit.equipmentIdentifier,
|
|
||||||
displayName: targetCircuit.displayName ?? undefined,
|
|
||||||
sortOrder: targetCircuit.sortOrder,
|
|
||||||
protectionType: targetCircuit.protectionType ?? undefined,
|
|
||||||
protectionRatedCurrent: targetCircuit.protectionRatedCurrent ?? undefined,
|
|
||||||
protectionCharacteristic: targetCircuit.protectionCharacteristic ?? undefined,
|
|
||||||
cableType: targetCircuit.cableType ?? undefined,
|
|
||||||
cableCrossSection: targetCircuit.cableCrossSection ?? undefined,
|
|
||||||
cableLength: targetCircuit.cableLength ?? undefined,
|
|
||||||
rcdAssignment: targetCircuit.rcdAssignment ?? undefined,
|
|
||||||
terminalDesignation: targetCircuit.terminalDesignation ?? undefined,
|
|
||||||
voltage: targetCircuit.voltage ?? undefined,
|
|
||||||
controlRequirement: targetCircuit.controlRequirement ?? undefined,
|
|
||||||
status: targetCircuit.status ?? undefined,
|
|
||||||
isReserve: false,
|
|
||||||
remark: targetCircuit.remark ?? undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.deviceRowRepository.findById(rowId);
|
return this.deviceRowRepository.findById(rowId);
|
||||||
}
|
}
|
||||||
@@ -436,7 +398,17 @@ export class CircuitWriteService {
|
|||||||
throw new Error("Invalid target circuit id.");
|
throw new Error("Invalid target circuit id.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk placeholder move creates exactly one new circuit as common target.
|
// Bulk placeholder move prepares exactly one new circuit as common target.
|
||||||
|
// Its actual creation happens together with the row moves in one transaction.
|
||||||
|
let createTargetCircuit:
|
||||||
|
| {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
if (!targetCircuit) {
|
if (!targetCircuit) {
|
||||||
if (!input.targetSectionId || !input.createNewCircuit) {
|
if (!input.targetSectionId || !input.createNewCircuit) {
|
||||||
throw new Error("Invalid move target.");
|
throw new Error("Invalid move target.");
|
||||||
@@ -446,86 +418,27 @@ export class CircuitWriteService {
|
|||||||
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
||||||
const nextSortOrder =
|
const nextSortOrder =
|
||||||
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||||
const createdId = await this.circuitRepository.create({
|
createTargetCircuit = {
|
||||||
circuitListId: referenceSourceCircuit.circuitListId,
|
circuitListId: referenceSourceCircuit.circuitListId,
|
||||||
sectionId: section.id,
|
sectionId: section.id,
|
||||||
equipmentIdentifier: nextIdentifier,
|
equipmentIdentifier: nextIdentifier,
|
||||||
displayName: "New circuit",
|
displayName: "Neuer Stromkreis",
|
||||||
sortOrder: nextSortOrder,
|
sortOrder: nextSortOrder,
|
||||||
isReserve: false,
|
};
|
||||||
});
|
|
||||||
targetCircuit = await this.circuitRepository.findById(createdId);
|
|
||||||
if (!targetCircuit) {
|
|
||||||
throw new Error("Failed to create target circuit.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any source circuit emptied by bulk move is preserved as reserve circuit.
|
const targetCircuitListId = targetCircuit?.circuitListId ?? createTargetCircuit!.circuitListId;
|
||||||
for (const sourceCircuit of sourceCircuits.values()) {
|
for (const sourceCircuit of sourceCircuits.values()) {
|
||||||
if (sourceCircuit.circuitListId !== targetCircuit.circuitListId) {
|
if (sourceCircuit.circuitListId !== targetCircuitListId) {
|
||||||
throw new Error("All moved rows must belong to same circuit list as target.");
|
throw new Error("All moved rows must belong to same circuit list as target.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetCount = await this.deviceRowRepository.countByCircuit(targetCircuit.id);
|
return this.deviceRowRepository.moveRowsTransactional({
|
||||||
let offset = 1;
|
rows: rows.map((row) => ({ id: row.id, expectedCircuitId: row.circuitId })),
|
||||||
for (const row of rows) {
|
targetCircuitId: targetCircuit?.id,
|
||||||
await this.deviceRowRepository.moveToCircuit(row.id, targetCircuit.id, (targetCount + offset) * 10);
|
createTargetCircuit,
|
||||||
offset += 1;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
for (const sourceCircuit of sourceCircuits.values()) {
|
|
||||||
const remaining = await this.deviceRowRepository.countByCircuit(sourceCircuit.id);
|
|
||||||
if (remaining === 0) {
|
|
||||||
await this.circuitRepository.update(sourceCircuit.id, {
|
|
||||||
sectionId: sourceCircuit.sectionId,
|
|
||||||
equipmentIdentifier: sourceCircuit.equipmentIdentifier,
|
|
||||||
displayName: sourceCircuit.displayName ?? undefined,
|
|
||||||
sortOrder: sourceCircuit.sortOrder,
|
|
||||||
protectionType: sourceCircuit.protectionType ?? undefined,
|
|
||||||
protectionRatedCurrent: sourceCircuit.protectionRatedCurrent ?? undefined,
|
|
||||||
protectionCharacteristic: sourceCircuit.protectionCharacteristic ?? undefined,
|
|
||||||
cableType: sourceCircuit.cableType ?? undefined,
|
|
||||||
cableCrossSection: sourceCircuit.cableCrossSection ?? undefined,
|
|
||||||
cableLength: sourceCircuit.cableLength ?? undefined,
|
|
||||||
rcdAssignment: sourceCircuit.rcdAssignment ?? undefined,
|
|
||||||
terminalDesignation: sourceCircuit.terminalDesignation ?? undefined,
|
|
||||||
voltage: sourceCircuit.voltage ?? undefined,
|
|
||||||
controlRequirement: sourceCircuit.controlRequirement ?? undefined,
|
|
||||||
status: sourceCircuit.status ?? undefined,
|
|
||||||
isReserve: true,
|
|
||||||
remark: sourceCircuit.remark ?? undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Boolean(targetCircuit.isReserve)) {
|
|
||||||
await this.circuitRepository.update(targetCircuit.id, {
|
|
||||||
sectionId: targetCircuit.sectionId,
|
|
||||||
equipmentIdentifier: targetCircuit.equipmentIdentifier,
|
|
||||||
displayName: targetCircuit.displayName ?? undefined,
|
|
||||||
sortOrder: targetCircuit.sortOrder,
|
|
||||||
protectionType: targetCircuit.protectionType ?? undefined,
|
|
||||||
protectionRatedCurrent: targetCircuit.protectionRatedCurrent ?? undefined,
|
|
||||||
protectionCharacteristic: targetCircuit.protectionCharacteristic ?? undefined,
|
|
||||||
cableType: targetCircuit.cableType ?? undefined,
|
|
||||||
cableCrossSection: targetCircuit.cableCrossSection ?? undefined,
|
|
||||||
cableLength: targetCircuit.cableLength ?? undefined,
|
|
||||||
rcdAssignment: targetCircuit.rcdAssignment ?? undefined,
|
|
||||||
terminalDesignation: targetCircuit.terminalDesignation ?? undefined,
|
|
||||||
voltage: targetCircuit.voltage ?? undefined,
|
|
||||||
controlRequirement: targetCircuit.controlRequirement ?? undefined,
|
|
||||||
status: targetCircuit.status ?? undefined,
|
|
||||||
isReserve: false,
|
|
||||||
remark: targetCircuit.remark ?? undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
movedRowIds: rows.map((row) => row.id),
|
|
||||||
targetCircuitId: targetCircuit.id,
|
|
||||||
createdCircuitId: input.targetCircuitId ? undefined : targetCircuit.id,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextIdentifier(sectionId: string) {
|
async getNextIdentifier(sectionId: string) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
||||||
|
import { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js";
|
||||||
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
||||||
import { db } from "../src/db/client.js";
|
import { db } from "../src/db/client.js";
|
||||||
import { createCircuitSchema } from "../src/shared/validation/circuit.schemas.js";
|
import { createCircuitSchema } from "../src/shared/validation/circuit.schemas.js";
|
||||||
@@ -298,24 +299,24 @@ describe("circuit write service rules", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("moving a device row to another circuit preserves row and toggles reserve flags", async () => {
|
it("moving a device row to another circuit preserves row and toggles reserve flags", async () => {
|
||||||
const updatedReserve: Array<{ id: string; isReserve: boolean }> = [];
|
let transactionalMove:
|
||||||
const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = [];
|
| {
|
||||||
|
rows: Array<{ id: string; expectedCircuitId: string }>;
|
||||||
|
targetCircuitId?: string;
|
||||||
|
createTargetCircuit?: unknown;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
deviceRowRepository: {
|
deviceRowRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
return { id: "r1", circuitId: "c1" } as never;
|
return { id: "r1", circuitId: "c1" } as never;
|
||||||
},
|
},
|
||||||
async countByCircuit(circuitId: string) {
|
moveRowsTransactional(input: typeof transactionalMove) {
|
||||||
if (circuitId === "c2") {
|
transactionalMove = input;
|
||||||
return 2;
|
return {
|
||||||
}
|
movedRowIds: input!.rows.map((row) => row.id),
|
||||||
if (circuitId === "c1") {
|
targetCircuitId: input!.targetCircuitId!,
|
||||||
return 0;
|
};
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
|
|
||||||
movedCalls.push({ rowId, targetCircuitId, sortOrder });
|
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
@@ -339,35 +340,39 @@ describe("circuit write service rules", () => {
|
|||||||
isReserve: 1,
|
isReserve: 1,
|
||||||
} as never;
|
} as never;
|
||||||
},
|
},
|
||||||
async update(id: string, payload: { isReserve: boolean }) {
|
|
||||||
updatedReserve.push({ id, isReserve: payload.isReserve });
|
|
||||||
},
|
|
||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.moveDeviceRow("r1", { targetCircuitId: "c2" });
|
await service.moveDeviceRow("r1", { targetCircuitId: "c2" });
|
||||||
assert.deepEqual(movedCalls, [{ rowId: "r1", targetCircuitId: "c2", sortOrder: 30 }]);
|
assert.deepEqual(transactionalMove, {
|
||||||
assert.deepEqual(updatedReserve, [
|
rows: [{ id: "r1", expectedCircuitId: "c1" }],
|
||||||
{ id: "c1", isReserve: true },
|
targetCircuitId: "c2",
|
||||||
{ id: "c2", isReserve: false },
|
createTargetCircuit: undefined,
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moving a device row to placeholder creates a new circuit in target section", async () => {
|
it("moving a device row to placeholder creates a new circuit in target section", async () => {
|
||||||
let createdCircuitPayload: { sectionId: string; equipmentIdentifier: string; isReserve?: boolean } | null = null;
|
let preparedTarget:
|
||||||
|
| {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
deviceRowRepository: {
|
deviceRowRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
return { id: "r1", circuitId: "c1" } as never;
|
return { id: "r1", circuitId: "c1" } as never;
|
||||||
},
|
},
|
||||||
async countByCircuit(circuitId: string) {
|
moveRowsTransactional(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) {
|
||||||
if (circuitId === "c-new") {
|
preparedTarget = input.createTargetCircuit;
|
||||||
return 0;
|
return {
|
||||||
}
|
movedRowIds: input.rows.map((row) => row.id),
|
||||||
return 1;
|
targetCircuitId: "c-new",
|
||||||
},
|
createdCircuitId: "c-new",
|
||||||
async moveToCircuit() {
|
};
|
||||||
return;
|
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
@@ -382,28 +387,11 @@ describe("circuit write service rules", () => {
|
|||||||
isReserve: 0,
|
isReserve: 0,
|
||||||
} as never;
|
} as never;
|
||||||
}
|
}
|
||||||
if (circuitId === "c-new") {
|
|
||||||
return {
|
|
||||||
id: "c-new",
|
|
||||||
sectionId: "s2",
|
|
||||||
circuitListId: "l1",
|
|
||||||
equipmentIdentifier: "-2F8",
|
|
||||||
sortOrder: 50,
|
|
||||||
isReserve: 0,
|
|
||||||
} as never;
|
|
||||||
}
|
|
||||||
return null as never;
|
return null as never;
|
||||||
},
|
},
|
||||||
async listBySection() {
|
async listBySection() {
|
||||||
return [{ sortOrder: 40 }] as never[];
|
return [{ sortOrder: 40 }] as never[];
|
||||||
},
|
},
|
||||||
async create(payload: { sectionId: string; equipmentIdentifier: string; isReserve?: boolean }) {
|
|
||||||
createdCircuitPayload = payload;
|
|
||||||
return "c-new";
|
|
||||||
},
|
|
||||||
async update() {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
} as never,
|
} as never,
|
||||||
circuitSectionRepository: {
|
circuitSectionRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
@@ -418,14 +406,54 @@ describe("circuit write service rules", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true });
|
await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true });
|
||||||
assert.equal(createdCircuitPayload?.sectionId, "s2");
|
assert.deepEqual(preparedTarget, {
|
||||||
assert.equal(createdCircuitPayload?.equipmentIdentifier, "-2F8");
|
circuitListId: "l1",
|
||||||
assert.equal(createdCircuitPayload?.isReserve, false);
|
sectionId: "s2",
|
||||||
|
equipmentIdentifier: "-2F8",
|
||||||
|
displayName: "Neuer Stromkreis",
|
||||||
|
sortOrder: 50,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moving a device row to its current circuit remains a no-op", async () => {
|
||||||
|
let transactionalMoveCount = 0;
|
||||||
|
const service = new CircuitWriteService({
|
||||||
|
deviceRowRepository: {
|
||||||
|
async findById() {
|
||||||
|
return { id: "r1", circuitId: "c1" } as never;
|
||||||
|
},
|
||||||
|
moveRowsTransactional() {
|
||||||
|
transactionalMoveCount += 1;
|
||||||
|
throw new Error("same-circuit move must not write");
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
circuitRepository: {
|
||||||
|
async findById() {
|
||||||
|
return {
|
||||||
|
id: "c1",
|
||||||
|
sectionId: "s1",
|
||||||
|
circuitListId: "l1",
|
||||||
|
equipmentIdentifier: "-1F1",
|
||||||
|
sortOrder: 10,
|
||||||
|
isReserve: 0,
|
||||||
|
} as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.moveDeviceRow("r1", { targetCircuitId: "c1" });
|
||||||
|
assert.equal(transactionalMoveCount, 0);
|
||||||
|
assert.equal(result?.id, "r1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => {
|
it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => {
|
||||||
const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = [];
|
let transactionalMove:
|
||||||
const reserveUpdates: Array<{ id: string; isReserve: boolean }> = [];
|
| {
|
||||||
|
rows: Array<{ id: string; expectedCircuitId: string }>;
|
||||||
|
targetCircuitId?: string;
|
||||||
|
createTargetCircuit?: unknown;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
deviceRowRepository: {
|
deviceRowRepository: {
|
||||||
async findById(rowId: string) {
|
async findById(rowId: string) {
|
||||||
@@ -434,14 +462,12 @@ describe("circuit write service rules", () => {
|
|||||||
}
|
}
|
||||||
return { id: "r2", circuitId: "c2" } as never;
|
return { id: "r2", circuitId: "c2" } as never;
|
||||||
},
|
},
|
||||||
async countByCircuit(circuitId: string) {
|
moveRowsTransactional(input: typeof transactionalMove) {
|
||||||
if (circuitId === "c3") {
|
transactionalMove = input;
|
||||||
return 1;
|
return {
|
||||||
}
|
movedRowIds: input!.rows.map((row) => row.id),
|
||||||
return 0;
|
targetCircuitId: input!.targetCircuitId!,
|
||||||
},
|
};
|
||||||
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
|
|
||||||
movedCalls.push({ rowId, targetCircuitId, sortOrder });
|
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
@@ -454,39 +480,48 @@ describe("circuit write service rules", () => {
|
|||||||
}
|
}
|
||||||
return { id: "c3", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F3", sortOrder: 30, isReserve: 1 } as never;
|
return { id: "c3", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F3", sortOrder: 30, isReserve: 1 } as never;
|
||||||
},
|
},
|
||||||
async update(id: string, payload: { isReserve: boolean }) {
|
|
||||||
reserveUpdates.push({ id, isReserve: payload.isReserve });
|
|
||||||
},
|
|
||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetCircuitId: "c3" });
|
const result = await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetCircuitId: "c3" });
|
||||||
assert.deepEqual(movedCalls, [
|
assert.deepEqual(transactionalMove, {
|
||||||
{ rowId: "r1", targetCircuitId: "c3", sortOrder: 20 },
|
rows: [
|
||||||
{ rowId: "r2", targetCircuitId: "c3", sortOrder: 30 },
|
{ id: "r1", expectedCircuitId: "c1" },
|
||||||
]);
|
{ id: "r2", expectedCircuitId: "c2" },
|
||||||
assert.deepEqual(reserveUpdates, [
|
],
|
||||||
{ id: "c1", isReserve: true },
|
targetCircuitId: "c3",
|
||||||
{ id: "c2", isReserve: true },
|
createTargetCircuit: undefined,
|
||||||
{ id: "c3", isReserve: false },
|
});
|
||||||
]);
|
assert.deepEqual(result, {
|
||||||
|
movedRowIds: ["r1", "r2"],
|
||||||
|
targetCircuitId: "c3",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moving multiple device rows to placeholder creates exactly one new circuit", async () => {
|
it("moving multiple device rows to placeholder creates exactly one new circuit", async () => {
|
||||||
let createCount = 0;
|
let transactionCount = 0;
|
||||||
|
let preparedTarget:
|
||||||
|
| {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
deviceRowRepository: {
|
deviceRowRepository: {
|
||||||
async findById(rowId: string) {
|
async findById(rowId: string) {
|
||||||
return { id: rowId, circuitId: rowId === "r1" ? "c1" : "c2" } as never;
|
return { id: rowId, circuitId: rowId === "r1" ? "c1" : "c2" } as never;
|
||||||
},
|
},
|
||||||
async countByCircuit(circuitId: string) {
|
moveRowsTransactional(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) {
|
||||||
if (circuitId === "c-new") {
|
transactionCount += 1;
|
||||||
return 0;
|
preparedTarget = input.createTargetCircuit;
|
||||||
}
|
return {
|
||||||
return 1;
|
movedRowIds: input.rows.map((row) => row.id),
|
||||||
},
|
targetCircuitId: "c-new",
|
||||||
async moveToCircuit() {
|
createdCircuitId: "c-new",
|
||||||
return;
|
};
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
@@ -494,21 +529,11 @@ describe("circuit write service rules", () => {
|
|||||||
if (circuitId === "c1" || circuitId === "c2") {
|
if (circuitId === "c1" || circuitId === "c2") {
|
||||||
return { id: circuitId, sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
return { id: circuitId, sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
||||||
}
|
}
|
||||||
if (circuitId === "c-new") {
|
|
||||||
return { id: "c-new", sectionId: "s2", circuitListId: "l1", equipmentIdentifier: "-2F8", sortOrder: 40, isReserve: 0 } as never;
|
|
||||||
}
|
|
||||||
return null as never;
|
return null as never;
|
||||||
},
|
},
|
||||||
async listBySection() {
|
async listBySection() {
|
||||||
return [{ sortOrder: 30 }] as never[];
|
return [{ sortOrder: 30 }] as never[];
|
||||||
},
|
},
|
||||||
async create() {
|
|
||||||
createCount += 1;
|
|
||||||
return "c-new";
|
|
||||||
},
|
|
||||||
async update() {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
} as never,
|
} as never,
|
||||||
circuitSectionRepository: {
|
circuitSectionRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
@@ -527,7 +552,14 @@ describe("circuit write service rules", () => {
|
|||||||
targetSectionId: "s2",
|
targetSectionId: "s2",
|
||||||
createNewCircuit: true,
|
createNewCircuit: true,
|
||||||
});
|
});
|
||||||
assert.equal(createCount, 1);
|
assert.equal(transactionCount, 1);
|
||||||
|
assert.deepEqual(preparedTarget, {
|
||||||
|
circuitListId: "l1",
|
||||||
|
sectionId: "s2",
|
||||||
|
equipmentIdentifier: "-2F8",
|
||||||
|
displayName: "Neuer Stromkreis",
|
||||||
|
sortOrder: 40,
|
||||||
|
});
|
||||||
assert.equal(result.createdCircuitId, "c-new");
|
assert.equal(result.createdCircuitId, "c-new");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -650,6 +682,94 @@ describe("circuit write service rules", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("bulk device row move uses one synchronous transaction callback", () => {
|
||||||
|
const repository = new CircuitDeviceRowRepository();
|
||||||
|
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
||||||
|
|
||||||
|
let callbackReturnedPromise = false;
|
||||||
|
let selectCall = 0;
|
||||||
|
let rowUpdateCount = 0;
|
||||||
|
const selectedRows = [
|
||||||
|
[
|
||||||
|
{ id: "r1", circuitId: "c1" },
|
||||||
|
{ id: "r2", circuitId: "c2" },
|
||||||
|
],
|
||||||
|
[{ id: "c3", circuitListId: "l1" }],
|
||||||
|
[
|
||||||
|
{ id: "c1", circuitListId: "l1" },
|
||||||
|
{ id: "c2", circuitListId: "l1" },
|
||||||
|
],
|
||||||
|
[{ id: "existing-target-row", sortOrder: 10 }],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
|
||||||
|
(db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => {
|
||||||
|
const fakeTx = {
|
||||||
|
select() {
|
||||||
|
const rows = selectedRows[selectCall++] ?? [];
|
||||||
|
const query = {
|
||||||
|
from() {
|
||||||
|
return query;
|
||||||
|
},
|
||||||
|
where() {
|
||||||
|
return query;
|
||||||
|
},
|
||||||
|
limit() {
|
||||||
|
return query;
|
||||||
|
},
|
||||||
|
all() {
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return query;
|
||||||
|
},
|
||||||
|
update() {
|
||||||
|
return {
|
||||||
|
set() {
|
||||||
|
return {
|
||||||
|
where() {
|
||||||
|
return {
|
||||||
|
run() {
|
||||||
|
rowUpdateCount += 1;
|
||||||
|
return { changes: 1 };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const callbackResult = cb(fakeTx);
|
||||||
|
callbackReturnedPromise = Boolean(
|
||||||
|
callbackResult && typeof (callbackResult as Promise<unknown>).then === "function"
|
||||||
|
);
|
||||||
|
if (callbackReturnedPromise) {
|
||||||
|
throw new Error("Transaction function cannot return a promise");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = repository.moveRowsTransactional({
|
||||||
|
rows: [
|
||||||
|
{ id: "r1", expectedCircuitId: "c1" },
|
||||||
|
{ id: "r2", expectedCircuitId: "c2" },
|
||||||
|
],
|
||||||
|
targetCircuitId: "c3",
|
||||||
|
});
|
||||||
|
assert.equal(callbackReturnedPromise, false);
|
||||||
|
assert.equal(rowUpdateCount, 5);
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
movedRowIds: ["r1", "r2"],
|
||||||
|
targetCircuitId: "c3",
|
||||||
|
createdCircuitId: undefined,
|
||||||
|
});
|
||||||
|
} 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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user