Persist device row moves
This commit is contained in:
@@ -220,14 +220,15 @@ After saving, the row becomes linked to the new project device.
|
|||||||
|
|
||||||
## Undo / Redo
|
## Undo / Redo
|
||||||
|
|
||||||
The editor's visible undo/redo stack remains session-local while move, reorder
|
The editor's visible undo/redo stack remains session-local while reorder and
|
||||||
and renumber operations are being migrated. Existing Circuit and
|
renumber operations are being migrated. Existing Circuit and CircuitDeviceRow
|
||||||
CircuitDeviceRow cell edits plus standalone insertion/deletion already execute
|
cell edits, standalone insertion/deletion and CircuitDeviceRow moves already
|
||||||
persistent project commands; their toolbar undo/redo actions use the
|
execute persistent project commands; their toolbar undo/redo actions use the
|
||||||
project-wide history endpoints. Insertions use client-generated stable UUIDs,
|
project-wide history endpoints. Insertions and generated move targets use
|
||||||
and deletion undo restores the same ids from complete server snapshots. The
|
client-generated stable UUIDs, and undo restores the same ids from complete
|
||||||
tree response supplies the optimistic `currentRevision`; obsolete direct field
|
server snapshots. The tree response supplies the optimistic `currentRevision`;
|
||||||
PATCH, structure POST and CircuitDeviceRow DELETE routes are removed.
|
obsolete direct field PATCH, structure POST, move, Circuit and CircuitDeviceRow
|
||||||
|
DELETE routes are removed.
|
||||||
Reloading the page does not yet reconstruct the visible editor stack.
|
Reloading the page does not yet reconstruct the visible editor stack.
|
||||||
CircuitDeviceRow moves between existing circuits and moves that create one new
|
CircuitDeviceRow moves between existing circuits and moves that create one new
|
||||||
placeholder target circuit are persisted. The latter stores the complete empty
|
placeholder target circuit are persisted. The latter stores the complete empty
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ snapshots contain zero, one or multiple complete device rows. Move commands
|
|||||||
contain each row's expected and target circuit plus its exact expected and
|
contain each row's expected and target circuit plus its exact expected and
|
||||||
target sort order. This makes deletion and moves undoable without generating
|
target sort order. This makes deletion and moves undoable without generating
|
||||||
replacement identities or renumbering circuits. Existing Circuit and
|
replacement identities or renumbering circuits. Existing Circuit and
|
||||||
CircuitDeviceRow cell edits plus standalone insert/delete actions in the editor
|
CircuitDeviceRow cell edits, standalone insert/delete actions and device-row
|
||||||
consume these endpoints. Move, reorder and renumber UI paths are still being
|
moves in the editor consume these endpoints. Reorder and renumber UI paths are
|
||||||
migrated.
|
still being migrated.
|
||||||
|
|
||||||
`circuit-device-row.move` targets existing circuits in the same circuit list.
|
`circuit-device-row.move` targets existing circuits in the same circuit list.
|
||||||
`circuit-device-row.move-with-new-circuit` atomically creates exactly one
|
`circuit-device-row.move-with-new-circuit` atomically creates exactly one
|
||||||
@@ -169,48 +169,11 @@ Response sketch:
|
|||||||
- `GET /circuit-sections/:sectionId/next-identifier`
|
- `GET /circuit-sections/:sectionId/next-identifier`
|
||||||
- preview next identifier for section (`prefix + maxSuffix + 1`)
|
- preview next identifier for section (`prefix + maxSuffix + 1`)
|
||||||
|
|
||||||
Circuit and device-row field updates, standalone insertions and standalone
|
Circuit and device-row field updates, standalone insertions/deletions and
|
||||||
deletions are available only as the corresponding versioned commands through
|
single or bulk device-row moves are available only as the corresponding
|
||||||
`POST /projects/:projectId/commands`. There are no direct field-update PATCH,
|
versioned commands through `POST /projects/:projectId/commands`. There are no
|
||||||
structure POST or CircuitDeviceRow DELETE routes. The direct
|
direct field-update PATCH, structure POST, move, Circuit DELETE or
|
||||||
`DELETE /circuits/:circuitId` route remains temporarily for the inverse of the
|
CircuitDeviceRow DELETE routes.
|
||||||
not-yet-migrated placeholder move UI and must not be used by new features.
|
|
||||||
|
|
||||||
### Move Device Row
|
|
||||||
|
|
||||||
- `PATCH /circuit-device-rows/:rowId/move`
|
|
||||||
- Purpose: move one row to existing circuit or to newly created circuit in target section.
|
|
||||||
|
|
||||||
Request sketch:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"targetCircuitId": "cir_target"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"targetSectionId": "sec_target",
|
|
||||||
"createNewCircuit": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bulk Move Device Rows
|
|
||||||
|
|
||||||
- `PATCH /circuit-device-rows/move-bulk`
|
|
||||||
- Purpose: move multiple rows in one command flow.
|
|
||||||
|
|
||||||
Request sketch:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"rowIds": ["row_1", "row_2"],
|
|
||||||
"targetCircuitId": "cir_target"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reorder Circuits
|
### Reorder Circuits
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Circuit List Editor Known Limitations
|
# Circuit List Editor Known Limitations
|
||||||
|
|
||||||
- Existing Circuit and CircuitDeviceRow cell edits plus standalone
|
- Existing Circuit and CircuitDeviceRow cell edits plus standalone
|
||||||
insert/delete actions use persistent project commands and project-wide
|
insert/delete actions and CircuitDeviceRow moves use persistent project
|
||||||
toolbar undo/redo. The editor's visible stack is still session-local, is
|
commands and project-wide toolbar undo/redo. The editor's visible stack is
|
||||||
empty after reload and still contains direct move/reorder/renumber operations
|
still session-local, is empty after reload and still contains direct
|
||||||
during the cutover.
|
reorder/renumber operations during the cutover.
|
||||||
- Server-side project revisions and persistent undo/redo eligibility exist for
|
- Server-side project revisions and persistent undo/redo eligibility exist for
|
||||||
the command types documented in `circuit-list-editor-api.md`; complete editor
|
the command types documented in `circuit-list-editor-api.md`; complete editor
|
||||||
command coverage and history reconstruction are not complete.
|
command coverage and history reconstruction are not complete.
|
||||||
|
|||||||
@@ -91,10 +91,11 @@ sowie eigenständiges Einfügen und Löschen verwendet das Grid bereits über di
|
|||||||
öffentliche Command-Grenze. Neue Circuits und Gerätezeilen erhalten ihre stabile
|
öffentliche Command-Grenze. Neue Circuits und Gerätezeilen erhalten ihre stabile
|
||||||
UUID vor dem Command; Löschen und Undo bewahren diese Identität. Der Tree liefert
|
UUID vor dem Command; Löschen und Undo bewahren diese Identität. Der Tree liefert
|
||||||
dazu `currentRevision`; Undo/Redo für diese Aktionen läuft über die projektweite
|
dazu `currentRevision`; Undo/Redo für diese Aktionen läuft über die projektweite
|
||||||
Serverhistorie. Direkte Feld-PATCH-, Struktur-POST- und Gerätezeilen-DELETE-
|
Serverhistorie. Direkte Feld-PATCH-, Struktur-POST-, Move-, Circuit- und
|
||||||
Endpunkte sind entfernt. Move-, Reorder- und Renumber-UI-Pfade verbleiben
|
Gerätezeilen-DELETE-Endpunkte sind entfernt. Gerätezeilen-Moves im Grid
|
||||||
vorerst im sichtbaren sitzungslokalen Stack; nach einem Reload wird dieser noch
|
verwenden die persistenten Kommandos bereits. Nur Reorder- und
|
||||||
nicht aus der Serverhistorie rekonstruiert.
|
Renumber-UI-Pfade verbleiben vorerst im sichtbaren sitzungslokalen Stack; nach
|
||||||
|
einem Reload wird dieser noch nicht aus der Serverhistorie rekonstruiert.
|
||||||
`circuit-device-row.move` verschiebt oder sortiert eine oder mehrere Zeilen
|
`circuit-device-row.move` verschiebt oder sortiert eine oder mehrere Zeilen
|
||||||
zwischen vorhandenen Stromkreisen derselben Liste. Erwartete und neue
|
zwischen vorhandenen Stromkreisen derselben Liste. Erwartete und neue
|
||||||
Stromkreis-/Sortierpositionen machen Forward und Inverse deterministisch;
|
Stromkreis-/Sortierpositionen machen Forward und Inverse deterministisch;
|
||||||
@@ -104,6 +105,9 @@ Verschieben auf einen freien Platz ab: Ein Zielstromkreis mit stabiler UUID und
|
|||||||
BMK wird zusammen mit allen Zeilenbewegungen erzeugt. Undo stellt die exakten
|
BMK wird zusammen mit allen Zeilenbewegungen erzeugt. Undo stellt die exakten
|
||||||
Quellpositionen wieder her und löscht den erzeugten Stromkreis nur, wenn dessen
|
Quellpositionen wieder her und löscht den erzeugten Stromkreis nur, wenn dessen
|
||||||
Felder und vollständiger Zeilenbestand unverändert sind.
|
Felder und vollständiger Zeilenbestand unverändert sind.
|
||||||
|
Der Editor erzeugt die vollständigen Move-Zuweisungen aus dem geladenen Tree,
|
||||||
|
vergibt für neue Ziele vor dem Kommando eine stabile UUID und führt das
|
||||||
|
Toolbar-Undo/Redo über die projektweite Historie aus.
|
||||||
`circuit.reorder-section` speichert die erwartete und neue Sortierposition
|
`circuit.reorder-section` speichert die erwartete und neue Sortierposition
|
||||||
jedes Stromkreises eines vollständigen Abschnitts. Forward, Undo und Redo
|
jedes Stromkreises eines vollständigen Abschnitts. Forward, Undo und Redo
|
||||||
ändern ausschließlich `sortOrder`; Stromkreisblöcke, Gerätezeilen und BMKs
|
ändern ausschließlich `sortOrder`; Stromkreisblöcke, Gerätezeilen und BMKs
|
||||||
|
|||||||
@@ -130,27 +130,8 @@ Expected response shape:
|
|||||||
|
|
||||||
If migrations were not applied, endpoint may return an empty fallback with a warning.
|
If migrations were not applied, endpoint may return an empty fallback with a warning.
|
||||||
|
|
||||||
## Dev-only visual test helper (multi-device circuit)
|
## Visual verification
|
||||||
|
|
||||||
Add one extra manual device row to an existing circuit:
|
Manual device rows and multi-device circuits can be created directly in the
|
||||||
|
circuit-list editor. The former development scripts for direct database writes
|
||||||
```bash
|
were removed because they bypassed project revisions and persistent undo/redo.
|
||||||
npm run dev:add-manual-circuit-row -- <circuitId>
|
|
||||||
```
|
|
||||||
|
|
||||||
Default inserted values:
|
|
||||||
- `name`: `Test sub device`
|
|
||||||
- `displayName`: `Beleuchtung WC`
|
|
||||||
- `phaseType`: `single_phase`
|
|
||||||
- `quantity`: `1`
|
|
||||||
- `powerPerUnit`: `0.05`
|
|
||||||
- `simultaneityFactor`: `1`
|
|
||||||
- `cosPhi`: `1`
|
|
||||||
|
|
||||||
The script prints the created row id.
|
|
||||||
|
|
||||||
Delete the test row again:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev:delete-circuit-row -- <rowId>
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -194,6 +194,9 @@ Completed foundation:
|
|||||||
empty target-circuit snapshot; forward creation and all row moves share one
|
empty target-circuit snapshot; forward creation and all row moves share one
|
||||||
transaction, while undo restores the source positions and deletes only an
|
transaction, while undo restores the source positions and deletes only an
|
||||||
unchanged generated target
|
unchanged generated target
|
||||||
|
- the circuit-list editor executes existing-target and generated-target row
|
||||||
|
moves through these commands; obsolete direct move and Circuit DELETE routes
|
||||||
|
plus their separate transaction adapter are removed
|
||||||
- `circuit.reorder-section` requires the complete section circuit set and
|
- `circuit.reorder-section` requires the complete section circuit set and
|
||||||
stores exact expected/target sort positions; its inverse changes no
|
stores exact expected/target sort positions; its inverse changes no
|
||||||
equipment identifier and never splits a circuit block
|
equipment identifier and never splits a circuit block
|
||||||
@@ -211,9 +214,8 @@ Completed foundation:
|
|||||||
|
|
||||||
Remaining constraints before completing persistent history:
|
Remaining constraints before completing persistent history:
|
||||||
|
|
||||||
- route the remaining move, reorder and renumber editor writes through their
|
- route the remaining reorder and renumber editor writes through their existing
|
||||||
existing typed persistent commands and remove the final direct mutation
|
typed persistent commands and remove the final direct mutation routes
|
||||||
routes
|
|
||||||
- reconstruct the visible frontend history state after reload; Circuit and
|
- reconstruct the visible frontend history state after reload; Circuit and
|
||||||
CircuitDeviceRow field/insert/delete actions plus ProjectDevice operations
|
CircuitDeviceRow field/insert/delete actions plus ProjectDevice operations
|
||||||
already use persistent commands and project-wide undo
|
already use persistent commands and project-wide undo
|
||||||
|
|||||||
+3
-5
@@ -14,16 +14,14 @@
|
|||||||
"build:api": "tsc -p tsconfig.json",
|
"build:api": "tsc -p tsconfig.json",
|
||||||
"build:web": "next build",
|
"build:web": "next build",
|
||||||
"start": "node dist/server/index.js",
|
"start": "node dist/server/index.js",
|
||||||
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/circuit-device-row-transaction.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-transaction.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-transaction.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||||
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/circuit-device-row-transaction.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-transaction.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-transaction.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:backup": "tsx scripts/db-backup.ts",
|
"db:backup": "tsx scripts/db-backup.ts",
|
||||||
"db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js",
|
"db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js",
|
||||||
"db:backfill:sections": "tsx scripts/db-backfill-sections.ts",
|
"db:backfill:sections": "tsx scripts/db-backfill-sections.ts",
|
||||||
"db:migrate:legacy-consumers": "tsx scripts/db-migrate-legacy-consumers.ts",
|
"db:migrate:legacy-consumers": "tsx scripts/db-migrate-legacy-consumers.ts"
|
||||||
"dev:add-manual-circuit-row": "tsx scripts/dev-add-manual-circuit-row.ts",
|
|
||||||
"dev:delete-circuit-row": "tsx scripts/dev-delete-circuit-row.ts"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { db } from "../src/db/client.js";
|
|
||||||
import { CircuitDeviceRowTransactionRepository } from "../src/db/repositories/circuit-device-row-transaction.repository.js";
|
|
||||||
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const circuitId = process.argv[2];
|
|
||||||
if (!circuitId) {
|
|
||||||
console.error("Usage: npm run dev:add-manual-circuit-row -- <circuitId>");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const circuitRepository = new CircuitRepository();
|
|
||||||
const rowTransactionRepository = new CircuitDeviceRowTransactionRepository(db);
|
|
||||||
|
|
||||||
const circuit = await circuitRepository.findById(circuitId);
|
|
||||||
if (!circuit) {
|
|
||||||
console.error(`Circuit not found: ${circuitId}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdRowId = rowTransactionRepository.createInCircuit({
|
|
||||||
circuitId,
|
|
||||||
name: "Test sub device",
|
|
||||||
displayName: "Beleuchtung WC",
|
|
||||||
phaseType: "single_phase",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.05,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
cosPhi: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Created test row id: ${createdRowId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
run().catch((error) => {
|
|
||||||
console.error("Failed to create test row:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { db } from "../src/db/client.js";
|
|
||||||
import { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js";
|
|
||||||
import { CircuitDeviceRowTransactionRepository } from "../src/db/repositories/circuit-device-row-transaction.repository.js";
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const rowId = process.argv[2];
|
|
||||||
if (!rowId) {
|
|
||||||
console.error("Usage: npm run dev:delete-circuit-row -- <rowId>");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowRepository = new CircuitDeviceRowRepository();
|
|
||||||
const rowTransactionRepository = new CircuitDeviceRowTransactionRepository(db);
|
|
||||||
const row = await rowRepository.findById(rowId);
|
|
||||||
if (!row) {
|
|
||||||
console.error(`Row not found: ${rowId}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
rowTransactionRepository.deleteFromCircuit(rowId, row.circuitId);
|
|
||||||
console.log(`Deleted row id: ${rowId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
run().catch((error) => {
|
|
||||||
console.error("Failed to delete row:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
|
||||||
import type {
|
|
||||||
CircuitDeviceRowTransactionStore,
|
|
||||||
CreateCircuitDeviceRowTransactionInput,
|
|
||||||
CreateCircuitWithDeviceRowsTransactionInput,
|
|
||||||
MoveCircuitDeviceRowsTransactionInput,
|
|
||||||
} 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 { toCircuitCreateValues } from "./circuit.persistence.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
createCircuitWithDeviceRows(input: CreateCircuitWithDeviceRowsTransactionInput) {
|
|
||||||
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());
|
|
||||||
this.database.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(
|
|
||||||
toCircuitDeviceRowCreateValues(rowIds[index], {
|
|
||||||
...row,
|
|
||||||
circuitId,
|
|
||||||
sortOrder: row.sortOrder ?? (index + 1) * 10,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { circuitId, rowIds };
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
moveRows(input: MoveCircuitDeviceRowsTransactionInput) {
|
|
||||||
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!;
|
|
||||||
|
|
||||||
this.database.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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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 CreateCircuitTransactionInput {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateCircuitWithDeviceRowsTransactionInput {
|
|
||||||
circuit: CreateCircuitTransactionInput;
|
|
||||||
deviceRows: Array<Omit<CreateCircuitDeviceRowTransactionInput, "circuitId">>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoveCircuitDeviceRowsTransactionInput {
|
|
||||||
rows: Array<{ id: string; expectedCircuitId: string }>;
|
|
||||||
targetCircuitId?: string;
|
|
||||||
createTargetCircuit?: {
|
|
||||||
circuitListId: string;
|
|
||||||
sectionId: string;
|
|
||||||
equipmentIdentifier: string;
|
|
||||||
displayName: string;
|
|
||||||
sortOrder: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoveCircuitDeviceRowsTransactionResult {
|
|
||||||
movedRowIds: string[];
|
|
||||||
targetCircuitId: string;
|
|
||||||
createdCircuitId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CircuitDeviceRowTransactionStore {
|
|
||||||
createInCircuit(input: CreateCircuitDeviceRowTransactionInput): string;
|
|
||||||
createCircuitWithDeviceRows(
|
|
||||||
input: CreateCircuitWithDeviceRowsTransactionInput
|
|
||||||
): { circuitId: string; rowIds: string[] };
|
|
||||||
deleteFromCircuit(rowId: string, expectedCircuitId: string): void;
|
|
||||||
moveRows(
|
|
||||||
input: MoveCircuitDeviceRowsTransactionInput
|
|
||||||
): MoveCircuitDeviceRowsTransactionResult;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
|
||||||
import type { CircuitDeviceRowTransactionStore } from "../ports/circuit-device-row-transaction.store.js";
|
|
||||||
import type { CircuitSectionTransactionStore } from "../ports/circuit-section-transaction.store.js";
|
import type { CircuitSectionTransactionStore } from "../ports/circuit-section-transaction.store.js";
|
||||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||||
import type {
|
import type {
|
||||||
MoveCircuitDeviceRowInput,
|
|
||||||
MoveCircuitDeviceRowsBulkInput,
|
|
||||||
ReorderSectionCircuitsInput,
|
ReorderSectionCircuitsInput,
|
||||||
UpdateSectionEquipmentIdentifiersInput,
|
UpdateSectionEquipmentIdentifiersInput,
|
||||||
} from "../../shared/validation/circuit.schemas.js";
|
} from "../../shared/validation/circuit.schemas.js";
|
||||||
@@ -14,46 +10,21 @@ import { CircuitNumberingService } from "./circuit-numbering.service.js";
|
|||||||
export class CircuitWriteService {
|
export class CircuitWriteService {
|
||||||
private readonly circuitRepository: CircuitRepository;
|
private readonly circuitRepository: CircuitRepository;
|
||||||
private readonly circuitSectionRepository: CircuitSectionRepository;
|
private readonly circuitSectionRepository: CircuitSectionRepository;
|
||||||
private readonly deviceRowRepository: CircuitDeviceRowRepository;
|
|
||||||
private readonly deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
|
||||||
private readonly circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
private readonly circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
||||||
private readonly numberingService: CircuitNumberingService;
|
private readonly numberingService: CircuitNumberingService;
|
||||||
|
|
||||||
constructor(deps?: {
|
constructor(deps?: {
|
||||||
circuitRepository?: CircuitRepository;
|
circuitRepository?: CircuitRepository;
|
||||||
circuitSectionRepository?: CircuitSectionRepository;
|
circuitSectionRepository?: CircuitSectionRepository;
|
||||||
deviceRowRepository?: CircuitDeviceRowRepository;
|
|
||||||
deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
|
||||||
circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
||||||
numberingService?: CircuitNumberingService;
|
numberingService?: CircuitNumberingService;
|
||||||
}) {
|
}) {
|
||||||
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
||||||
this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository();
|
this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository();
|
||||||
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
|
|
||||||
this.deviceRowTransactionStore = deps?.deviceRowTransactionStore;
|
|
||||||
this.circuitSectionTransactionStore = deps?.circuitSectionTransactionStore;
|
this.circuitSectionTransactionStore = deps?.circuitSectionTransactionStore;
|
||||||
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
|
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensures writes never connect a section to the wrong circuit list.
|
|
||||||
private async assertSectionInList(sectionId: string, circuitListId: string) {
|
|
||||||
const section = await this.circuitSectionRepository.findById(sectionId);
|
|
||||||
if (!section) {
|
|
||||||
throw new Error("Invalid section id.");
|
|
||||||
}
|
|
||||||
if (section.circuitListId !== circuitListId) {
|
|
||||||
throw new Error("Section does not belong to circuit list.");
|
|
||||||
}
|
|
||||||
return section;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDeviceRowTransactionStore() {
|
|
||||||
if (!this.deviceRowTransactionStore) {
|
|
||||||
throw new Error("Circuit device-row transactions are not configured.");
|
|
||||||
}
|
|
||||||
return this.deviceRowTransactionStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCircuitSectionTransactionStore() {
|
private getCircuitSectionTransactionStore() {
|
||||||
if (!this.circuitSectionTransactionStore) {
|
if (!this.circuitSectionTransactionStore) {
|
||||||
throw new Error("Circuit-section transactions are not configured.");
|
throw new Error("Circuit-section transactions are not configured.");
|
||||||
@@ -61,157 +32,6 @@ export class CircuitWriteService {
|
|||||||
return this.circuitSectionTransactionStore;
|
return this.circuitSectionTransactionStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCircuit(circuitId: string) {
|
|
||||||
const current = await this.circuitRepository.findById(circuitId);
|
|
||||||
if (!current) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
await this.circuitRepository.delete(circuitId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async moveDeviceRow(rowId: string, input: MoveCircuitDeviceRowInput) {
|
|
||||||
const row = await this.deviceRowRepository.findById(rowId);
|
|
||||||
if (!row) {
|
|
||||||
throw new Error("Invalid device row id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceCircuit = await this.circuitRepository.findById(row.circuitId);
|
|
||||||
if (!sourceCircuit) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetCircuit = input.targetCircuitId
|
|
||||||
? await this.circuitRepository.findById(input.targetCircuitId)
|
|
||||||
: null;
|
|
||||||
if (input.targetCircuitId && !targetCircuit) {
|
|
||||||
throw new Error("Invalid target circuit id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (!input.targetSectionId || !input.createNewCircuit) {
|
|
||||||
throw new Error("Invalid move target.");
|
|
||||||
}
|
|
||||||
const section = await this.assertSectionInList(input.targetSectionId, sourceCircuit.circuitListId);
|
|
||||||
const nextIdentifier = await this.numberingService.getNextIdentifier(section.id);
|
|
||||||
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
|
||||||
const nextSortOrder =
|
|
||||||
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
|
||||||
createTargetCircuit = {
|
|
||||||
circuitListId: sourceCircuit.circuitListId,
|
|
||||||
sectionId: section.id,
|
|
||||||
equipmentIdentifier: nextIdentifier,
|
|
||||||
displayName: "Neuer Stromkreis",
|
|
||||||
sortOrder: nextSortOrder,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetCircuit && targetCircuit.circuitListId !== sourceCircuit.circuitListId) {
|
|
||||||
throw new Error("Target circuit does not belong to same circuit list.");
|
|
||||||
}
|
|
||||||
if (targetCircuit?.id === sourceCircuit.id) {
|
|
||||||
return this.deviceRowRepository.findById(rowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getDeviceRowTransactionStore().moveRows({
|
|
||||||
rows: [{ id: row.id, expectedCircuitId: row.circuitId }],
|
|
||||||
targetCircuitId: targetCircuit?.id,
|
|
||||||
createTargetCircuit,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.deviceRowRepository.findById(rowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async moveDeviceRowsBulk(input: MoveCircuitDeviceRowsBulkInput) {
|
|
||||||
// Bulk move keeps input order and resolves all source circuits first so undo can
|
|
||||||
// restore per-source assignment deterministically.
|
|
||||||
const uniqueRowIds = [...new Set(input.rowIds)];
|
|
||||||
if (uniqueRowIds.length === 0) {
|
|
||||||
throw new Error("No device rows provided.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = [];
|
|
||||||
for (const rowId of uniqueRowIds) {
|
|
||||||
const row = await this.deviceRowRepository.findById(rowId);
|
|
||||||
if (!row) {
|
|
||||||
throw new Error("Invalid device row id.");
|
|
||||||
}
|
|
||||||
rows.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceCircuits = new Map<string, Awaited<ReturnType<CircuitRepository["findById"]>>>();
|
|
||||||
for (const row of rows) {
|
|
||||||
if (!sourceCircuits.has(row.circuitId)) {
|
|
||||||
const circuit = await this.circuitRepository.findById(row.circuitId);
|
|
||||||
if (!circuit) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
sourceCircuits.set(row.circuitId, circuit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const referenceSourceCircuit = sourceCircuits.get(rows[0].circuitId)!;
|
|
||||||
let targetCircuit = input.targetCircuitId
|
|
||||||
? await this.circuitRepository.findById(input.targetCircuitId)
|
|
||||||
: null;
|
|
||||||
if (input.targetCircuitId && !targetCircuit) {
|
|
||||||
throw new Error("Invalid target circuit id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (!input.targetSectionId || !input.createNewCircuit) {
|
|
||||||
throw new Error("Invalid move target.");
|
|
||||||
}
|
|
||||||
const section = await this.assertSectionInList(input.targetSectionId, referenceSourceCircuit.circuitListId);
|
|
||||||
const nextIdentifier = await this.numberingService.getNextIdentifier(section.id);
|
|
||||||
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
|
||||||
const nextSortOrder =
|
|
||||||
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
|
||||||
createTargetCircuit = {
|
|
||||||
circuitListId: referenceSourceCircuit.circuitListId,
|
|
||||||
sectionId: section.id,
|
|
||||||
equipmentIdentifier: nextIdentifier,
|
|
||||||
displayName: "Neuer Stromkreis",
|
|
||||||
sortOrder: nextSortOrder,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetCircuitListId = targetCircuit?.circuitListId ?? createTargetCircuit!.circuitListId;
|
|
||||||
for (const sourceCircuit of sourceCircuits.values()) {
|
|
||||||
if (sourceCircuit.circuitListId !== targetCircuitListId) {
|
|
||||||
throw new Error("All moved rows must belong to same circuit list as target.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getDeviceRowTransactionStore().moveRows({
|
|
||||||
rows: rows.map((row) => ({ id: row.id, expectedCircuitId: row.circuitId })),
|
|
||||||
targetCircuitId: targetCircuit?.id,
|
|
||||||
createTargetCircuit,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNextIdentifier(sectionId: string) {
|
async getNextIdentifier(sectionId: string) {
|
||||||
return this.numberingService.getNextIdentifier(sectionId);
|
return this.numberingService.getNextIdentifier(sectionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import {
|
|||||||
buildCircuitDeviceRowInsertSnapshot,
|
buildCircuitDeviceRowInsertSnapshot,
|
||||||
buildCircuitInsertSnapshot,
|
buildCircuitInsertSnapshot,
|
||||||
} from "../utils/circuit-structure-command";
|
} from "../utils/circuit-structure-command";
|
||||||
|
import {
|
||||||
|
buildCircuitDeviceRowMoveAssignments,
|
||||||
|
} from "../utils/circuit-device-row-move-command";
|
||||||
import type {
|
import type {
|
||||||
CellKey,
|
CellKey,
|
||||||
CellKind,
|
CellKind,
|
||||||
@@ -40,7 +43,6 @@ import {
|
|||||||
} from "../utils/circuit-grid-projection";
|
} from "../utils/circuit-grid-projection";
|
||||||
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
|
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
|
||||||
import {
|
import {
|
||||||
deleteCircuitById,
|
|
||||||
deleteCircuitCommand,
|
deleteCircuitCommand,
|
||||||
deleteCircuitDeviceRowCommand,
|
deleteCircuitDeviceRowCommand,
|
||||||
getCircuitTree,
|
getCircuitTree,
|
||||||
@@ -48,8 +50,8 @@ import {
|
|||||||
insertCircuitCommand,
|
insertCircuitCommand,
|
||||||
insertCircuitDeviceRowCommand,
|
insertCircuitDeviceRowCommand,
|
||||||
listProjectDevices,
|
listProjectDevices,
|
||||||
moveCircuitDeviceRowsBulk,
|
moveCircuitDeviceRowsCommand,
|
||||||
moveCircuitDeviceRowById,
|
moveCircuitDeviceRowsToNewCircuitCommand,
|
||||||
reorderSectionCircuits,
|
reorderSectionCircuits,
|
||||||
renumberCircuitSection,
|
renumberCircuitSection,
|
||||||
redoProjectCommand,
|
redoProjectCommand,
|
||||||
@@ -2230,8 +2232,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles device-row drag intent (single or bulk) and preserves enough source grouping
|
// Handles single and bulk device-row drag through persistent project history.
|
||||||
// for deterministic undo back to original circuits.
|
// Exact source and target positions make undo deterministic.
|
||||||
async function handleDeviceRowDropWithIntent(event: DragEvent<HTMLElement>, intent: DeviceRowMoveDropIntent) {
|
async function handleDeviceRowDropWithIntent(event: DragEvent<HTMLElement>, intent: DeviceRowMoveDropIntent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -2269,65 +2271,117 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device-row drag supports bulk selection, but target intent is always explicit:
|
const circuits = (data?.sections ?? []).flatMap(
|
||||||
// move into existing circuit or create new circuit in a target section.
|
(section) => section.circuits
|
||||||
|
);
|
||||||
|
const label =
|
||||||
|
rowIds.length > 1
|
||||||
|
? `${rowIds.length} Gerätezeilen verschieben`
|
||||||
|
: "Gerätezeile verschieben";
|
||||||
|
|
||||||
if (intent.kind === "move-to-circuit") {
|
if (intent.kind === "move-to-circuit") {
|
||||||
const groupedBySource = new Map<string, string[]>();
|
const targetCircuit = circuits.find(
|
||||||
for (const rowId of rowIds) {
|
(circuit) => circuit.id === intent.circuitId
|
||||||
const source = sourceByRowId.get(rowId)!;
|
);
|
||||||
if (!groupedBySource.has(source)) {
|
if (!targetCircuit) {
|
||||||
groupedBySource.set(source, []);
|
setError("Der Zielstromkreis ist ungültig.");
|
||||||
}
|
return;
|
||||||
groupedBySource.get(source)!.push(rowId);
|
}
|
||||||
|
const moves = buildCircuitDeviceRowMoveAssignments({
|
||||||
|
circuits,
|
||||||
|
rowIds,
|
||||||
|
targetCircuitId: targetCircuit.id,
|
||||||
|
targetDeviceRows: targetCircuit.deviceRows,
|
||||||
|
});
|
||||||
|
if (moves.length === 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
await runCommand({
|
await runCommand({
|
||||||
label: rowIds.length > 1 ? `${rowIds.length} Gerätezeilen verschieben` : "Gerätezeile verschieben",
|
label,
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
await moveCircuitDeviceRowsBulk({ rowIds, targetCircuitId: intent.circuitId });
|
const result = await moveCircuitDeviceRowsCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
moves,
|
||||||
|
label
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => null,
|
||||||
for (const [source, ids] of groupedBySource) {
|
persistent: {
|
||||||
await moveCircuitDeviceRowsBulk({ rowIds: ids, targetCircuitId: source });
|
undoIntent: () => {
|
||||||
}
|
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||||
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
return null;
|
||||||
return null;
|
},
|
||||||
|
redoIntent: () => {
|
||||||
|
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let createdCircuitId: string | null = null;
|
const targetSection = data?.sections.find(
|
||||||
const groupedBySource = new Map<string, string[]>();
|
(section) => section.id === intent.sectionId
|
||||||
for (const rowId of rowIds) {
|
);
|
||||||
const source = sourceByRowId.get(rowId)!;
|
if (!targetSection) {
|
||||||
if (!groupedBySource.has(source)) {
|
setError("Der Zielbereich ist ungültig.");
|
||||||
groupedBySource.set(source, []);
|
return;
|
||||||
}
|
|
||||||
groupedBySource.get(source)!.push(rowId);
|
|
||||||
}
|
}
|
||||||
|
const newCircuitLabel =
|
||||||
|
rowIds.length > 1
|
||||||
|
? `${rowIds.length} Gerätezeilen in neuen Stromkreis verschieben`
|
||||||
|
: "Gerätezeile in neuen Stromkreis verschieben";
|
||||||
await runCommand({
|
await runCommand({
|
||||||
label: rowIds.length > 1 ? `${rowIds.length} Gerätezeilen in neuen Stromkreis verschieben` : "Gerätezeile in neuen Stromkreis verschieben",
|
label: newCircuitLabel,
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
const moved = await moveCircuitDeviceRowsBulk({
|
const next = await getNextCircuitIdentifier(intent.sectionId);
|
||||||
rowIds,
|
const sortOrder =
|
||||||
targetSectionId: intent.sectionId,
|
targetSection.circuits.length > 0
|
||||||
createNewCircuit: true,
|
? Math.max(
|
||||||
|
...targetSection.circuits.map(
|
||||||
|
(circuit) => circuit.sortOrder
|
||||||
|
)
|
||||||
|
) + 10
|
||||||
|
: 10;
|
||||||
|
const targetCircuit = createCircuitSnapshot({
|
||||||
|
sectionId: intent.sectionId,
|
||||||
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
|
displayName: "Neuer Stromkreis",
|
||||||
|
sortOrder,
|
||||||
|
isReserve: true,
|
||||||
});
|
});
|
||||||
createdCircuitId = moved.createdCircuitId ?? null;
|
const moves = buildCircuitDeviceRowMoveAssignments({
|
||||||
|
circuits,
|
||||||
|
rowIds,
|
||||||
|
targetCircuitId: targetCircuit.id,
|
||||||
|
targetDeviceRows: [],
|
||||||
|
});
|
||||||
|
const result =
|
||||||
|
await moveCircuitDeviceRowsToNewCircuitCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
targetCircuit,
|
||||||
|
moves,
|
||||||
|
newCircuitLabel
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => null,
|
||||||
for (const [source, ids] of groupedBySource) {
|
persistent: {
|
||||||
await moveCircuitDeviceRowsBulk({ rowIds: ids, targetCircuitId: source });
|
undoIntent: () => {
|
||||||
}
|
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||||
if (createdCircuitId) {
|
return null;
|
||||||
await deleteCircuitById(createdCircuitId);
|
},
|
||||||
}
|
redoIntent: () => {
|
||||||
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||||
return null;
|
return null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-24
@@ -33,6 +33,9 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
CircuitSnapshot,
|
CircuitSnapshot,
|
||||||
} from "../../domain/models/circuit-structure-project-command.model";
|
} from "../../domain/models/circuit-structure-project-command.model";
|
||||||
|
import type {
|
||||||
|
CircuitDeviceRowMoveAssignment,
|
||||||
|
} from "../../domain/models/circuit-device-row-move-project-command.model";
|
||||||
|
|
||||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -212,12 +215,6 @@ export function deleteCircuitCommand(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteCircuitById(circuitId: string) {
|
|
||||||
return request<void>(`/api/circuits/${circuitId}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getNextCircuitIdentifier(sectionId: string) {
|
export function getNextCircuitIdentifier(sectionId: string) {
|
||||||
return request<{ sectionId: string; nextIdentifier: string }>(
|
return request<{ sectionId: string; nextIdentifier: string }>(
|
||||||
`/api/circuit-sections/${sectionId}/next-identifier`
|
`/api/circuit-sections/${sectionId}/next-identifier`
|
||||||
@@ -275,28 +272,44 @@ export function deleteCircuitDeviceRowCommand(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moveCircuitDeviceRowById(
|
export function moveCircuitDeviceRowsCommand(
|
||||||
rowId: string,
|
projectId: string,
|
||||||
input: { targetCircuitId?: string; targetSectionId?: string; createNewCircuit?: boolean }
|
expectedRevision: number,
|
||||||
|
moves: CircuitDeviceRowMoveAssignment[],
|
||||||
|
description: string
|
||||||
) {
|
) {
|
||||||
return request(`/api/circuit-device-rows/${rowId}/move`, {
|
return executeProjectCommand(
|
||||||
method: "PATCH",
|
projectId,
|
||||||
body: JSON.stringify(input),
|
expectedRevision,
|
||||||
});
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "circuit-device-row.move",
|
||||||
|
payload: { moves },
|
||||||
|
},
|
||||||
|
description
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moveCircuitDeviceRowsBulk(input: {
|
export function moveCircuitDeviceRowsToNewCircuitCommand(
|
||||||
rowIds: string[];
|
projectId: string,
|
||||||
targetCircuitId?: string;
|
expectedRevision: number,
|
||||||
targetSectionId?: string;
|
targetCircuit: CircuitSnapshot,
|
||||||
createNewCircuit?: boolean;
|
moves: CircuitDeviceRowMoveAssignment[],
|
||||||
}) {
|
description: string
|
||||||
return request<{ movedRowIds: string[]; targetCircuitId: string; createdCircuitId?: string }>(
|
) {
|
||||||
"/api/circuit-device-rows/move-bulk",
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
schemaVersion: 1,
|
||||||
body: JSON.stringify(input),
|
type: "circuit-device-row.move-with-new-circuit",
|
||||||
}
|
payload: {
|
||||||
|
targetCircuitAction: "create",
|
||||||
|
targetCircuit,
|
||||||
|
moves,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type {
|
||||||
|
CircuitTreeCircuitDto,
|
||||||
|
CircuitTreeDeviceRowDto,
|
||||||
|
} from "../types.js";
|
||||||
|
import type {
|
||||||
|
CircuitDeviceRowMoveAssignment,
|
||||||
|
} from "../../domain/models/circuit-device-row-move-project-command.model.js";
|
||||||
|
|
||||||
|
export function buildCircuitDeviceRowMoveAssignments(input: {
|
||||||
|
circuits: CircuitTreeCircuitDto[];
|
||||||
|
rowIds: string[];
|
||||||
|
targetCircuitId: string;
|
||||||
|
targetDeviceRows: CircuitTreeDeviceRowDto[];
|
||||||
|
}): CircuitDeviceRowMoveAssignment[] {
|
||||||
|
const { circuits, rowIds, targetCircuitId, targetDeviceRows } =
|
||||||
|
input;
|
||||||
|
if (new Set(rowIds).size !== rowIds.length) {
|
||||||
|
throw new Error("Gerätezeilen dürfen nicht mehrfach ausgewählt sein.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceByRowId = new Map<
|
||||||
|
string,
|
||||||
|
{ circuitId: string; sortOrder: number }
|
||||||
|
>();
|
||||||
|
for (const circuit of circuits) {
|
||||||
|
for (const row of circuit.deviceRows) {
|
||||||
|
sourceByRowId.set(row.id, {
|
||||||
|
circuitId: circuit.id,
|
||||||
|
sortOrder: row.sortOrder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const movedRowIds = new Set(rowIds);
|
||||||
|
const retainedTargetRows = targetDeviceRows.filter(
|
||||||
|
(row) => !movedRowIds.has(row.id)
|
||||||
|
);
|
||||||
|
const lastTargetSortOrder = retainedTargetRows.reduce(
|
||||||
|
(highest, row) => Math.max(highest, row.sortOrder),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return rowIds.flatMap((rowId, index) => {
|
||||||
|
const source = sourceByRowId.get(rowId);
|
||||||
|
if (!source) {
|
||||||
|
throw new Error("Eine ausgewählte Gerätezeile wurde nicht gefunden.");
|
||||||
|
}
|
||||||
|
const targetSortOrder =
|
||||||
|
lastTargetSortOrder + (index + 1) * 10;
|
||||||
|
if (
|
||||||
|
source.circuitId === targetCircuitId &&
|
||||||
|
source.sortOrder === targetSortOrder
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rowId,
|
||||||
|
expectedCircuitId: source.circuitId,
|
||||||
|
expectedSortOrder: source.sortOrder,
|
||||||
|
targetCircuitId,
|
||||||
|
targetSortOrder,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||||
import { db } from "../../db/client.js";
|
import { db } from "../../db/client.js";
|
||||||
import { CircuitDeviceRowTransactionRepository } from "../../db/repositories/circuit-device-row-transaction.repository.js";
|
|
||||||
import { CircuitSectionTransactionRepository } from "../../db/repositories/circuit-section-transaction.repository.js";
|
import { CircuitSectionTransactionRepository } from "../../db/repositories/circuit-section-transaction.repository.js";
|
||||||
|
|
||||||
export const circuitWriteService = new CircuitWriteService({
|
export const circuitWriteService = new CircuitWriteService({
|
||||||
deviceRowTransactionStore: new CircuitDeviceRowTransactionRepository(db),
|
|
||||||
circuitSectionTransactionStore: new CircuitSectionTransactionRepository(db),
|
circuitSectionTransactionStore: new CircuitSectionTransactionRepository(db),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { Request, Response } from "express";
|
|
||||||
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
|
||||||
import {
|
|
||||||
moveCircuitDeviceRowsBulkSchema,
|
|
||||||
moveCircuitDeviceRowSchema,
|
|
||||||
} from "../../shared/validation/circuit.schemas.js";
|
|
||||||
|
|
||||||
export async function moveCircuitDeviceRow(req: Request, res: Response) {
|
|
||||||
const { rowId } = req.params;
|
|
||||||
if (typeof rowId !== "string") {
|
|
||||||
return res.status(400).json({ error: "Invalid rowId" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = moveCircuitDeviceRowSchema.safeParse(req.body);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const moved = await circuitWriteService.moveDeviceRow(rowId, parsed.data);
|
|
||||||
return res.json(moved);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to move device row." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function moveCircuitDeviceRowsBulk(req: Request, res: Response) {
|
|
||||||
const parsed = moveCircuitDeviceRowsBulkSchema.safeParse(req.body);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const moved = await circuitWriteService.moveDeviceRowsBulk(parsed.data);
|
|
||||||
return res.json(moved);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to move device rows." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,6 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
||||||
|
|
||||||
export async function deleteCircuit(req: Request, res: Response) {
|
|
||||||
const { circuitId } = req.params;
|
|
||||||
if (typeof circuitId !== "string") {
|
|
||||||
return res.status(400).json({ error: "Invalid circuitId" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await circuitWriteService.deleteCircuit(circuitId);
|
|
||||||
return res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to delete circuit." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNextCircuitIdentifier(req: Request, res: Response) {
|
export async function getNextCircuitIdentifier(req: Request, res: Response) {
|
||||||
const { sectionId } = req.params;
|
const { sectionId } = req.params;
|
||||||
if (typeof sectionId !== "string") {
|
if (typeof sectionId !== "string") {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { circuitDeviceRowRouter } from "./routes/circuit-device-row.routes.js";
|
|
||||||
import { circuitRouter } from "./routes/circuit.routes.js";
|
import { circuitRouter } from "./routes/circuit.routes.js";
|
||||||
import { circuitSectionRouter } from "./routes/circuit-section.routes.js";
|
import { circuitSectionRouter } from "./routes/circuit-section.routes.js";
|
||||||
import { globalDeviceRouter } from "./routes/global-device.routes.js";
|
import { globalDeviceRouter } from "./routes/global-device.routes.js";
|
||||||
@@ -18,7 +17,6 @@ app.get("/health", (_req, res) => {
|
|||||||
|
|
||||||
app.use("/api/projects", projectRouter);
|
app.use("/api/projects", projectRouter);
|
||||||
app.use("/api", circuitRouter);
|
app.use("/api", circuitRouter);
|
||||||
app.use("/api", circuitDeviceRowRouter);
|
|
||||||
app.use("/api", circuitSectionRouter);
|
app.use("/api", circuitSectionRouter);
|
||||||
app.use("/api/global-devices", globalDeviceRouter);
|
app.use("/api/global-devices", globalDeviceRouter);
|
||||||
app.use("/api/project-devices", projectDeviceRouter);
|
app.use("/api/project-devices", projectDeviceRouter);
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import {
|
|
||||||
moveCircuitDeviceRowsBulk,
|
|
||||||
moveCircuitDeviceRow,
|
|
||||||
} from "../controllers/circuit-device-row.controller.js";
|
|
||||||
|
|
||||||
export const circuitDeviceRowRouter = Router();
|
|
||||||
|
|
||||||
circuitDeviceRowRouter.patch("/circuit-device-rows/move-bulk", moveCircuitDeviceRowsBulk);
|
|
||||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId/move", moveCircuitDeviceRow);
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import {
|
import {
|
||||||
deleteCircuit,
|
|
||||||
getNextCircuitIdentifier,
|
getNextCircuitIdentifier,
|
||||||
} from "../controllers/circuit.controller.js";
|
} from "../controllers/circuit.controller.js";
|
||||||
|
|
||||||
export const circuitRouter = Router();
|
export const circuitRouter = Router();
|
||||||
|
|
||||||
circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
|
|
||||||
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const moveCircuitDeviceRowSchema = z
|
|
||||||
.object({
|
|
||||||
targetCircuitId: z.string().min(1).optional(),
|
|
||||||
targetSectionId: z.string().min(1).optional(),
|
|
||||||
createNewCircuit: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(value) =>
|
|
||||||
Boolean(value.targetCircuitId) ||
|
|
||||||
(Boolean(value.targetSectionId) && value.createNewCircuit === true),
|
|
||||||
{ message: "Either targetCircuitId or targetSectionId+createNewCircuit=true is required." }
|
|
||||||
);
|
|
||||||
|
|
||||||
export const moveCircuitDeviceRowsBulkSchema = z
|
|
||||||
.object({
|
|
||||||
rowIds: z.array(z.string().min(1)).min(1),
|
|
||||||
targetCircuitId: z.string().min(1).optional(),
|
|
||||||
targetSectionId: z.string().min(1).optional(),
|
|
||||||
createNewCircuit: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(value) =>
|
|
||||||
Boolean(value.targetCircuitId) ||
|
|
||||||
(Boolean(value.targetSectionId) && value.createNewCircuit === true),
|
|
||||||
{ message: "Either targetCircuitId or targetSectionId+createNewCircuit=true is required." }
|
|
||||||
);
|
|
||||||
|
|
||||||
export const reorderSectionCircuitsSchema = z.object({
|
export const reorderSectionCircuitsSchema = z.object({
|
||||||
orderedCircuitIds: z.array(z.string().min(1)).min(1),
|
orderedCircuitIds: z.array(z.string().min(1)).min(1),
|
||||||
});
|
});
|
||||||
@@ -42,7 +15,5 @@ export const updateSectionEquipmentIdentifiersSchema = z.object({
|
|||||||
.min(1),
|
.min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
|
||||||
export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;
|
|
||||||
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
||||||
export type UpdateSectionEquipmentIdentifiersInput = z.infer<typeof updateSectionEquipmentIdentifiersSchema>;
|
export type UpdateSectionEquipmentIdentifiersInput = z.infer<typeof updateSectionEquipmentIdentifiersSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
buildCircuitDeviceRowMoveAssignments,
|
||||||
|
} from "../src/frontend/utils/circuit-device-row-move-command.js";
|
||||||
|
import type {
|
||||||
|
CircuitTreeCircuitDto,
|
||||||
|
CircuitTreeDeviceRowDto,
|
||||||
|
} from "../src/frontend/types.js";
|
||||||
|
|
||||||
|
function row(
|
||||||
|
id: string,
|
||||||
|
sortOrder: number
|
||||||
|
): CircuitTreeDeviceRowDto {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
sortOrder,
|
||||||
|
name: id,
|
||||||
|
displayName: id,
|
||||||
|
quantity: 1,
|
||||||
|
powerPerUnit: 0.1,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
rowTotalPower: 0.1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function circuit(
|
||||||
|
id: string,
|
||||||
|
deviceRows: CircuitTreeDeviceRowDto[]
|
||||||
|
): CircuitTreeCircuitDto {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "section-1",
|
||||||
|
equipmentIdentifier: id,
|
||||||
|
sortOrder: 10,
|
||||||
|
isReserve: deviceRows.length === 0,
|
||||||
|
circuitTotalPower: 0,
|
||||||
|
deviceRows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("circuit device-row move command builder", () => {
|
||||||
|
it("records exact sources and appends rows in selection order", () => {
|
||||||
|
const targetRows = [row("target-row", 20)];
|
||||||
|
const circuits = [
|
||||||
|
circuit("source-1", [row("row-1", 30)]),
|
||||||
|
circuit("source-2", [row("row-2", 10)]),
|
||||||
|
circuit("target", targetRows),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
buildCircuitDeviceRowMoveAssignments({
|
||||||
|
circuits,
|
||||||
|
rowIds: ["row-2", "row-1"],
|
||||||
|
targetCircuitId: "target",
|
||||||
|
targetDeviceRows: targetRows,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
rowId: "row-2",
|
||||||
|
expectedCircuitId: "source-2",
|
||||||
|
expectedSortOrder: 10,
|
||||||
|
targetCircuitId: "target",
|
||||||
|
targetSortOrder: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rowId: "row-1",
|
||||||
|
expectedCircuitId: "source-1",
|
||||||
|
expectedSortOrder: 30,
|
||||||
|
targetCircuitId: "target",
|
||||||
|
targetSortOrder: 40,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts a generated target circuit at sort order ten", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
buildCircuitDeviceRowMoveAssignments({
|
||||||
|
circuits: [circuit("source", [row("row-1", 40)])],
|
||||||
|
rowIds: ["row-1"],
|
||||||
|
targetCircuitId: "generated-target",
|
||||||
|
targetDeviceRows: [],
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
rowId: "row-1",
|
||||||
|
expectedCircuitId: "source",
|
||||||
|
expectedSortOrder: 40,
|
||||||
|
targetCircuitId: "generated-target",
|
||||||
|
targetSortOrder: 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits unchanged assignments during a same-circuit reorder", () => {
|
||||||
|
const rows = [row("row-1", 10), row("row-2", 20)];
|
||||||
|
assert.deepEqual(
|
||||||
|
buildCircuitDeviceRowMoveAssignments({
|
||||||
|
circuits: [circuit("target", rows)],
|
||||||
|
rowIds: ["row-1", "row-2"],
|
||||||
|
targetCircuitId: "target",
|
||||||
|
targetDeviceRows: rows,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects duplicate or missing selected rows", () => {
|
||||||
|
const circuits = [circuit("source", [row("row-1", 10)])];
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
buildCircuitDeviceRowMoveAssignments({
|
||||||
|
circuits,
|
||||||
|
rowIds: ["row-1", "row-1"],
|
||||||
|
targetCircuitId: "target",
|
||||||
|
targetDeviceRows: [],
|
||||||
|
}),
|
||||||
|
/mehrfach/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
buildCircuitDeviceRowMoveAssignments({
|
||||||
|
circuits,
|
||||||
|
rowIds: ["missing"],
|
||||||
|
targetCircuitId: "target",
|
||||||
|
targetDeviceRows: [],
|
||||||
|
}),
|
||||||
|
/nicht gefunden/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,529 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { describe, it } from "node:test";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
||||||
import {
|
|
||||||
createDatabaseContext,
|
|
||||||
type DatabaseContext,
|
|
||||||
} from "../src/db/database-context.js";
|
|
||||||
import { CircuitDeviceRowTransactionRepository } from "../src/db/repositories/circuit-device-row-transaction.repository.js";
|
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
|
||||||
import { projects } from "../src/db/schema/projects.js";
|
|
||||||
|
|
||||||
function createTestDatabase(): DatabaseContext {
|
|
||||||
const context = createDatabaseContext(":memory:");
|
|
||||||
migrate(context.db, {
|
|
||||||
migrationsFolder: path.resolve("src", "db", "migrations"),
|
|
||||||
});
|
|
||||||
context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run();
|
|
||||||
const board = new DistributionBoardRepository(
|
|
||||||
context.db
|
|
||||||
).createWithCircuitListAndDefaultSections(
|
|
||||||
"project-1",
|
|
||||||
"UV-01"
|
|
||||||
);
|
|
||||||
const [section] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuitSections)
|
|
||||||
.where(eq(circuitSections.circuitListId, board.id))
|
|
||||||
.limit(1)
|
|
||||||
.all();
|
|
||||||
context.db
|
|
||||||
.insert(circuits)
|
|
||||||
.values({
|
|
||||||
id: "circuit-1",
|
|
||||||
circuitListId: board.id,
|
|
||||||
sectionId: section.id,
|
|
||||||
equipmentIdentifier: "-1F1",
|
|
||||||
displayName: "Reserve",
|
|
||||||
sortOrder: 10,
|
|
||||||
isReserve: 1,
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertCircuit(
|
|
||||||
context: DatabaseContext,
|
|
||||||
input: {
|
|
||||||
id: string;
|
|
||||||
equipmentIdentifier: string;
|
|
||||||
sortOrder: number;
|
|
||||||
isReserve?: number;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
|
|
||||||
context.db
|
|
||||||
.insert(circuits)
|
|
||||||
.values({
|
|
||||||
id: input.id,
|
|
||||||
circuitListId: seedCircuit.circuitListId,
|
|
||||||
sectionId: seedCircuit.sectionId,
|
|
||||||
equipmentIdentifier: input.equipmentIdentifier,
|
|
||||||
displayName: input.equipmentIdentifier,
|
|
||||||
sortOrder: input.sortOrder,
|
|
||||||
isReserve: input.isReserve ?? 1,
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertDeviceRow(
|
|
||||||
context: DatabaseContext,
|
|
||||||
input: {
|
|
||||||
id?: string;
|
|
||||||
circuitId?: string;
|
|
||||||
sortOrder?: number;
|
|
||||||
displayName?: string;
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
const rowId = input.id ?? "row-1";
|
|
||||||
const circuitId = input.circuitId ?? "circuit-1";
|
|
||||||
context.db
|
|
||||||
.insert(circuitDeviceRows)
|
|
||||||
.values({
|
|
||||||
id: rowId,
|
|
||||||
circuitId,
|
|
||||||
sortOrder: input.sortOrder ?? 10,
|
|
||||||
name: "Leuchte",
|
|
||||||
displayName: input.displayName ?? "Leuchte",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.1,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
context.db
|
|
||||||
.update(circuits)
|
|
||||||
.set({ isReserve: 0 })
|
|
||||||
.where(eq(circuits.id, circuitId))
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("circuit device-row transaction repository", () => {
|
|
||||||
it("commits a new device row and clears the circuit reserve status together", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
const rowId = repository.createInCircuit({
|
|
||||||
circuitId: "circuit-1",
|
|
||||||
name: "Steckdose",
|
|
||||||
displayName: "Steckdose",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.2,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [row] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuitDeviceRows)
|
|
||||||
.where(eq(circuitDeviceRows.id, rowId))
|
|
||||||
.all();
|
|
||||||
const [circuit] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, "circuit-1"))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
assert.equal(row.circuitId, "circuit-1");
|
|
||||||
assert.equal(row.sortOrder, 10);
|
|
||||||
assert.equal(circuit.isReserve, 0);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rolls back the row insert when clearing the reserve status fails", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
context.sqlite.exec(`
|
|
||||||
CREATE TRIGGER fail_reserve_clear
|
|
||||||
BEFORE UPDATE OF is_reserve ON circuits
|
|
||||||
WHEN NEW.is_reserve = 0
|
|
||||||
BEGIN
|
|
||||||
SELECT RAISE(ABORT, 'forced reserve clear failure');
|
|
||||||
END;
|
|
||||||
`);
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() =>
|
|
||||||
repository.createInCircuit({
|
|
||||||
circuitId: "circuit-1",
|
|
||||||
name: "Steckdose",
|
|
||||||
displayName: "Steckdose",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.2,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
}),
|
|
||||||
/forced reserve clear failure/
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
|
|
||||||
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 1);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("commits a new circuit and all initial device rows together", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
const created = repository.createCircuitWithDeviceRows({
|
|
||||||
circuit: {
|
|
||||||
circuitListId: seedCircuit.circuitListId,
|
|
||||||
sectionId: seedCircuit.sectionId,
|
|
||||||
equipmentIdentifier: "-1F2",
|
|
||||||
displayName: "Beleuchtung",
|
|
||||||
sortOrder: 20,
|
|
||||||
},
|
|
||||||
deviceRows: [
|
|
||||||
{
|
|
||||||
name: "Leuchte",
|
|
||||||
displayName: "Leuchte 1",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.1,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
sortOrder: 15,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Leuchte",
|
|
||||||
displayName: "Leuchte 2",
|
|
||||||
quantity: 2,
|
|
||||||
powerPerUnit: 0.1,
|
|
||||||
simultaneityFactor: 0.8,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [createdCircuit] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, created.circuitId))
|
|
||||||
.all();
|
|
||||||
const createdRows = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuitDeviceRows)
|
|
||||||
.where(eq(circuitDeviceRows.circuitId, created.circuitId))
|
|
||||||
.all()
|
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
|
||||||
|
|
||||||
assert.equal(createdCircuit.equipmentIdentifier, "-1F2");
|
|
||||||
assert.equal(createdCircuit.isReserve, 0);
|
|
||||||
assert.deepEqual(
|
|
||||||
createdRows.map((row) => [row.id, row.displayName, row.sortOrder]),
|
|
||||||
[
|
|
||||||
[created.rowIds[0], "Leuchte 1", 15],
|
|
||||||
[created.rowIds[1], "Leuchte 2", 20],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rolls back the circuit and earlier rows when an initial row insert fails", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
|
|
||||||
context.sqlite.exec(`
|
|
||||||
CREATE TRIGGER fail_initial_device_row
|
|
||||||
BEFORE INSERT ON circuit_device_rows
|
|
||||||
WHEN NEW.name = 'Fail'
|
|
||||||
BEGIN
|
|
||||||
SELECT RAISE(ABORT, 'forced initial row failure');
|
|
||||||
END;
|
|
||||||
`);
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() =>
|
|
||||||
repository.createCircuitWithDeviceRows({
|
|
||||||
circuit: {
|
|
||||||
circuitListId: seedCircuit.circuitListId,
|
|
||||||
sectionId: seedCircuit.sectionId,
|
|
||||||
equipmentIdentifier: "-1F2",
|
|
||||||
displayName: "Rollback",
|
|
||||||
sortOrder: 20,
|
|
||||||
},
|
|
||||||
deviceRows: [
|
|
||||||
{
|
|
||||||
name: "First",
|
|
||||||
displayName: "Erste Zeile",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.1,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Fail",
|
|
||||||
displayName: "Fehlerhafte Zeile",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.1,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
/forced initial row failure/
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(context.db.select().from(circuits).all().length, 1);
|
|
||||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("commits the last-row deletion and activates the circuit reserve status together", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
insertDeviceRow(context);
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
|
|
||||||
repository.deleteFromCircuit("row-1", "circuit-1");
|
|
||||||
|
|
||||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
|
|
||||||
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 1);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rolls back the row deletion when activating the reserve status fails", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
insertDeviceRow(context);
|
|
||||||
context.sqlite.exec(`
|
|
||||||
CREATE TRIGGER fail_reserve_activation
|
|
||||||
BEFORE UPDATE OF is_reserve ON circuits
|
|
||||||
WHEN NEW.is_reserve = 1
|
|
||||||
BEGIN
|
|
||||||
SELECT RAISE(ABORT, 'forced reserve activation failure');
|
|
||||||
END;
|
|
||||||
`);
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() => repository.deleteFromCircuit("row-1", "circuit-1"),
|
|
||||||
/forced reserve activation failure/
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 1);
|
|
||||||
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 0);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("moves rows to an existing circuit and updates both reserve states together", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
insertCircuit(context, {
|
|
||||||
id: "circuit-2",
|
|
||||||
equipmentIdentifier: "-1F2",
|
|
||||||
sortOrder: 20,
|
|
||||||
});
|
|
||||||
insertCircuit(context, {
|
|
||||||
id: "circuit-3",
|
|
||||||
equipmentIdentifier: "-1F3",
|
|
||||||
sortOrder: 30,
|
|
||||||
});
|
|
||||||
insertDeviceRow(context);
|
|
||||||
insertDeviceRow(context, {
|
|
||||||
id: "row-2",
|
|
||||||
circuitId: "circuit-2",
|
|
||||||
sortOrder: 10,
|
|
||||||
displayName: "Zweite Quellzeile",
|
|
||||||
});
|
|
||||||
insertDeviceRow(context, {
|
|
||||||
id: "target-row",
|
|
||||||
circuitId: "circuit-3",
|
|
||||||
sortOrder: 10,
|
|
||||||
displayName: "Bestehende Zielzeile",
|
|
||||||
});
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
|
|
||||||
const result = repository.moveRows({
|
|
||||||
rows: [
|
|
||||||
{ id: "row-1", expectedCircuitId: "circuit-1" },
|
|
||||||
{ id: "row-2", expectedCircuitId: "circuit-2" },
|
|
||||||
],
|
|
||||||
targetCircuitId: "circuit-3",
|
|
||||||
});
|
|
||||||
|
|
||||||
const movedRows = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuitDeviceRows)
|
|
||||||
.all()
|
|
||||||
.filter((row) => row.id === "row-1" || row.id === "row-2")
|
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
|
||||||
const persistedCircuits = context.db.select().from(circuits).all();
|
|
||||||
const targetCircuit = persistedCircuits.find(
|
|
||||||
(circuit) => circuit.id === "circuit-3"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.deepEqual(result, {
|
|
||||||
movedRowIds: ["row-1", "row-2"],
|
|
||||||
targetCircuitId: "circuit-3",
|
|
||||||
createdCircuitId: undefined,
|
|
||||||
});
|
|
||||||
assert.deepEqual(
|
|
||||||
movedRows.map((row) => [row.id, row.circuitId, row.sortOrder]),
|
|
||||||
[
|
|
||||||
["row-1", "circuit-3", 20],
|
|
||||||
["row-2", "circuit-3", 30],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert.equal(targetCircuit?.isReserve, 0);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rolls back an existing-target move when a reserve update fails", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
insertCircuit(context, {
|
|
||||||
id: "circuit-2",
|
|
||||||
equipmentIdentifier: "-1F2",
|
|
||||||
sortOrder: 20,
|
|
||||||
});
|
|
||||||
insertDeviceRow(context);
|
|
||||||
context.sqlite.exec(`
|
|
||||||
CREATE TRIGGER fail_source_reserve_update
|
|
||||||
BEFORE UPDATE OF is_reserve ON circuits
|
|
||||||
WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1
|
|
||||||
BEGIN
|
|
||||||
SELECT RAISE(ABORT, 'forced source reserve failure');
|
|
||||||
END;
|
|
||||||
`);
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() =>
|
|
||||||
repository.moveRows({
|
|
||||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
|
||||||
targetCircuitId: "circuit-2",
|
|
||||||
}),
|
|
||||||
/forced source reserve failure/
|
|
||||||
);
|
|
||||||
|
|
||||||
const [row] = context.db.select().from(circuitDeviceRows).all();
|
|
||||||
const persistedCircuits = context.db.select().from(circuits).all();
|
|
||||||
assert.equal(row.circuitId, "circuit-1");
|
|
||||||
assert.equal(
|
|
||||||
persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a new target circuit and moves rows into it atomically", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
insertDeviceRow(context);
|
|
||||||
const [sourceCircuit] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, "circuit-1"))
|
|
||||||
.all();
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
|
|
||||||
const result = repository.moveRows({
|
|
||||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
|
||||||
createTargetCircuit: {
|
|
||||||
circuitListId: sourceCircuit.circuitListId,
|
|
||||||
sectionId: sourceCircuit.sectionId,
|
|
||||||
equipmentIdentifier: "-1F2",
|
|
||||||
displayName: "Neues Ziel",
|
|
||||||
sortOrder: 20,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [movedRow] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuitDeviceRows)
|
|
||||||
.where(eq(circuitDeviceRows.id, "row-1"))
|
|
||||||
.all();
|
|
||||||
const [createdCircuit] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, result.createdCircuitId!))
|
|
||||||
.all();
|
|
||||||
const [updatedSource] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, "circuit-1"))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
assert.equal(result.targetCircuitId, result.createdCircuitId);
|
|
||||||
assert.equal(createdCircuit.equipmentIdentifier, "-1F2");
|
|
||||||
assert.equal(createdCircuit.isReserve, 0);
|
|
||||||
assert.equal(movedRow.circuitId, createdCircuit.id);
|
|
||||||
assert.equal(updatedSource.isReserve, 1);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rolls back a newly created target circuit when the move fails", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
insertDeviceRow(context);
|
|
||||||
const [sourceCircuit] = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, "circuit-1"))
|
|
||||||
.all();
|
|
||||||
context.sqlite.exec(`
|
|
||||||
CREATE TRIGGER fail_new_target_source_reserve
|
|
||||||
BEFORE UPDATE OF is_reserve ON circuits
|
|
||||||
WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1
|
|
||||||
BEGIN
|
|
||||||
SELECT RAISE(ABORT, 'forced new-target move failure');
|
|
||||||
END;
|
|
||||||
`);
|
|
||||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() =>
|
|
||||||
repository.moveRows({
|
|
||||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
|
||||||
createTargetCircuit: {
|
|
||||||
circuitListId: sourceCircuit.circuitListId,
|
|
||||||
sectionId: sourceCircuit.sectionId,
|
|
||||||
equipmentIdentifier: "-1F2",
|
|
||||||
displayName: "Rollback-Ziel",
|
|
||||||
sortOrder: 20,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
/forced new-target move failure/
|
|
||||||
);
|
|
||||||
|
|
||||||
const [row] = context.db.select().from(circuitDeviceRows).all();
|
|
||||||
const persistedCircuits = context.db.select().from(circuits).all();
|
|
||||||
assert.equal(row.circuitId, "circuit-1");
|
|
||||||
assert.equal(persistedCircuits.length, 1);
|
|
||||||
assert.equal(persistedCircuits[0].isReserve, 0);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -23,7 +23,10 @@ describe("circuit write service rules", () => {
|
|||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
circuitSectionTransactionStore: {
|
circuitSectionTransactionStore: {
|
||||||
updateEquipmentIdentifiers(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) {
|
updateEquipmentIdentifiers(
|
||||||
|
_listId: string,
|
||||||
|
updates: Array<{ id: string; equipmentIdentifier: string }>
|
||||||
|
) {
|
||||||
safeUpdatePayload = updates;
|
safeUpdatePayload = updates;
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
@@ -58,7 +61,10 @@ describe("circuit write service rules", () => {
|
|||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
circuitSectionTransactionStore: {
|
circuitSectionTransactionStore: {
|
||||||
updateEquipmentIdentifiers(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) {
|
updateEquipmentIdentifiers(
|
||||||
|
_listId: string,
|
||||||
|
updates: Array<{ id: string; equipmentIdentifier: string }>
|
||||||
|
) {
|
||||||
safeUpdatePayload = updates;
|
safeUpdatePayload = updates;
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
@@ -72,7 +78,7 @@ describe("circuit write service rules", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renumber handles gaps and keeps device rows untouched by identifier-only update path", async () => {
|
it("renumber handles gaps without touching device rows", async () => {
|
||||||
let safeCalled = 0;
|
let safeCalled = 0;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
circuitSectionRepository: {
|
circuitSectionRepository: {
|
||||||
@@ -97,292 +103,12 @@ describe("circuit write service rules", () => {
|
|||||||
safeCalled += 1;
|
safeCalled += 1;
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
deviceRowRepository: {
|
|
||||||
async update() {
|
|
||||||
throw new Error("device rows must not be touched");
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.renumberSection("s1");
|
await service.renumberSection("s1");
|
||||||
assert.equal(safeCalled, 1);
|
assert.equal(safeCalled, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moving a device row to another circuit preserves row and toggles reserve flags", async () => {
|
|
||||||
let transactionalMove:
|
|
||||||
| {
|
|
||||||
rows: Array<{ id: string; expectedCircuitId: string }>;
|
|
||||||
targetCircuitId?: string;
|
|
||||||
createTargetCircuit?: unknown;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
deviceRowRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "r1", circuitId: "c1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowTransactionStore: {
|
|
||||||
moveRows(input: typeof transactionalMove) {
|
|
||||||
transactionalMove = input;
|
|
||||||
return {
|
|
||||||
movedRowIds: input!.rows.map((row) => row.id),
|
|
||||||
targetCircuitId: input!.targetCircuitId!,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async findById(circuitId: string) {
|
|
||||||
if (circuitId === "c1") {
|
|
||||||
return {
|
|
||||||
id: "c1",
|
|
||||||
sectionId: "s1",
|
|
||||||
circuitListId: "l1",
|
|
||||||
equipmentIdentifier: "-1F1",
|
|
||||||
sortOrder: 10,
|
|
||||||
isReserve: 0,
|
|
||||||
} as never;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: "c2",
|
|
||||||
sectionId: "s1",
|
|
||||||
circuitListId: "l1",
|
|
||||||
equipmentIdentifier: "-1F2",
|
|
||||||
sortOrder: 20,
|
|
||||||
isReserve: 1,
|
|
||||||
} as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.moveDeviceRow("r1", { targetCircuitId: "c2" });
|
|
||||||
assert.deepEqual(transactionalMove, {
|
|
||||||
rows: [{ id: "r1", expectedCircuitId: "c1" }],
|
|
||||||
targetCircuitId: "c2",
|
|
||||||
createTargetCircuit: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("moving a device row to placeholder creates a new circuit in target section", async () => {
|
|
||||||
let preparedTarget:
|
|
||||||
| {
|
|
||||||
circuitListId: string;
|
|
||||||
sectionId: string;
|
|
||||||
equipmentIdentifier: string;
|
|
||||||
displayName: string;
|
|
||||||
sortOrder: number;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
deviceRowRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "r1", circuitId: "c1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowTransactionStore: {
|
|
||||||
moveRows(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) {
|
|
||||||
preparedTarget = input.createTargetCircuit;
|
|
||||||
return {
|
|
||||||
movedRowIds: input.rows.map((row) => row.id),
|
|
||||||
targetCircuitId: "c-new",
|
|
||||||
createdCircuitId: "c-new",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async findById(circuitId: string) {
|
|
||||||
if (circuitId === "c1") {
|
|
||||||
return {
|
|
||||||
id: "c1",
|
|
||||||
sectionId: "s1",
|
|
||||||
circuitListId: "l1",
|
|
||||||
equipmentIdentifier: "-1F1",
|
|
||||||
sortOrder: 10,
|
|
||||||
isReserve: 0,
|
|
||||||
} as never;
|
|
||||||
}
|
|
||||||
return null as never;
|
|
||||||
},
|
|
||||||
async listBySection() {
|
|
||||||
return [{ sortOrder: 40 }] as never[];
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitSectionRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "s2", circuitListId: "l1", prefix: "-2F" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
numberingService: {
|
|
||||||
async getNextIdentifier() {
|
|
||||||
return "-2F8";
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true });
|
|
||||||
assert.deepEqual(preparedTarget, {
|
|
||||||
circuitListId: "l1",
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowTransactionStore: {
|
|
||||||
moveRows() {
|
|
||||||
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 () => {
|
|
||||||
let transactionalMove:
|
|
||||||
| {
|
|
||||||
rows: Array<{ id: string; expectedCircuitId: string }>;
|
|
||||||
targetCircuitId?: string;
|
|
||||||
createTargetCircuit?: unknown;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
deviceRowRepository: {
|
|
||||||
async findById(rowId: string) {
|
|
||||||
if (rowId === "r1") {
|
|
||||||
return { id: "r1", circuitId: "c1" } as never;
|
|
||||||
}
|
|
||||||
return { id: "r2", circuitId: "c2" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowTransactionStore: {
|
|
||||||
moveRows(input: typeof transactionalMove) {
|
|
||||||
transactionalMove = input;
|
|
||||||
return {
|
|
||||||
movedRowIds: input!.rows.map((row) => row.id),
|
|
||||||
targetCircuitId: input!.targetCircuitId!,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async findById(circuitId: string) {
|
|
||||||
if (circuitId === "c1") {
|
|
||||||
return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
|
||||||
}
|
|
||||||
if (circuitId === "c2") {
|
|
||||||
return { id: "c2", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0 } as never;
|
|
||||||
}
|
|
||||||
return { id: "c3", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F3", sortOrder: 30, isReserve: 1 } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetCircuitId: "c3" });
|
|
||||||
assert.deepEqual(transactionalMove, {
|
|
||||||
rows: [
|
|
||||||
{ id: "r1", expectedCircuitId: "c1" },
|
|
||||||
{ id: "r2", expectedCircuitId: "c2" },
|
|
||||||
],
|
|
||||||
targetCircuitId: "c3",
|
|
||||||
createTargetCircuit: undefined,
|
|
||||||
});
|
|
||||||
assert.deepEqual(result, {
|
|
||||||
movedRowIds: ["r1", "r2"],
|
|
||||||
targetCircuitId: "c3",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("moving multiple device rows to placeholder creates exactly one new circuit", async () => {
|
|
||||||
let transactionCount = 0;
|
|
||||||
let preparedTarget:
|
|
||||||
| {
|
|
||||||
circuitListId: string;
|
|
||||||
sectionId: string;
|
|
||||||
equipmentIdentifier: string;
|
|
||||||
displayName: string;
|
|
||||||
sortOrder: number;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
deviceRowRepository: {
|
|
||||||
async findById(rowId: string) {
|
|
||||||
return { id: rowId, circuitId: rowId === "r1" ? "c1" : "c2" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowTransactionStore: {
|
|
||||||
moveRows(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) {
|
|
||||||
transactionCount += 1;
|
|
||||||
preparedTarget = input.createTargetCircuit;
|
|
||||||
return {
|
|
||||||
movedRowIds: input.rows.map((row) => row.id),
|
|
||||||
targetCircuitId: "c-new",
|
|
||||||
createdCircuitId: "c-new",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async findById(circuitId: string) {
|
|
||||||
if (circuitId === "c1" || circuitId === "c2") {
|
|
||||||
return { id: circuitId, sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
|
||||||
}
|
|
||||||
return null as never;
|
|
||||||
},
|
|
||||||
async listBySection() {
|
|
||||||
return [{ sortOrder: 30 }] as never[];
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitSectionRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "s2", circuitListId: "l1", prefix: "-2F" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
numberingService: {
|
|
||||||
async getNextIdentifier() {
|
|
||||||
return "-2F8";
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.moveDeviceRowsBulk({
|
|
||||||
rowIds: ["r1", "r2"],
|
|
||||||
targetSectionId: "s2",
|
|
||||||
createNewCircuit: true,
|
|
||||||
});
|
|
||||||
assert.equal(transactionCount, 1);
|
|
||||||
assert.deepEqual(preparedTarget, {
|
|
||||||
circuitListId: "l1",
|
|
||||||
sectionId: "s2",
|
|
||||||
equipmentIdentifier: "-2F8",
|
|
||||||
displayName: "Neuer Stromkreis",
|
|
||||||
sortOrder: 40,
|
|
||||||
});
|
|
||||||
assert.equal(result.createdCircuitId, "c-new");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reorders circuits inside one section without renumbering identifiers", async () => {
|
it("reorders circuits inside one section without renumbering identifiers", async () => {
|
||||||
let safeReorder: { sectionId: string; circuitIds: string[] } | undefined;
|
let safeReorder: { sectionId: string; circuitIds: string[] } | undefined;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
@@ -407,14 +133,16 @@ describe("circuit write service rules", () => {
|
|||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.reorderCircuitsInSection("s1", { orderedCircuitIds: ["c3", "c1", "c2"] });
|
await service.reorderCircuitsInSection("s1", {
|
||||||
|
orderedCircuitIds: ["c3", "c1", "c2"],
|
||||||
|
});
|
||||||
assert.deepEqual(safeReorder, {
|
assert.deepEqual(safeReorder, {
|
||||||
sectionId: "s1",
|
sectionId: "s1",
|
||||||
circuitIds: ["c3", "c1", "c2"],
|
circuitIds: ["c3", "c1", "c2"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("safe section identifier bulk update validates full section set (undo safety)", async () => {
|
it("validates the complete section set before safe identifier updates", async () => {
|
||||||
let safeCalled = false;
|
let safeCalled = false;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
circuitSectionRepository: {
|
circuitSectionRepository: {
|
||||||
@@ -436,6 +164,7 @@ describe("circuit write service rules", () => {
|
|||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.updateSectionEquipmentIdentifiers("s1", {
|
await service.updateSectionEquipmentIdentifiers("s1", {
|
||||||
identifiers: [
|
identifiers: [
|
||||||
{ circuitId: "c1", equipmentIdentifier: "-1F2" },
|
{ circuitId: "c1", equipmentIdentifier: "-1F2" },
|
||||||
@@ -444,5 +173,4 @@ describe("circuit write service rules", () => {
|
|||||||
});
|
});
|
||||||
assert.equal(safeCalled, true);
|
assert.equal(safeCalled, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user