Persist section renumbering

This commit is contained in:
2026-07-25 21:30:29 +02:00
parent abcb468807
commit c45afd0981
24 changed files with 317 additions and 638 deletions
+12 -11
View File
@@ -220,17 +220,18 @@ 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 renumber The editor's visible undo/redo stack remains session-local and is not yet
operations are being migrated. Existing Circuit and CircuitDeviceRow cell reconstructed after reload. All currently supported Circuit and
edits, standalone insertion/deletion, CircuitDeviceRow moves and Circuit CircuitDeviceRow writes execute persistent project commands; their toolbar
reorders already execute persistent project commands; their toolbar undo/redo undo/redo actions use the project-wide history endpoints. Applying a sorted
actions use the project-wide history endpoints. Applying a sorted view across view across multiple sections is one atomic `circuit.reorder-sections` command
multiple sections is one atomic `circuit.reorder-sections` command and one undo and one undo step. Explicit renumbering uses the collision-safe
step. Insertions and generated move targets use client-generated stable UUIDs, `circuit.renumber-section` command and is never triggered implicitly.
and undo restores the same ids from complete server snapshots. The tree Insertions and generated move targets use client-generated stable UUIDs, and
response supplies the optimistic `currentRevision`; obsolete direct field undo restores the same ids from complete server snapshots. The tree response
PATCH, structure POST, move, reorder, Circuit and CircuitDeviceRow DELETE supplies the optimistic `currentRevision`; obsolete direct field PATCH,
routes are removed. structure POST, move, reorder, renumber, identifier-restore, 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
+6 -28
View File
@@ -37,9 +37,8 @@ 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, standalone insert/delete actions and device-row CircuitDeviceRow cell edits, standalone insert/delete actions, device-row
moves and reorders in the editor consume these endpoints. The renumber UI path moves, reorders and explicit renumbering in the editor consume these endpoints.
is 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
@@ -176,32 +175,11 @@ Response sketch:
- preview next identifier for section (`prefix + maxSuffix + 1`) - preview next identifier for section (`prefix + maxSuffix + 1`)
Circuit and device-row field updates, standalone insertions/deletions, single Circuit and device-row field updates, standalone insertions/deletions, single
or bulk device-row moves and circuit reorders are available only as the or bulk device-row moves, circuit reorders and explicit renumbering are
corresponding versioned commands through available only as the corresponding versioned commands through
`POST /projects/:projectId/commands`. There are no direct field-update PATCH, `POST /projects/:projectId/commands`. There are no direct field-update PATCH,
structure POST, move, reorder, Circuit DELETE or CircuitDeviceRow DELETE structure POST, move, reorder, renumber, identifier-restore, Circuit DELETE or
routes. CircuitDeviceRow DELETE routes.
### Renumber Section
- `POST /circuit-sections/:sectionId/renumber`
- Purpose: explicit renumbering by section prefix; never implicit on move/sort.
### Safe Equipment Identifier Update
- `PATCH /circuit-sections/:sectionId/equipment-identifiers`
- Purpose: apply explicit per-circuit identifiers safely even with unique constraints.
Request sketch:
```json
{
"identifiers": [
{ "circuitId": "cir_1", "equipmentIdentifier": "-2F1" },
{ "circuitId": "cir_2", "equipmentIdentifier": "-2F2" }
]
}
```
## Removed Legacy Endpoints ## Removed Legacy Endpoints
+2 -4
View File
@@ -129,8 +129,8 @@ undo/redo calls the project-wide server history. New entities receive stable
UUIDs before insertion, and deletion undo restores those same ids. Device-row UUIDs before insertion, and deletion undo restores those same ids. Device-row
moves and circuit reorders also use persistent project commands. Applying a moves and circuit reorders also use persistent project commands. Applying a
sorted view across multiple sections is atomic and occupies one history step. sorted view across multiple sections is atomic and occupies one history step.
Renumbering still uses its direct API write and session-local inverse callback Explicit renumbering uses a collision-safe persistent project command and one
during the cutover. project-history step.
Covered operations include: Covered operations include:
@@ -144,5 +144,3 @@ Covered operations include:
Current limitations: Current limitations:
- the visible editor stack is still session-local and is empty after reload - the visible editor stack is still session-local and is empty after reload
- renumber editor actions are not connected to the persistent command boundary
yet
@@ -1,14 +1,12 @@
# Circuit List Editor Known Limitations # Circuit List Editor Known Limitations
- Existing Circuit and CircuitDeviceRow cell edits plus standalone - All currently supported Circuit and CircuitDeviceRow editor writes use
insert/delete actions and CircuitDeviceRow moves use persistent project persistent project commands and project-wide toolbar undo/redo, including
commands and project-wide toolbar undo/redo. The editor's visible stack is moves, atomic multi-section reorders and explicit renumbering. The editor's
still session-local, is empty after reload and still contains the direct visible stack is still session-local and empty after reload.
renumber operation during the cutover. Circuit reorder is already persistent,
including atomic multi-section sorted-order application.
- 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`; visible history
command coverage and history reconstruction are not complete. reconstruction is not complete.
- Named logical project snapshots and restore are not implemented yet. - Named logical project snapshots and restore are not implemented yet.
- No Revit/CSV/IFCGUID import and export workflow is implemented yet. - No Revit/CSV/IFCGUID import and export workflow is implemented yet.
- Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime. - Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime.
+9 -4
View File
@@ -92,10 +92,10 @@ sowie eigenständiges Einfügen und Löschen verwendet das Grid bereits über di
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-, Move-, Circuit- und Serverhistorie. Direkte Feld-PATCH-, Struktur-POST-, Move-, Circuit- und
Gerätezeilen-DELETE-Endpunkte sind entfernt. Gerätezeilen-Moves und Gerätezeilen-DELETE-Endpunkte sind entfernt. Gerätezeilen-Moves,
Stromkreis-Reorders im Grid verwenden die persistenten Kommandos bereits. Nur Stromkreis-Reorders und die explizite Neunummerierung im Grid verwenden die
der Renumber-UI-Pfad verbleibt vorerst im sichtbaren sitzungslokalen Stack; persistenten Kommandos. Der sichtbare Undo-/Redo-Stack wird nach einem Reload
nach einem Reload wird dieser noch nicht aus der Serverhistorie rekonstruiert. 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;
@@ -115,6 +115,11 @@ bleiben unverändert. `circuit.renumber-section` bildet die getrennte,
ausdrücklich ausgelöste Neunummerierung ab. Es speichert alle erwarteten und ausdrücklich ausgelöste Neunummerierung ab. Es speichert alle erwarteten und
neuen BMKs des Abschnitts, löst Tauschkollisionen über temporäre Werte und neuen BMKs des Abschnitts, löst Tauschkollisionen über temporäre Werte und
ändert weder Sortierung noch Gerätezeilen. ändert weder Sortierung noch Gerätezeilen.
Die Bereichsaktion erzeugt die vollständigen erwarteten und neuen BMKs aus
Präfix und aktueller Stromkreisreihenfolge und verwendet das persistente
Kommando für Toolbar-Undo/Redo. Die früheren direkten Renumber- und
Identifier-Restore-Endpunkte sowie ihr separater Transaktionsadapter sind
entfernt.
Drag-and-drop verwendet `circuit.reorder-section`. Die explizite Übernahme Drag-and-drop verwendet `circuit.reorder-section`. Die explizite Übernahme
einer sortierten Ansicht verwendet `circuit.reorder-sections`, damit alle einer sortierten Ansicht verwendet `circuit.reorder-sections`, damit alle
betroffenen Bereiche in einer Transaktion und als ein Undo-Schritt gespeichert betroffenen Bereiche in einer Transaktion und als ein Undo-Schritt gespeichert
@@ -207,6 +207,9 @@ Completed foundation:
- `circuit.renumber-section` is the separate explicit renumber operation; it - `circuit.renumber-section` is the separate explicit renumber operation; it
stores every expected/target equipment identifier, uses collision-safe stores every expected/target equipment identifier, uses collision-safe
temporary values and restores the exact prior identifiers on undo temporary values and restores the exact prior identifiers on undo
- the editor's explicit renumber action uses this command; obsolete direct
renumber and identifier-restore routes plus their transaction adapter are
removed
- `project-device.sync-rows` records synchronization, disconnect and reconnect - `project-device.sync-rows` records synchronization, disconnect and reconnect
as one atomic multi-row operation; complete expected/target sync snapshots as one atomic multi-row operation; complete expected/target sync snapshots
protect local values, links and override metadata against silent stale writes protect local values, links and override metadata against silent stale writes
@@ -218,11 +221,9 @@ Completed foundation:
Remaining constraints before completing persistent history: Remaining constraints before completing persistent history:
- route the remaining renumber editor writes through its existing typed
persistent command and remove the final direct mutation 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 operations plus ProjectDevice operations already use
already use persistent commands and project-wide undo persistent commands and project-wide undo
- extend project-scoped commands only when further project mutations are - extend project-scoped commands only when further project mutations are
deliberately added to history; the generic versioned envelope is complete deliberately added to history; the generic versioned envelope is complete
- do not model IFCGUID as an overloaded circuit equipment identifier - do not model IFCGUID as an overloaded circuit equipment identifier
+4 -3
View File
@@ -335,7 +335,7 @@ Implemented foundation:
- a new circuit and all of its initial device rows use the same transaction store - a new circuit and all of its initial device rows use the same transaction store
- device-row moves, reserve-state updates and optional target creation share one transaction - device-row moves, reserve-state updates and optional target creation share one transaction
- project-device synchronization, disconnect and reconnect use a separate injected transaction store - project-device synchronization, disconnect and reconnect use a separate injected transaction store
- section renumbering and circuit reordering use a separate injected transaction store - section renumbering and circuit reordering use persistent command stores
- circuit-device-row transaction tests cover both successful commits and forced SQLite rollbacks - circuit-device-row transaction tests cover both successful commits and forced SQLite rollbacks
- project-device row synchronization has real multi-row SQLite commit and rollback coverage - project-device row synchronization has real multi-row SQLite commit and rollback coverage
- BMK swaps and section reorder have real SQLite commit and rollback coverage - BMK swaps and section reorder have real SQLite commit and rollback coverage
@@ -434,8 +434,9 @@ ProjectDevice CRUD, synchronization and disconnect on the project page and
Circuit/CircuitDeviceRow field/insert/delete actions, device-row moves and Circuit/CircuitDeviceRow field/insert/delete actions, device-row moves and
circuit reorders in the circuit-list editor are connected to this history circuit reorders in the circuit-list editor are connected to this history
boundary. Applying sorted order across multiple sections is one atomic command. boundary. Applying sorted order across multiple sections is one atomic command.
The renumber UI path is not yet connected; the editor's visible undo/redo stack Explicit renumbering is connected to the same persistent boundary. The
therefore remains session-local and is not reconstructed after reload. editor's visible undo/redo stack remains session-local and is not reconstructed
after reload.
Acceptance criteria: Acceptance criteria:
+2 -2
View File
@@ -14,8 +14,8 @@
"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-device-row-move-command.test.ts tests/circuit-section-reorder-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": "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-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-section-reorder-command.test.ts tests/circuit-section-renumber-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-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-section-reorder-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-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-section-reorder-command.test.ts tests/circuit-section-renumber-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-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",
@@ -1,68 +0,0 @@
import { and, eq, inArray } from "drizzle-orm";
import type { CircuitSectionTransactionStore } from "../../domain/ports/circuit-section-transaction.store.js";
import type { AppDatabase } from "../database-context.js";
import { circuits } from "../schema/circuits.js";
export class CircuitSectionTransactionRepository
implements CircuitSectionTransactionStore
{
constructor(private readonly database: AppDatabase) {}
updateEquipmentIdentifiers(
circuitListId: string,
updates: Array<{ id: string; equipmentIdentifier: string }>,
tempNamespace: string
) {
if (updates.length === 0) {
return;
}
this.database.transaction((tx) => {
const ids = updates.map((entry) => entry.id);
const existing = tx
.select({ id: circuits.id })
.from(circuits)
.where(
and(
eq(circuits.circuitListId, circuitListId),
inArray(circuits.id, ids)
)
)
.all();
if (existing.length !== ids.length) {
throw new Error(
"One or more circuit ids are invalid for circuit list."
);
}
const stamp = Date.now();
for (let index = 0; index < updates.length; index += 1) {
const entry = updates[index];
const tempIdentifier = `__tmp_renumber_${tempNamespace}_${stamp}_${index}`;
tx
.update(circuits)
.set({ equipmentIdentifier: tempIdentifier })
.where(
and(
eq(circuits.circuitListId, circuitListId),
eq(circuits.id, entry.id)
)
)
.run();
}
for (const entry of updates) {
tx
.update(circuits)
.set({ equipmentIdentifier: entry.equipmentIdentifier })
.where(
and(
eq(circuits.circuitListId, circuitListId),
eq(circuits.id, entry.id)
)
)
.run();
}
});
}
}
@@ -1,7 +0,0 @@
export interface CircuitSectionTransactionStore {
updateEquipmentIdentifiers(
circuitListId: string,
updates: Array<{ id: string; equipmentIdentifier: string }>,
tempNamespace: string
): void;
}
@@ -1,99 +0,0 @@
import type { CircuitSectionTransactionStore } from "../ports/circuit-section-transaction.store.js";
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
import type {
UpdateSectionEquipmentIdentifiersInput,
} from "../../shared/validation/circuit.schemas.js";
import { CircuitNumberingService } from "./circuit-numbering.service.js";
export class CircuitWriteService {
private readonly circuitRepository: CircuitRepository;
private readonly circuitSectionRepository: CircuitSectionRepository;
private readonly circuitSectionTransactionStore?: CircuitSectionTransactionStore;
private readonly numberingService: CircuitNumberingService;
constructor(deps?: {
circuitRepository?: CircuitRepository;
circuitSectionRepository?: CircuitSectionRepository;
circuitSectionTransactionStore?: CircuitSectionTransactionStore;
numberingService?: CircuitNumberingService;
}) {
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository();
this.circuitSectionTransactionStore = deps?.circuitSectionTransactionStore;
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
}
private getCircuitSectionTransactionStore() {
if (!this.circuitSectionTransactionStore) {
throw new Error("Circuit-section transactions are not configured.");
}
return this.circuitSectionTransactionStore;
}
async getNextIdentifier(sectionId: string) {
return this.numberingService.getNextIdentifier(sectionId);
}
async renumberSection(sectionId: string) {
// Explicit renumber operation for one section only.
// Never renumbers other sections and never runs implicitly during move/sort operations.
const section = await this.circuitSectionRepository.findById(sectionId);
if (!section) {
throw new Error("Invalid section id.");
}
const sectionCircuits = await this.circuitRepository.listBySection(sectionId);
const otherCircuits = (await this.circuitRepository.listByCircuitList(section.circuitListId)).filter(
(circuit) => circuit.sectionId !== sectionId
);
const otherIdentifiers = new Set(otherCircuits.map((circuit) => circuit.equipmentIdentifier));
const finalAssignments: Array<{ id: string; equipmentIdentifier: string }> = [];
let index = 1;
for (const circuit of sectionCircuits) {
let candidate = `${section.prefix}${index}`;
while (otherIdentifiers.has(candidate)) {
index += 1;
candidate = `${section.prefix}${index}`;
}
finalAssignments.push({ id: circuit.id, equipmentIdentifier: candidate });
index += 1;
}
// Uses safe two-phase identifier update to avoid UNIQUE collisions during swaps.
this.getCircuitSectionTransactionStore().updateEquipmentIdentifiers(
section.circuitListId,
finalAssignments,
sectionId
);
return this.circuitRepository.listBySection(sectionId);
}
async updateSectionEquipmentIdentifiers(
sectionId: string,
input: UpdateSectionEquipmentIdentifiersInput
) {
const section = await this.circuitSectionRepository.findById(sectionId);
if (!section) {
throw new Error("Invalid section id.");
}
const sectionCircuits = await this.circuitRepository.listBySection(sectionId);
const sectionIds = new Set(sectionCircuits.map((circuit) => circuit.id));
if (input.identifiers.length !== sectionCircuits.length) {
throw new Error("identifiers must include all circuits in the section.");
}
for (const entry of input.identifiers) {
if (!sectionIds.has(entry.circuitId)) {
throw new Error("Circuit id does not belong to section.");
}
}
this.getCircuitSectionTransactionStore().updateEquipmentIdentifiers(
section.circuitListId,
input.identifiers.map((entry) => ({ id: entry.circuitId, equipmentIdentifier: entry.equipmentIdentifier })),
sectionId
);
return this.circuitRepository.listBySection(sectionId);
}
}
+27 -19
View File
@@ -33,6 +33,9 @@ import {
import { import {
buildCircuitSectionReorderAssignments, buildCircuitSectionReorderAssignments,
} from "../utils/circuit-section-reorder-command"; } from "../utils/circuit-section-reorder-command";
import {
buildCircuitSectionRenumberAssignments,
} from "../utils/circuit-section-renumber-command";
import type { import type {
CellKey, CellKey,
CellKind, CellKind,
@@ -57,9 +60,8 @@ import {
moveCircuitDeviceRowsToNewCircuitCommand, moveCircuitDeviceRowsToNewCircuitCommand,
reorderCircuitSectionCommand, reorderCircuitSectionCommand,
reorderCircuitSectionsCommand, reorderCircuitSectionsCommand,
renumberCircuitSection, renumberCircuitSectionCommand,
redoProjectCommand, redoProjectCommand,
updateSectionEquipmentIdentifiers,
updateCircuitById, updateCircuitById,
updateCircuitDeviceRowById, updateCircuitDeviceRowById,
undoProjectCommand, undoProjectCommand,
@@ -2592,7 +2594,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
await handleDeleteCircuit(intent.circuitId); await handleDeleteCircuit(intent.circuitId);
} }
// Explicit renumber action. Undo restores previous BMKs via safe section identifier update. // Explicit renumber action through project history. Sorting and device rows
// remain unchanged; the server handles collision-safe identifier swaps.
async function handleRenumberSection(sectionId: string) { async function handleRenumberSection(sectionId: string) {
if (!confirm("Diesen Bereich neu nummerieren? Nur die Stromkreise in diesem Bereich werden geändert.")) { if (!confirm("Diesen Bereich neu nummerieren? Nur die Stromkreise in diesem Bereich werden geändert.")) {
return; return;
@@ -2601,26 +2604,31 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
if (!section) { if (!section) {
return; return;
} }
const before = section.circuits.map((circuit) => ({ const assignments = buildCircuitSectionRenumberAssignments(
id: circuit.id, data?.sections ?? [],
equipmentIdentifier: circuit.equipmentIdentifier, sectionId
})); );
if (assignments.length === 0) {
return;
}
const label = "Bereich neu nummerieren";
await runCommand({ await runCommand({
label: "Bereich neu nummerieren", label,
redo: async () => { redo: async () => {
await renumberCircuitSection(sectionId); const result = await renumberCircuitSectionCommand(
projectId,
getExpectedProjectRevision(),
sectionId,
assignments,
label
);
applyProjectCommandResult(result);
return null; return null;
}, },
undo: async () => { undo: async () => null,
// Restore prior BMKs through safe bulk endpoint to avoid transient unique collisions. persistent: {
await updateSectionEquipmentIdentifiers( undoIntent: () => null,
sectionId, redoIntent: () => null,
before.map((entry) => ({
circuitId: entry.id,
equipmentIdentifier: entry.equipmentIdentifier,
}))
);
return null;
}, },
}); });
} }
+18 -12
View File
@@ -42,6 +42,9 @@ import type {
import type { import type {
CircuitSectionsReorderSection, CircuitSectionsReorderSection,
} from "../../domain/models/circuit-sections-reorder-project-command.model"; } from "../../domain/models/circuit-sections-reorder-project-command.model";
import type {
CircuitSectionRenumberAssignment,
} from "../../domain/models/circuit-section-renumber-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, {
@@ -319,12 +322,6 @@ export function moveCircuitDeviceRowsToNewCircuitCommand(
); );
} }
export function renumberCircuitSection(sectionId: string) {
return request(`/api/circuit-sections/${sectionId}/renumber`, {
method: "POST",
});
}
export function reorderCircuitSectionCommand( export function reorderCircuitSectionCommand(
projectId: string, projectId: string,
expectedRevision: number, expectedRevision: number,
@@ -362,14 +359,23 @@ export function reorderCircuitSectionsCommand(
); );
} }
export function updateSectionEquipmentIdentifiers( export function renumberCircuitSectionCommand(
projectId: string,
expectedRevision: number,
sectionId: string, sectionId: string,
identifiers: Array<{ circuitId: string; equipmentIdentifier: string }> assignments: CircuitSectionRenumberAssignment[],
description: string
) { ) {
return request(`/api/circuit-sections/${sectionId}/equipment-identifiers`, { return executeProjectCommand(
method: "PATCH", projectId,
body: JSON.stringify({ identifiers }), expectedRevision,
}); {
schemaVersion: 1,
type: "circuit.renumber-section",
payload: { sectionId, assignments },
},
description
);
} }
export function listFloors(projectId: string) { export function listFloors(projectId: string) {
@@ -0,0 +1,54 @@
import type {
CircuitSectionRenumberAssignment,
} from "../../domain/models/circuit-section-renumber-project-command.model.js";
import type { CircuitTreeSectionDto } from "../types.js";
export function buildCircuitSectionRenumberAssignments(
sections: CircuitTreeSectionDto[],
sectionId: string
): CircuitSectionRenumberAssignment[] {
const targetSection = sections.find(
(section) => section.id === sectionId
);
if (!targetSection) {
throw new Error("Der Bereich wurde nicht gefunden.");
}
const identifiersOutsideSection = new Set(
sections
.filter((section) => section.id !== sectionId)
.flatMap((section) =>
section.circuits.map(
(circuit) => circuit.equipmentIdentifier
)
)
);
const assignments: CircuitSectionRenumberAssignment[] = [];
let suffix = 1;
for (const circuit of targetSection.circuits) {
let targetEquipmentIdentifier = `${targetSection.prefix}${suffix}`;
while (
identifiersOutsideSection.has(targetEquipmentIdentifier)
) {
suffix += 1;
targetEquipmentIdentifier = `${targetSection.prefix}${suffix}`;
}
assignments.push({
circuitId: circuit.id,
expectedEquipmentIdentifier: circuit.equipmentIdentifier,
targetEquipmentIdentifier,
});
suffix += 1;
}
if (
assignments.every(
(assignment) =>
assignment.expectedEquipmentIdentifier ===
assignment.targetEquipmentIdentifier
)
) {
return [];
}
return assignments;
}
@@ -0,0 +1,3 @@
import { CircuitNumberingService } from "../../domain/services/circuit-numbering.service.js";
export const circuitNumberingService = new CircuitNumberingService();
@@ -1,7 +0,0 @@
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
import { db } from "../../db/client.js";
import { CircuitSectionTransactionRepository } from "../../db/repositories/circuit-section-transaction.repository.js";
export const circuitWriteService = new CircuitWriteService({
circuitSectionTransactionStore: new CircuitSectionTransactionRepository(db),
});
@@ -1,38 +0,0 @@
import type { Request, Response } from "express";
import { circuitWriteService } from "../composition/circuit-write-service.js";
import {
updateSectionEquipmentIdentifiersSchema,
} from "../../shared/validation/circuit.schemas.js";
export async function renumberCircuitSection(req: Request, res: Response) {
const { sectionId } = req.params;
if (typeof sectionId !== "string") {
return res.status(400).json({ error: "Invalid sectionId" });
}
try {
const updatedCircuits = await circuitWriteService.renumberSection(sectionId);
return res.json({ sectionId, circuits: updatedCircuits });
} catch (error) {
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to renumber section." });
}
}
export async function updateSectionEquipmentIdentifiers(req: Request, res: Response) {
const { sectionId } = req.params;
if (typeof sectionId !== "string") {
return res.status(400).json({ error: "Invalid sectionId" });
}
const parsed = updateSectionEquipmentIdentifiersSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
try {
const updatedCircuits = await circuitWriteService.updateSectionEquipmentIdentifiers(sectionId, parsed.data);
return res.json({ sectionId, circuits: updatedCircuits });
} catch (error) {
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to update section identifiers." });
}
}
+3 -2
View File
@@ -1,5 +1,5 @@
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { circuitWriteService } from "../composition/circuit-write-service.js"; import { circuitNumberingService } from "../composition/circuit-numbering-service.js";
export async function getNextCircuitIdentifier(req: Request, res: Response) { export async function getNextCircuitIdentifier(req: Request, res: Response) {
const { sectionId } = req.params; const { sectionId } = req.params;
@@ -7,7 +7,8 @@ export async function getNextCircuitIdentifier(req: Request, res: Response) {
return res.status(400).json({ error: "Invalid sectionId" }); return res.status(400).json({ error: "Invalid sectionId" });
} }
try { try {
const nextIdentifier = await circuitWriteService.getNextIdentifier(sectionId); const nextIdentifier =
await circuitNumberingService.getNextIdentifier(sectionId);
return res.json({ sectionId, nextIdentifier }); return res.json({ sectionId, nextIdentifier });
} catch (error) { } catch (error) {
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to get identifier." }); return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to get identifier." });
-2
View File
@@ -1,6 +1,5 @@
import express from "express"; import express from "express";
import { circuitRouter } from "./routes/circuit.routes.js"; import { circuitRouter } from "./routes/circuit.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";
import { projectDeviceRouter } from "./routes/project-device.routes.js"; import { projectDeviceRouter } from "./routes/project-device.routes.js";
import { projectRouter } from "./routes/project.routes.js"; import { projectRouter } from "./routes/project.routes.js";
@@ -17,7 +16,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", 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 {
renumberCircuitSection,
updateSectionEquipmentIdentifiers,
} from "../controllers/circuit-section.controller.js";
export const circuitSectionRouter = Router();
circuitSectionRouter.post("/circuit-sections/:sectionId/renumber", renumberCircuitSection);
circuitSectionRouter.patch("/circuit-sections/:sectionId/equipment-identifiers", updateSectionEquipmentIdentifiers);
-14
View File
@@ -1,14 +0,0 @@
import { z } from "zod";
export const updateSectionEquipmentIdentifiersSchema = z.object({
identifiers: z
.array(
z.object({
circuitId: z.string().min(1),
equipmentIdentifier: z.string().min(1),
})
)
.min(1),
});
export type UpdateSectionEquipmentIdentifiersInput = z.infer<typeof updateSectionEquipmentIdentifiersSchema>;
@@ -0,0 +1,166 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
buildCircuitSectionRenumberAssignments,
} from "../src/frontend/utils/circuit-section-renumber-command.js";
import {
renumberCircuitSectionCommand,
} from "../src/frontend/utils/api.js";
import type {
CircuitTreeCircuitDto,
CircuitTreeSectionDto,
} from "../src/frontend/types.js";
function circuit(
id: string,
equipmentIdentifier: string,
sortOrder: number
): CircuitTreeCircuitDto {
return {
id,
circuitListId: "list-1",
sectionId: "section-1",
equipmentIdentifier,
sortOrder,
isReserve: true,
circuitTotalPower: 0,
deviceRows: [],
};
}
function section(
id: string,
prefix: string,
circuits: CircuitTreeCircuitDto[]
): CircuitTreeSectionDto {
return {
id,
key: id,
displayName: id,
prefix,
sortOrder: 10,
circuits: circuits.map((entry) => ({
...entry,
sectionId: id,
})),
};
}
describe("circuit section renumber command adapters", () => {
it("plans sequential identifiers and skips conflicts outside the section", () => {
const sections = [
section("target", "-1F", [
circuit("circuit-1", "-1F7", 10),
circuit("circuit-2", "-1F9", 20),
circuit("circuit-3", "-1F11", 30),
]),
section("other", "-2F", [
circuit("circuit-other", "-1F2", 10),
]),
];
assert.deepEqual(
buildCircuitSectionRenumberAssignments(sections, "target"),
[
{
circuitId: "circuit-1",
expectedEquipmentIdentifier: "-1F7",
targetEquipmentIdentifier: "-1F1",
},
{
circuitId: "circuit-2",
expectedEquipmentIdentifier: "-1F9",
targetEquipmentIdentifier: "-1F3",
},
{
circuitId: "circuit-3",
expectedEquipmentIdentifier: "-1F11",
targetEquipmentIdentifier: "-1F4",
},
]
);
});
it("omits an already canonical section and rejects an unknown section", () => {
const sections = [
section("target", "-2F", [
circuit("circuit-1", "-2F1", 10),
circuit("circuit-2", "-2F2", 20),
]),
];
assert.deepEqual(
buildCircuitSectionRenumberAssignments(sections, "target"),
[]
);
assert.throws(
() =>
buildCircuitSectionRenumberAssignments(
sections,
"missing"
),
/nicht gefunden/
);
});
it("sends the complete assignment set through project history", async () => {
let requestBody: Record<string, unknown> | null = null;
const originalFetch = globalThis.fetch;
globalThis.fetch = async (_input, init) => {
requestBody = JSON.parse(
String(init?.body)
) as Record<string, unknown>;
return new Response(
JSON.stringify({
revision: {
revisionId: "revision-1",
changeSetId: "change-1",
projectId: "project-1",
revisionNumber: 1,
createdAtIso: "2026-07-25T00:00:00.000Z",
},
history: {
projectId: "project-1",
currentRevision: 1,
undoDepth: 1,
redoDepth: 0,
undoChangeSetId: "change-1",
redoChangeSetId: null,
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
};
const assignments = [
{
circuitId: "circuit-1",
expectedEquipmentIdentifier: "-2F9",
targetEquipmentIdentifier: "-2F1",
},
];
try {
await renumberCircuitSectionCommand(
"project-1",
0,
"section-1",
assignments,
"renumber"
);
} finally {
globalThis.fetch = originalFetch;
}
assert.deepEqual(requestBody, {
expectedRevision: 0,
description: "renumber",
command: {
schemaVersion: 1,
type: "circuit.renumber-section",
payload: {
sectionId: "section-1",
assignments,
},
},
});
});
});
@@ -1,153 +0,0 @@
import path from "node:path";
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { asc, eq } from "drizzle-orm";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import {
createDatabaseContext,
type DatabaseContext,
} from "../src/db/database-context.js";
import { CircuitSectionTransactionRepository } from "../src/db/repositories/circuit-section-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: "Stromkreis 1",
sortOrder: 10,
isReserve: 0,
},
{
id: "circuit-2",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1F2",
displayName: "Stromkreis 2",
sortOrder: 20,
isReserve: 1,
},
{
id: "circuit-3",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1F3",
displayName: "Stromkreis 3",
sortOrder: 30,
isReserve: 1,
},
])
.run();
context.db
.insert(circuitDeviceRows)
.values({
id: "row-1",
circuitId: "circuit-1",
sortOrder: 10,
name: "Leuchte",
displayName: "Leuchte",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
})
.run();
return context;
}
function listCircuits(context: DatabaseContext) {
return context.db
.select()
.from(circuits)
.orderBy(asc(circuits.id))
.all();
}
describe("circuit-section transaction repository", () => {
it("commits identifier swaps without violating the circuit-list uniqueness constraint", () => {
const context = createTestDatabase();
try {
const persisted = listCircuits(context);
const repository = new CircuitSectionTransactionRepository(context.db);
repository.updateEquipmentIdentifiers(
persisted[0].circuitListId,
[
{ id: "circuit-1", equipmentIdentifier: "-1F2" },
{ id: "circuit-2", equipmentIdentifier: "-1F1" },
],
persisted[0].sectionId
);
assert.deepEqual(
listCircuits(context).map((circuit) => circuit.equipmentIdentifier),
["-1F2", "-1F1", "-1F3"]
);
assert.equal(
context.db.select().from(circuitDeviceRows).all()[0].circuitId,
"circuit-1"
);
} finally {
context.close();
}
});
it("rolls back temporary and final identifiers when a later update fails", () => {
const context = createTestDatabase();
try {
const persisted = listCircuits(context);
context.sqlite.exec(`
CREATE TRIGGER fail_second_final_identifier
BEFORE UPDATE OF equipment_identifier ON circuits
WHEN OLD.id = 'circuit-2' AND NEW.equipment_identifier = '-1F1'
BEGIN
SELECT RAISE(ABORT, 'forced identifier failure');
END;
`);
const repository = new CircuitSectionTransactionRepository(context.db);
assert.throws(
() =>
repository.updateEquipmentIdentifiers(
persisted[0].circuitListId,
[
{ id: "circuit-1", equipmentIdentifier: "-1F2" },
{ id: "circuit-2", equipmentIdentifier: "-1F1" },
],
persisted[0].sectionId
),
/forced identifier failure/
);
assert.deepEqual(
listCircuits(context).map((circuit) => circuit.equipmentIdentifier),
["-1F1", "-1F2", "-1F3"]
);
} finally {
context.close();
}
});
});
-143
View File
@@ -1,143 +0,0 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
describe("circuit write service rules", () => {
it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
const service = new CircuitWriteService({
circuitSectionRepository: {
async findById() {
return { id: "s1", circuitListId: "l1", prefix: "-2F" } as never;
},
} as never,
circuitRepository: {
async listBySection() {
return [
{ id: "cB", sectionId: "s1", equipmentIdentifier: "-2F2", sortOrder: 10, isReserve: 0 },
{ id: "cA", sectionId: "s1", equipmentIdentifier: "-2F1", sortOrder: 20, isReserve: 1 },
] as never[];
},
async listByCircuitList() {
return [{ id: "x1", sectionId: "s2", equipmentIdentifier: "-3F1" }] as never[];
},
} as never,
circuitSectionTransactionStore: {
updateEquipmentIdentifiers(
_listId: string,
updates: Array<{ id: string; equipmentIdentifier: string }>
) {
safeUpdatePayload = updates;
},
} as never,
});
const result = await service.renumberSection("s1");
assert.deepEqual(safeUpdatePayload, [
{ id: "cB", equipmentIdentifier: "-2F1" },
{ id: "cA", equipmentIdentifier: "-2F2" },
]);
assert.equal(result.length, 2);
});
it("renumber shifts forward/backward and respects other sections", async () => {
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
const service = new CircuitWriteService({
circuitSectionRepository: {
async findById() {
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
},
} as never,
circuitRepository: {
async listBySection() {
return [
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F5", sortOrder: 10, isReserve: 0 },
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 20, isReserve: 0 },
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-1F9", sortOrder: 30, isReserve: 0 },
] as never[];
},
async listByCircuitList() {
return [{ id: "o1", sectionId: "s2", equipmentIdentifier: "-1F2" }] as never[];
},
} as never,
circuitSectionTransactionStore: {
updateEquipmentIdentifiers(
_listId: string,
updates: Array<{ id: string; equipmentIdentifier: string }>
) {
safeUpdatePayload = updates;
},
} as never,
});
await service.renumberSection("s1");
assert.deepEqual(safeUpdatePayload, [
{ id: "c2", equipmentIdentifier: "-1F1" },
{ id: "c1", equipmentIdentifier: "-1F3" },
{ id: "c3", equipmentIdentifier: "-1F4" },
]);
});
it("renumber handles gaps without touching device rows", async () => {
let safeCalled = 0;
const service = new CircuitWriteService({
circuitSectionRepository: {
async findById() {
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
},
} as never,
circuitRepository: {
async listBySection() {
return [
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 },
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F5", sortOrder: 20, isReserve: 0 },
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-1F9", sortOrder: 30, isReserve: 0 },
] as never[];
},
async listByCircuitList() {
return [] as never[];
},
} as never,
circuitSectionTransactionStore: {
updateEquipmentIdentifiers() {
safeCalled += 1;
},
} as never,
});
await service.renumberSection("s1");
assert.equal(safeCalled, 1);
});
it("validates the complete section set before safe identifier updates", async () => {
let safeCalled = false;
const service = new CircuitWriteService({
circuitSectionRepository: {
async findById() {
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
},
} as never,
circuitRepository: {
async listBySection() {
return [
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 },
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0 },
] as never[];
},
} as never,
circuitSectionTransactionStore: {
updateEquipmentIdentifiers() {
safeCalled = true;
},
} as never,
});
await service.updateSectionEquipmentIdentifiers("s1", {
identifiers: [
{ circuitId: "c1", equipmentIdentifier: "-1F2" },
{ circuitId: "c2", equipmentIdentifier: "-1F1" },
],
});
assert.equal(safeCalled, true);
});
});