diff --git a/AGENTS.md b/AGENTS.md index 5e93317..4f291c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -220,17 +220,18 @@ After saving, the row becomes linked to the new project device. ## Undo / Redo -The editor's visible undo/redo stack remains session-local while renumber -operations are being migrated. Existing Circuit and CircuitDeviceRow cell -edits, standalone insertion/deletion, CircuitDeviceRow moves and Circuit -reorders already execute persistent project commands; their toolbar undo/redo -actions use the project-wide history endpoints. Applying a sorted view across -multiple sections is one atomic `circuit.reorder-sections` command and one undo -step. Insertions and generated move targets use client-generated stable UUIDs, -and undo restores the same ids from complete server snapshots. The tree -response supplies the optimistic `currentRevision`; obsolete direct field -PATCH, structure POST, move, reorder, Circuit and CircuitDeviceRow DELETE -routes are removed. +The editor's visible undo/redo stack remains session-local and is not yet +reconstructed after reload. All currently supported Circuit and +CircuitDeviceRow writes execute persistent project commands; their toolbar +undo/redo actions use the project-wide history endpoints. Applying a sorted +view across multiple sections is one atomic `circuit.reorder-sections` command +and one undo step. Explicit renumbering uses the collision-safe +`circuit.renumber-section` command and is never triggered implicitly. +Insertions and generated move targets use client-generated stable UUIDs, and +undo restores the same ids from complete server snapshots. The tree response +supplies the optimistic `currentRevision`; obsolete direct field PATCH, +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. CircuitDeviceRow moves between existing circuits and moves that create one new placeholder target circuit are persisted. The latter stores the complete empty diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index d9ef240..1ef6108 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -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 target sort order. This makes deletion and moves undoable without generating replacement identities or renumbering circuits. Existing Circuit and -CircuitDeviceRow cell edits, standalone insert/delete actions and device-row -moves and reorders in the editor consume these endpoints. The renumber UI path -is still being migrated. +CircuitDeviceRow cell edits, standalone insert/delete actions, device-row +moves, reorders and explicit renumbering in the editor consume these endpoints. `circuit-device-row.move` targets existing circuits in the same circuit list. `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`) Circuit and device-row field updates, standalone insertions/deletions, single -or bulk device-row moves and circuit reorders are available only as the -corresponding versioned commands through +or bulk device-row moves, circuit reorders and explicit renumbering are +available only as the corresponding versioned commands through `POST /projects/:projectId/commands`. There are no direct field-update PATCH, -structure POST, move, reorder, Circuit DELETE or 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" } - ] -} -``` +structure POST, move, reorder, renumber, identifier-restore, Circuit DELETE or +CircuitDeviceRow DELETE routes. ## Removed Legacy Endpoints diff --git a/docs/circuit-list-editor-interactions.md b/docs/circuit-list-editor-interactions.md index 368af3e..a944636 100644 --- a/docs/circuit-list-editor-interactions.md +++ b/docs/circuit-list-editor-interactions.md @@ -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 moves and circuit reorders also use persistent project commands. Applying a sorted view across multiple sections is atomic and occupies one history step. -Renumbering still uses its direct API write and session-local inverse callback -during the cutover. +Explicit renumbering uses a collision-safe persistent project command and one +project-history step. Covered operations include: @@ -144,5 +144,3 @@ Covered operations include: Current limitations: - 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 diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index ebe2c67..56989d2 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -1,14 +1,12 @@ # Circuit List Editor Known Limitations -- Existing Circuit and CircuitDeviceRow cell edits plus standalone - insert/delete actions and CircuitDeviceRow moves use persistent project - commands and project-wide toolbar undo/redo. The editor's visible stack is - still session-local, is empty after reload and still contains the direct - renumber operation during the cutover. Circuit reorder is already persistent, - including atomic multi-section sorted-order application. +- All currently supported Circuit and CircuitDeviceRow editor writes use + persistent project commands and project-wide toolbar undo/redo, including + moves, atomic multi-section reorders and explicit renumbering. The editor's + visible stack is still session-local and empty after reload. - Server-side project revisions and persistent undo/redo eligibility exist for - the command types documented in `circuit-list-editor-api.md`; complete editor - command coverage and history reconstruction are not complete. + the command types documented in `circuit-list-editor-api.md`; visible history + reconstruction is not complete. - Named logical project snapshots and restore are not 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. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 3ab31c3..f6f7e06 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -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 dazu `currentRevision`; Undo/Redo für diese Aktionen läuft über die projektweite Serverhistorie. Direkte Feld-PATCH-, Struktur-POST-, Move-, Circuit- und -Gerätezeilen-DELETE-Endpunkte sind entfernt. Gerätezeilen-Moves und -Stromkreis-Reorders im Grid verwenden die persistenten Kommandos bereits. Nur -der Renumber-UI-Pfad verbleibt vorerst im sichtbaren sitzungslokalen Stack; -nach einem Reload wird dieser noch nicht aus der Serverhistorie rekonstruiert. +Gerätezeilen-DELETE-Endpunkte sind entfernt. Gerätezeilen-Moves, +Stromkreis-Reorders und die explizite Neunummerierung im Grid verwenden die +persistenten Kommandos. Der sichtbare Undo-/Redo-Stack wird nach einem Reload +noch nicht aus der Serverhistorie rekonstruiert. `circuit-device-row.move` verschiebt oder sortiert eine oder mehrere Zeilen zwischen vorhandenen Stromkreisen derselben Liste. Erwartete und neue 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 neuen BMKs des Abschnitts, löst Tauschkollisionen über temporäre Werte und ä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 einer sortierten Ansicht verwendet `circuit.reorder-sections`, damit alle betroffenen Bereiche in einer Transaktion und als ein Undo-Schritt gespeichert diff --git a/docs/project-history-and-external-model-architecture.md b/docs/project-history-and-external-model-architecture.md index 50119f0..7acc92f 100644 --- a/docs/project-history-and-external-model-architecture.md +++ b/docs/project-history-and-external-model-architecture.md @@ -207,6 +207,9 @@ Completed foundation: - `circuit.renumber-section` is the separate explicit renumber operation; it stores every expected/target equipment identifier, uses collision-safe 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 as one atomic multi-row operation; complete expected/target sync snapshots protect local values, links and override metadata against silent stale writes @@ -218,11 +221,9 @@ Completed foundation: 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 - CircuitDeviceRow field/insert/delete actions plus ProjectDevice operations - already use persistent commands and project-wide undo + CircuitDeviceRow operations plus ProjectDevice operations already use + persistent commands and project-wide undo - extend project-scoped commands only when further project mutations are deliberately added to history; the generic versioned envelope is complete - do not model IFCGUID as an overloaded circuit equipment identifier diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index 6218b55..baa4c2c 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -335,7 +335,7 @@ Implemented foundation: - 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 - 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 - 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 @@ -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 reorders in the circuit-list editor are connected to this history 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 -therefore remains session-local and is not reconstructed after reload. +Explicit renumbering is connected to the same persistent boundary. The +editor's visible undo/redo stack remains session-local and is not reconstructed +after reload. Acceptance criteria: diff --git a/package.json b/package.json index fb1be40..b443feb 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "build:api": "tsc -p tsconfig.json", "build:web": "next build", "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: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": "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-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:migrate": "drizzle-kit migrate", "db:backup": "tsx scripts/db-backup.ts", diff --git a/src/db/repositories/circuit-section-transaction.repository.ts b/src/db/repositories/circuit-section-transaction.repository.ts deleted file mode 100644 index 0729683..0000000 --- a/src/db/repositories/circuit-section-transaction.repository.ts +++ /dev/null @@ -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(); - } - }); - } -} diff --git a/src/domain/ports/circuit-section-transaction.store.ts b/src/domain/ports/circuit-section-transaction.store.ts deleted file mode 100644 index 39e8023..0000000 --- a/src/domain/ports/circuit-section-transaction.store.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface CircuitSectionTransactionStore { - updateEquipmentIdentifiers( - circuitListId: string, - updates: Array<{ id: string; equipmentIdentifier: string }>, - tempNamespace: string - ): void; -} diff --git a/src/domain/services/circuit-write.service.ts b/src/domain/services/circuit-write.service.ts deleted file mode 100644 index c97252e..0000000 --- a/src/domain/services/circuit-write.service.ts +++ /dev/null @@ -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); - } - -} diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index c86e62d..28fc29b 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -33,6 +33,9 @@ import { import { buildCircuitSectionReorderAssignments, } from "../utils/circuit-section-reorder-command"; +import { + buildCircuitSectionRenumberAssignments, +} from "../utils/circuit-section-renumber-command"; import type { CellKey, CellKind, @@ -57,9 +60,8 @@ import { moveCircuitDeviceRowsToNewCircuitCommand, reorderCircuitSectionCommand, reorderCircuitSectionsCommand, - renumberCircuitSection, + renumberCircuitSectionCommand, redoProjectCommand, - updateSectionEquipmentIdentifiers, updateCircuitById, updateCircuitDeviceRowById, undoProjectCommand, @@ -2592,7 +2594,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str 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) { if (!confirm("Diesen Bereich neu nummerieren? Nur die Stromkreise in diesem Bereich werden geändert.")) { return; @@ -2601,26 +2604,31 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str if (!section) { return; } - const before = section.circuits.map((circuit) => ({ - id: circuit.id, - equipmentIdentifier: circuit.equipmentIdentifier, - })); + const assignments = buildCircuitSectionRenumberAssignments( + data?.sections ?? [], + sectionId + ); + if (assignments.length === 0) { + return; + } + const label = "Bereich neu nummerieren"; await runCommand({ - label: "Bereich neu nummerieren", + label, redo: async () => { - await renumberCircuitSection(sectionId); + const result = await renumberCircuitSectionCommand( + projectId, + getExpectedProjectRevision(), + sectionId, + assignments, + label + ); + applyProjectCommandResult(result); return null; }, - undo: async () => { - // Restore prior BMKs through safe bulk endpoint to avoid transient unique collisions. - await updateSectionEquipmentIdentifiers( - sectionId, - before.map((entry) => ({ - circuitId: entry.id, - equipmentIdentifier: entry.equipmentIdentifier, - })) - ); - return null; + undo: async () => null, + persistent: { + undoIntent: () => null, + redoIntent: () => null, }, }); } diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index 6c0ae99..e9bbbea 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -42,6 +42,9 @@ import type { import type { CircuitSectionsReorderSection, } from "../../domain/models/circuit-sections-reorder-project-command.model"; +import type { + CircuitSectionRenumberAssignment, +} from "../../domain/models/circuit-section-renumber-project-command.model"; async function request(url: string, init?: RequestInit): Promise { 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( projectId: string, expectedRevision: number, @@ -362,14 +359,23 @@ export function reorderCircuitSectionsCommand( ); } -export function updateSectionEquipmentIdentifiers( +export function renumberCircuitSectionCommand( + projectId: string, + expectedRevision: number, sectionId: string, - identifiers: Array<{ circuitId: string; equipmentIdentifier: string }> + assignments: CircuitSectionRenumberAssignment[], + description: string ) { - return request(`/api/circuit-sections/${sectionId}/equipment-identifiers`, { - method: "PATCH", - body: JSON.stringify({ identifiers }), - }); + return executeProjectCommand( + projectId, + expectedRevision, + { + schemaVersion: 1, + type: "circuit.renumber-section", + payload: { sectionId, assignments }, + }, + description + ); } export function listFloors(projectId: string) { diff --git a/src/frontend/utils/circuit-section-renumber-command.ts b/src/frontend/utils/circuit-section-renumber-command.ts new file mode 100644 index 0000000..8924d10 --- /dev/null +++ b/src/frontend/utils/circuit-section-renumber-command.ts @@ -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; +} diff --git a/src/server/composition/circuit-numbering-service.ts b/src/server/composition/circuit-numbering-service.ts new file mode 100644 index 0000000..0d3d6c5 --- /dev/null +++ b/src/server/composition/circuit-numbering-service.ts @@ -0,0 +1,3 @@ +import { CircuitNumberingService } from "../../domain/services/circuit-numbering.service.js"; + +export const circuitNumberingService = new CircuitNumberingService(); diff --git a/src/server/composition/circuit-write-service.ts b/src/server/composition/circuit-write-service.ts deleted file mode 100644 index 2d3153b..0000000 --- a/src/server/composition/circuit-write-service.ts +++ /dev/null @@ -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), -}); diff --git a/src/server/controllers/circuit-section.controller.ts b/src/server/controllers/circuit-section.controller.ts deleted file mode 100644 index 43ed1ea..0000000 --- a/src/server/controllers/circuit-section.controller.ts +++ /dev/null @@ -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." }); - } -} diff --git a/src/server/controllers/circuit.controller.ts b/src/server/controllers/circuit.controller.ts index 43cad10..a94912a 100644 --- a/src/server/controllers/circuit.controller.ts +++ b/src/server/controllers/circuit.controller.ts @@ -1,5 +1,5 @@ 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) { const { sectionId } = req.params; @@ -7,7 +7,8 @@ export async function getNextCircuitIdentifier(req: Request, res: Response) { return res.status(400).json({ error: "Invalid sectionId" }); } try { - const nextIdentifier = await circuitWriteService.getNextIdentifier(sectionId); + const nextIdentifier = + await circuitNumberingService.getNextIdentifier(sectionId); return res.json({ sectionId, nextIdentifier }); } catch (error) { return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to get identifier." }); diff --git a/src/server/index.ts b/src/server/index.ts index 6881a17..31e656c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,6 +1,5 @@ import express from "express"; import { circuitRouter } from "./routes/circuit.routes.js"; -import { circuitSectionRouter } from "./routes/circuit-section.routes.js"; import { globalDeviceRouter } from "./routes/global-device.routes.js"; import { projectDeviceRouter } from "./routes/project-device.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", circuitRouter); -app.use("/api", circuitSectionRouter); app.use("/api/global-devices", globalDeviceRouter); app.use("/api/project-devices", projectDeviceRouter); diff --git a/src/server/routes/circuit-section.routes.ts b/src/server/routes/circuit-section.routes.ts deleted file mode 100644 index e61a265..0000000 --- a/src/server/routes/circuit-section.routes.ts +++ /dev/null @@ -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); diff --git a/src/shared/validation/circuit.schemas.ts b/src/shared/validation/circuit.schemas.ts deleted file mode 100644 index 9eb8ded..0000000 --- a/src/shared/validation/circuit.schemas.ts +++ /dev/null @@ -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; diff --git a/tests/circuit-section-renumber-command.test.ts b/tests/circuit-section-renumber-command.test.ts new file mode 100644 index 0000000..0802d36 --- /dev/null +++ b/tests/circuit-section-renumber-command.test.ts @@ -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 | null = null; + const originalFetch = globalThis.fetch; + globalThis.fetch = async (_input, init) => { + requestBody = JSON.parse( + String(init?.body) + ) as Record; + 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, + }, + }, + }); + }); +}); diff --git a/tests/circuit-section-transaction.repository.test.ts b/tests/circuit-section-transaction.repository.test.ts deleted file mode 100644 index e85413d..0000000 --- a/tests/circuit-section-transaction.repository.test.ts +++ /dev/null @@ -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(); - } - }); - -}); diff --git a/tests/circuit-write.rules.test.ts b/tests/circuit-write.rules.test.ts deleted file mode 100644 index f14fd76..0000000 --- a/tests/circuit-write.rules.test.ts +++ /dev/null @@ -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); - }); -});