Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1fc81a083 | |||
| d7b98de56a | |||
| 859fa4a42a | |||
| c1be85e408 | |||
| 0cedeaa959 | |||
| 4789e01aac | |||
| 214ad728cd | |||
| e4d22caf09 | |||
| 0f306d9a90 | |||
| 266bdb4749 | |||
| d23e7d990c | |||
| ac465a1cc0 |
@@ -21,6 +21,24 @@ It must support circuits, device rows, project devices, drag-and-drop restructur
|
|||||||
`src/frontend/components/circuit-grid-*.ts` modules.
|
`src/frontend/components/circuit-grid-*.ts` modules.
|
||||||
- Critical multi-write commands use injected transaction repositories with real
|
- Critical multi-write commands use injected transaction repositories with real
|
||||||
SQLite commit/rollback tests.
|
SQLite commit/rollback tests.
|
||||||
|
- All supported runtime project-command stores, including full snapshot
|
||||||
|
restoration, share
|
||||||
|
`src/db/repositories/project-command-transaction.persistence.ts` for the
|
||||||
|
atomic domain-write, revision and history transition boundary. Its applied
|
||||||
|
forward-command variant preserves derived CircuitDeviceRow override metadata.
|
||||||
|
- Low-level `appendProjectRevision` persistence is adapter-internal and tested
|
||||||
|
directly; do not reintroduce a standalone runtime revision repository.
|
||||||
|
- Runtime domain services receive narrow reader/store dependencies explicitly;
|
||||||
|
concrete SQLite repositories are instantiated only under `src/server/composition`.
|
||||||
|
- General application repositories also require an explicit `AppDatabase`;
|
||||||
|
`src/server/composition/application-repositories.ts` owns their runtime
|
||||||
|
instances, and controllers never import the global SQLite client.
|
||||||
|
- The upgrade-only legacy migration service follows the same dependency rule;
|
||||||
|
its concrete repositories are instantiated only in
|
||||||
|
`scripts/db-migrate-legacy-consumers.ts`.
|
||||||
|
- General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard
|
||||||
|
repositories expose only active reads. Runtime writes belong in typed command
|
||||||
|
repositories; direct integration fixtures belong under `tests/support`.
|
||||||
|
|
||||||
The supported runtime model is Circuit-First. The former Consumer UI and API are
|
The supported runtime model is Circuit-First. The former Consumer UI and API are
|
||||||
removed. Retained `consumers` rows, migration mappings and reports are upgrade-only
|
removed. Retained `consumers` rows, migration mappings and reports are upgrade-only
|
||||||
@@ -272,6 +290,11 @@ Distribution-board setup uses `distribution-board.insert` with a complete
|
|||||||
stable snapshot of the board, circuit list and four default sections. Its
|
stable snapshot of the board, circuit list and four default sections. Its
|
||||||
inverse removes only the same unchanged and still-empty structure; the POST
|
inverse removes only the same unchanged and still-empty structure; the POST
|
||||||
route requires `expectedRevision` and returns the updated history state.
|
route requires `expectedRevision` and returns the updated history state.
|
||||||
|
Floor and room setup use `project-floor.insert` and `project-room.insert` with
|
||||||
|
complete stable snapshots. Their inverses remove only unchanged records and
|
||||||
|
reject floors with assigned rooms or rooms referenced by device rows or
|
||||||
|
retained upgrade data. Both POST routes require `expectedRevision` and return
|
||||||
|
the updated history state.
|
||||||
|
|
||||||
Required operations:
|
Required operations:
|
||||||
|
|
||||||
@@ -341,6 +364,7 @@ For a normal code change run the relevant focused tests plus:
|
|||||||
- `npm test`
|
- `npm test`
|
||||||
- `npm run build:api`
|
- `npm run build:api`
|
||||||
- `npm run build:web`
|
- `npm run build:web`
|
||||||
|
- `npm run typecheck:scripts`
|
||||||
- `npx tsc --noEmit -p tsconfig.next.json`
|
- `npx tsc --noEmit -p tsconfig.next.json`
|
||||||
|
|
||||||
Use a concise imperative commit message for each completed, verified work package.
|
Use a concise imperative commit message for each completed, verified work package.
|
||||||
|
|||||||
@@ -190,6 +190,23 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
|
|||||||
structure; Redo restores the same ids
|
structure; Redo restores the same ids
|
||||||
- stale revisions return `409 PROJECT_REVISION_CONFLICT`
|
- stale revisions return `409 PROJECT_REVISION_CONFLICT`
|
||||||
|
|
||||||
|
### Project Floors and Rooms
|
||||||
|
|
||||||
|
- `POST /projects/:projectId/floors`
|
||||||
|
- body: `{ "name": "EG", "expectedRevision": 13 }`
|
||||||
|
- executes `project-floor.insert` with a stable floor id
|
||||||
|
- response: `{ "floor": { ... }, "revision": { ... }, "history": { ... } }`
|
||||||
|
- `POST /projects/:projectId/rooms`
|
||||||
|
- body:
|
||||||
|
`{ "floorId": "floor_1", "roomNumber": "001", "roomName": "Technik", "expectedRevision": 14 }`
|
||||||
|
- executes `project-room.insert` with a stable room id; `floorId` is optional
|
||||||
|
and must belong to the project when present
|
||||||
|
- response: `{ "room": { ... }, "revision": { ... }, "history": { ... } }`
|
||||||
|
- Persistent Undo removes only unchanged, unreferenced records. A floor with
|
||||||
|
assigned rooms and a room referenced by device rows or retained upgrade data
|
||||||
|
are rejected instead of silently clearing foreign keys.
|
||||||
|
- Stale revisions return `409 PROJECT_REVISION_CONFLICT`.
|
||||||
|
|
||||||
### Tree Endpoint
|
### Tree Endpoint
|
||||||
|
|
||||||
- `GET /projects/:projectId/circuit-lists/:circuitListId/tree`
|
- `GET /projects/:projectId/circuit-lists/:circuitListId/tree`
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ liegt über einen Host-Mount außerhalb des Containers.
|
|||||||
- `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe
|
- `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe
|
||||||
- `src/server/index.ts` und `src/server/routes/` – API-Komposition
|
- `src/server/index.ts` und `src/server/routes/` – API-Komposition
|
||||||
- `src/domain/services/` – fachliche Command- und Synchronisierungsregeln
|
- `src/domain/services/` – fachliche Command- und Synchronisierungsregeln
|
||||||
|
- `src/server/composition/` – Verdrahtung fachlicher Services mit konkreten
|
||||||
|
SQLite-Repositories
|
||||||
- `src/db/repositories/` – Abfragen, Persistenzmapper und Transaktionsadapter
|
- `src/db/repositories/` – Abfragen, Persistenzmapper und Transaktionsadapter
|
||||||
- `src/db/schema/` und `src/db/migrations/` – SQLite-Schema und Migrationen
|
- `src/db/schema/` und `src/db/migrations/` – SQLite-Schema und Migrationen
|
||||||
|
|
||||||
@@ -66,11 +68,13 @@ Der Editor besitzt keinen sitzungslokalen Undo-/Redo-Stapel mehr. Beim initialen
|
|||||||
Laden und nach jedem Tree-Reload liest er den persistenten History-Status und
|
Laden und nach jedem Tree-Reload liest er den persistenten History-Status und
|
||||||
gleicht dessen Revision mit `currentRevision` des Trees ab. Das Datenmodell
|
gleicht dessen Revision mit `currentRevision` des Trees ab. Das Datenmodell
|
||||||
besitzt einen projektbezogenen Revisionszähler sowie getrennte Revision-/Change-Set-
|
besitzt einen projektbezogenen Revisionszähler sowie getrennte Revision-/Change-Set-
|
||||||
Tabellen. Ein getestetes Repository kann diese Historienmetadaten optimistisch
|
Tabellen. Eine direkt getestete Persistence-Funktion schreibt diese
|
||||||
und atomar fortschreiben. Vorwärts- und Rückwärtskommandos besitzen einen
|
Historienmetadaten innerhalb der zentralen Command-Transaktion optimistisch und
|
||||||
|
atomar fort. Vorwärts- und Rückwärtskommandos besitzen einen
|
||||||
versionierten, JSON-sicheren Umschlag; Typ und Payload können dadurch nach
|
versionierten, JSON-sicheren Umschlag; Typ und Payload können dadurch nach
|
||||||
einem Neustart verlustfrei rekonstruiert werden. Alle aktuell unterstützten
|
einem Neustart verlustfrei rekonstruiert werden. Alle aktuell unterstützten
|
||||||
Circuit-, Gerätezeilen-, Projektgeräte- und Projekteinstellungsänderungen
|
Circuit-, Gerätezeilen-, Projektgeräte-, Projektstruktur- und
|
||||||
|
Projekteinstellungsänderungen
|
||||||
verwenden typisierte Command-Stores, die Fachänderung, automatisch erzeugtes
|
verwenden typisierte Command-Stores, die Fachänderung, automatisch erzeugtes
|
||||||
inverses Kommando, Revision und Historienstapel gemeinsam committen
|
inverses Kommando, Revision und Historienstapel gemeinsam committen
|
||||||
beziehungsweise zurückrollen.
|
beziehungsweise zurückrollen.
|
||||||
@@ -83,6 +87,26 @@ absteigend paginierte Revisions-Timeline ist ohne Befehls-Payloads über
|
|||||||
`GET /api/projects/:projectId/history/revisions` lesbar. Ein zentraler
|
`GET /api/projects/:projectId/history/revisions` lesbar. Ein zentraler
|
||||||
Dispatcher führt die nachfolgend beschriebenen typisierten Kommandos über
|
Dispatcher führt die nachfolgend beschriebenen typisierten Kommandos über
|
||||||
öffentliche Command-, Undo- und Redo-Endpunkte aus.
|
öffentliche Command-, Undo- und Redo-Endpunkte aus.
|
||||||
|
Alle unterstützten Runtime-Project-Command-Stores einschließlich der
|
||||||
|
vollständigen Snapshot-Wiederherstellung verwenden dabei
|
||||||
|
`project-command-transaction.persistence.ts` als gemeinsame äußere
|
||||||
|
Transaktionsgrenze. Sie führt Fachänderung, Revisions-Append und
|
||||||
|
History-Transition in fester Reihenfolge innerhalb derselben SQLite-Transaktion
|
||||||
|
aus. Für CircuitDeviceRow-Feldänderungen übernimmt eine typisierte Variante den
|
||||||
|
während der Fachänderung um Override-Metadaten ergänzten Forward-Command. Die
|
||||||
|
fachlichen Validierungs-, Forward- und Inversenregeln bleiben im jeweiligen
|
||||||
|
Store.
|
||||||
|
Die Low-Level-Funktion `appendProjectRevision` bleibt ein internes Detail dieser
|
||||||
|
Persistenzgrenze und wird direkt mit einer realen SQLite-Transaktion getestet.
|
||||||
|
Ein eigenständiges Runtime-Revisions-Repository existiert nicht.
|
||||||
|
`ProjectDeviceSyncService` und `CircuitNumberingService` kennen nur schmale,
|
||||||
|
fachlich benannte Reader-Interfaces. Ihre SQLite-Repositories werden
|
||||||
|
ausschließlich in `src/server/composition/` erzeugt und injiziert.
|
||||||
|
Auch die allgemeinen Projekt-, Geräte-, Raum-, Geschoss- und Circuit-
|
||||||
|
Repositories verlangen einen expliziten `AppDatabase`-Kontext. Ihre
|
||||||
|
Anwendungsinstanzen werden zentral in
|
||||||
|
`src/server/composition/application-repositories.ts` erzeugt; Controller
|
||||||
|
importieren weder den globalen SQLite-Client noch konkrete Repository-Klassen.
|
||||||
`circuit-device-row.insert` und `circuit-device-row.delete` sind atomare
|
`circuit-device-row.insert` und `circuit-device-row.delete` sind atomare
|
||||||
Strukturkommandos. Beim Löschen wird die vollständige Zeile im inversen
|
Strukturkommandos. Beim Löschen wird die vollständige Zeile im inversen
|
||||||
Kommando gesichert, sodass Undo dieselbe UUID und alle Fachwerte wiederherstellt.
|
Kommando gesichert, sodass Undo dieselbe UUID und alle Fachwerte wiederherstellt.
|
||||||
@@ -198,6 +222,14 @@ Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil.
|
|||||||
vollständig unveränderten, weiterhin stromkreislosen Block. Controller und
|
vollständig unveränderten, weiterhin stromkreislosen Block. Controller und
|
||||||
Projektseite übergeben die erwartete Projektrevision; der frühere direkte
|
Projektseite übergeben die erwartete Projektrevision; der frühere direkte
|
||||||
Controller-Schreibweg ist entfernt.
|
Controller-Schreibweg ist entfernt.
|
||||||
|
`project-floor.insert` und `project-room.insert` versionieren die Anlage von
|
||||||
|
Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden
|
||||||
|
jeweils die persistierte Inverse für Undo/Redo. Ein Geschoss wird durch Undo nur
|
||||||
|
entfernt, solange ihm kein Raum zugeordnet wurde. Ein Raum wird nur entfernt,
|
||||||
|
solange weder eine CircuitDeviceRow noch ein aufbewahrter Upgrade-Datensatz auf
|
||||||
|
ihn verweist. Beide POST-Endpunkte verlangen `expectedRevision`, liefern den
|
||||||
|
aktualisierten Historienstand und besitzen keinen direkten Create-Schreibweg
|
||||||
|
mehr.
|
||||||
|
|
||||||
## Projektgeräte
|
## Projektgeräte
|
||||||
|
|
||||||
@@ -220,8 +252,20 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
|
|||||||
- `npm run db:migrate` wendet Drizzle-Migrationen an.
|
- `npm run db:migrate` wendet Drizzle-Migrationen an.
|
||||||
- `npm run db:verify:circuit-schema` prüft erforderliche und entfernte Spalten.
|
- `npm run db:verify:circuit-schema` prüft erforderliche und entfernte Spalten.
|
||||||
- `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup.
|
- `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup.
|
||||||
|
- `npm run typecheck:scripts` prüft alle TypeScript-Wartungs- und
|
||||||
|
Upgrade-Skripte mit ihren Anwendungspfaden, ohne Code zu erzeugen.
|
||||||
- Angewendete Migrationen werden niemals nachträglich verändert.
|
- Angewendete Migrationen werden niemals nachträglich verändert.
|
||||||
- `db:migrate:legacy-consumers` ist Upgrade-Werkzeug, kein Anwendungspfad.
|
- `db:migrate:legacy-consumers` ist Upgrade-Werkzeug, kein Anwendungspfad.
|
||||||
|
Der fachliche Migrationsdienst kennt nur schmale Reader-/Store-Ports unter
|
||||||
|
`src/domain/ports`; konkrete SQLite-Repositories werden ausschließlich im
|
||||||
|
CLI-Skript zusammengesetzt.
|
||||||
|
- Allgemeine Circuit-, Gerätezeilen-, CircuitList- und DistributionBoard-
|
||||||
|
Repositories stellen im Anwendungspfad nur noch benötigte Leseabfragen bereit.
|
||||||
|
Fachliche Schreibvorgänge liegen in den typisierten Command-Repositories;
|
||||||
|
Upgrade-Schreibvorgänge bleiben in expliziten Migrationsadaptern.
|
||||||
|
- Die vollständige Verteilungs-Testfixture liegt unter
|
||||||
|
`tests/support/distribution-board-fixture.ts` und ist kein exportierter
|
||||||
|
Produktions-Schreibweg.
|
||||||
|
|
||||||
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
||||||
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ Beispiel `Preserve circuit blocks during filtering`.
|
|||||||
npm test
|
npm test
|
||||||
npm run build:api
|
npm run build:api
|
||||||
npm run build:web
|
npm run build:web
|
||||||
|
npm run typecheck:scripts
|
||||||
npx tsc --noEmit -p tsconfig.next.json
|
npx tsc --noEmit -p tsconfig.next.json
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
@@ -83,6 +84,9 @@ Invoke-WebRequest http://localhost:3000/health
|
|||||||
```
|
```
|
||||||
|
|
||||||
Bei sichtbaren Editoränderungen ist außerdem ein kurzer GUI-Test erforderlich.
|
Bei sichtbaren Editoränderungen ist außerdem ein kurzer GUI-Test erforderlich.
|
||||||
|
Testaufbau, der vollständige Projektstrukturen direkt einfügt, gehört nach
|
||||||
|
`tests/support`. Produktions-Repositories dürfen nicht nur für Testfixtures
|
||||||
|
erneut direkte Schreibmethoden erhalten.
|
||||||
|
|
||||||
## Migrationen
|
## Migrationen
|
||||||
|
|
||||||
|
|||||||
@@ -270,11 +270,18 @@ Completed foundation:
|
|||||||
list and default-section structure with stable ids; its delete inverse
|
list and default-section structure with stable ids; its delete inverse
|
||||||
refuses changed or populated structures and the project-page POST tracks the
|
refuses changed or populated structures and the project-page POST tracks the
|
||||||
returned revision
|
returned revision
|
||||||
|
- `project-floor.insert` and `project-room.insert` persist complete location
|
||||||
|
records with stable ids; their inverses reject changed or referenced records
|
||||||
|
so Undo never clears room assignments or device-row links implicitly
|
||||||
|
- the floor and room POST routes require the current expected revision and the
|
||||||
|
project page tracks the returned history state
|
||||||
|
|
||||||
Remaining constraints before completing project version history:
|
Remaining constraints before completing project version history:
|
||||||
|
|
||||||
- extend project-scoped commands only when further project mutations are
|
- all current normal project-scoped runtime writes are behind the revision
|
||||||
deliberately added to history; the generic versioned envelope is complete
|
boundary; extend it whenever further project mutations are introduced
|
||||||
|
- project creation remains initialization at revision zero, while global device
|
||||||
|
templates remain outside any single project history
|
||||||
- do not model IFCGUID as an overloaded circuit equipment identifier
|
- do not model IFCGUID as an overloaded circuit equipment identifier
|
||||||
- keep database backup/restore checks separate from project history tests
|
- keep database backup/restore checks separate from project history tests
|
||||||
|
|
||||||
|
|||||||
@@ -303,6 +303,8 @@ Acceptance criteria:
|
|||||||
|
|
||||||
## Phase 11: Persistence and Transaction Foundation
|
## Phase 11: Persistence and Transaction Foundation
|
||||||
|
|
||||||
|
Status: Complete.
|
||||||
|
|
||||||
Goal:
|
Goal:
|
||||||
|
|
||||||
Prepare the current SQLite implementation for persistent history and a later PostgreSQL adapter without changing user-facing behavior.
|
Prepare the current SQLite implementation for persistent history and a later PostgreSQL adapter without changing user-facing behavior.
|
||||||
@@ -326,8 +328,9 @@ Implemented foundation:
|
|||||||
|
|
||||||
- database contexts can be created independently from the production singleton
|
- database contexts can be created independently from the production singleton
|
||||||
- SQLite foreign-key enforcement is enabled explicitly for every context
|
- SQLite foreign-key enforcement is enabled explicitly for every context
|
||||||
- the distribution-board repository receives its database dependency explicitly
|
- the read-only distribution-board repository receives its database dependency explicitly
|
||||||
- distribution-board setup has real in-memory SQLite commit and rollback coverage using production migrations
|
- distribution-board setup uses its persistent command repository with real
|
||||||
|
in-memory SQLite commit and rollback coverage using production migrations
|
||||||
- database backups use SQLite's online backup API and include committed WAL data
|
- database backups use SQLite's online backup API and include committed WAL data
|
||||||
- every backup is opened independently and checked for integrity and foreign-key violations
|
- every backup is opened independently and checked for integrity and foreign-key violations
|
||||||
- an integration test restores the backup into a separate database and verifies its snapshot contents
|
- an integration test restores the backup into a separate database and verifies its snapshot contents
|
||||||
@@ -339,9 +342,26 @@ Implemented foundation:
|
|||||||
- 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
|
||||||
- the legacy consumer migration receives its database dependency at the script entry point
|
- the legacy consumer migration service depends only on narrow domain ports;
|
||||||
|
all concrete repositories are composed at the script entry point
|
||||||
- legacy circuit, row, mapping and report writes have real late-failure rollback coverage
|
- legacy circuit, row, mapping and report writes have real late-failure rollback coverage
|
||||||
- circuit and device-row persistence value mapping is separated from the general repositories
|
- circuit and device-row persistence value mapping is separated from the general repositories
|
||||||
|
- obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and
|
||||||
|
Project repository writes are removed; integration fixtures live under
|
||||||
|
`tests/support` instead of production repositories
|
||||||
|
- all supported runtime project-command stores, including complete snapshot
|
||||||
|
restore, share one tested transaction wrapper for domain mutation, revision
|
||||||
|
append and history transition; its applied-forward variant retains derived
|
||||||
|
CircuitDeviceRow override metadata
|
||||||
|
- low-level revision append persistence is tested directly; the unused
|
||||||
|
standalone runtime revision repository and store interface are removed
|
||||||
|
- runtime project-device synchronization and circuit-numbering services depend
|
||||||
|
on narrow domain readers; server composition injects the SQLite repositories
|
||||||
|
- all general application repositories require an explicit database context;
|
||||||
|
server composition owns their runtime instances and controllers contain no
|
||||||
|
direct global SQLite access
|
||||||
|
- TypeScript maintenance and upgrade scripts have a dedicated no-emit
|
||||||
|
typecheck, including all referenced application modules
|
||||||
- the legacy consumer UI and application read/write endpoints are removed after verified data cutover
|
- the legacy consumer UI and application read/write endpoints are removed after verified data cutover
|
||||||
- retained legacy rows are accessible only through explicit database upgrade tooling
|
- retained legacy rows are accessible only through explicit database upgrade tooling
|
||||||
- project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields
|
- project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields
|
||||||
@@ -451,6 +471,9 @@ Implemented foundation:
|
|||||||
- distribution-board setup uses `distribution-board.insert` with a complete
|
- distribution-board setup uses `distribution-board.insert` with a complete
|
||||||
stable board/list/default-section snapshot; Undo removes only an unchanged
|
stable board/list/default-section snapshot; Undo removes only an unchanged
|
||||||
empty setup and the former direct controller write is removed
|
empty setup and the former direct controller write is removed
|
||||||
|
- floor and room setup use `project-floor.insert` and `project-room.insert`
|
||||||
|
with stable ids; their inverses refuse assigned or referenced records and the
|
||||||
|
former direct repository create methods are removed
|
||||||
- revision metadata, change-set payloads and the project counter are committed
|
- revision metadata, change-set payloads and the project counter are committed
|
||||||
in one SQLite transaction
|
in one SQLite transaction
|
||||||
- a stale expected revision produces no history writes
|
- a stale expected revision produces no history writes
|
||||||
|
|||||||
+3
-2
@@ -13,9 +13,10 @@
|
|||||||
"build": "npm run build:api",
|
"build": "npm run build:api",
|
||||||
"build:api": "tsc -p tsconfig.json",
|
"build:api": "tsc -p tsconfig.json",
|
||||||
"build:web": "next build",
|
"build:web": "next build",
|
||||||
|
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
||||||
"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-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.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/distribution-board-structure-project-command.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-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.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.service.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-editor-history.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-structure-project-command.repository.test.ts tests/project-location-structure-project-command.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.persistence.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-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.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-editor-history.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/distribution-board-structure-project-command.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-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.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.service.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-editor-history.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-structure-project-command.repository.test.ts tests/project-location-structure-project-command.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.persistence.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-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.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,10 +1,11 @@
|
|||||||
|
import { db } from "../src/db/client.js";
|
||||||
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
||||||
import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js";
|
import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js";
|
||||||
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
||||||
|
|
||||||
const projectRepository = new ProjectRepository();
|
const projectRepository = new ProjectRepository(db);
|
||||||
const circuitListRepository = new CircuitListRepository();
|
const circuitListRepository = new CircuitListRepository(db);
|
||||||
const circuitSectionRepository = new CircuitSectionRepository();
|
const circuitSectionRepository = new CircuitSectionRepository(db);
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const projects = await projectRepository.list();
|
const projects = await projectRepository.list();
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { db } from "../src/db/client.js";
|
import { db } from "../src/db/client.js";
|
||||||
|
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
||||||
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
||||||
|
import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js";
|
||||||
import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js";
|
import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js";
|
||||||
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
||||||
|
import { RoomRepository } from "../src/db/repositories/room.repository.js";
|
||||||
import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js";
|
import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js";
|
||||||
|
|
||||||
const projectRepository = new ProjectRepository();
|
const projectRepository = new ProjectRepository(db);
|
||||||
const circuitListRepository = new CircuitListRepository();
|
const circuitListRepository = new CircuitListRepository(db);
|
||||||
const migrationRepository = new LegacyConsumerMigrationRepository(db);
|
const migrationRepository = new LegacyConsumerMigrationRepository(db);
|
||||||
const migrationService = new LegacyConsumerMigrationService(
|
const migrationService = new LegacyConsumerMigrationService({
|
||||||
migrationRepository
|
circuitListReader: circuitListRepository,
|
||||||
);
|
sectionStore: new CircuitSectionRepository(db),
|
||||||
|
circuitReader: new CircuitRepository(db),
|
||||||
|
roomReader: new RoomRepository(db),
|
||||||
|
migrationStore: migrationRepository,
|
||||||
|
});
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const projects = await projectRepository.list();
|
const projects = await projectRepository.list();
|
||||||
|
|||||||
@@ -204,14 +204,19 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
async function handleCreateFloor(event: FormEvent<HTMLFormElement>) {
|
async function handleCreateFloor(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!projectId || !floorName.trim()) {
|
if (!projectId || !project || !floorName.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const created = await createFloor(projectId, { name: floorName.trim() });
|
const result = await createFloor(
|
||||||
setFloors((current) => [...current, created]);
|
projectId,
|
||||||
|
{ name: floorName.trim() },
|
||||||
|
project.currentRevision
|
||||||
|
);
|
||||||
|
setFloors((current) => [...current, result.floor]);
|
||||||
|
applyProjectRevision(result.history.currentRevision);
|
||||||
setFloorName("");
|
setFloorName("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Etage konnte nicht erstellt werden.");
|
setError(err instanceof Error ? err.message : "Etage konnte nicht erstellt werden.");
|
||||||
@@ -222,18 +227,28 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
async function handleCreateRoom(event: FormEvent<HTMLFormElement>) {
|
async function handleCreateRoom(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!projectId || !roomNumber.trim() || !roomName.trim()) {
|
if (
|
||||||
|
!projectId ||
|
||||||
|
!project ||
|
||||||
|
!roomNumber.trim() ||
|
||||||
|
!roomName.trim()
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const created = await createRoom(projectId, {
|
const result = await createRoom(
|
||||||
floorId: roomFloorId || undefined,
|
projectId,
|
||||||
roomNumber: roomNumber.trim(),
|
{
|
||||||
roomName: roomName.trim(),
|
floorId: roomFloorId || undefined,
|
||||||
});
|
roomNumber: roomNumber.trim(),
|
||||||
setRooms((current) => [...current, created]);
|
roomName: roomName.trim(),
|
||||||
|
},
|
||||||
|
project.currentRevision
|
||||||
|
);
|
||||||
|
setRooms((current) => [...current, result.room]);
|
||||||
|
applyProjectRevision(result.history.currentRevision);
|
||||||
setRoomNumber("");
|
setRoomNumber("");
|
||||||
setRoomName("");
|
setRoomName("");
|
||||||
setRoomFloorId("");
|
setRoomFloorId("");
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
|||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
interface PersistedMoveRow {
|
interface PersistedMoveRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -36,29 +35,12 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
|||||||
execute(input: ExecuteCircuitDeviceRowMoveCommandInput) {
|
execute(input: ExecuteCircuitDeviceRowMoveCommandInput) {
|
||||||
this.assertSupportedCommand(input.command);
|
this.assertSupportedCommand(input.command);
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const inverse = this.applyCommand(
|
this.database,
|
||||||
tx,
|
input,
|
||||||
input.projectId,
|
(tx) =>
|
||||||
input.command
|
this.applyCommand(tx, input.projectId, input.command)
|
||||||
);
|
);
|
||||||
const revision = appendProjectRevision(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
expectedRevision: input.expectedRevision,
|
|
||||||
source: input.source,
|
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertSupportedCommand(
|
private assertSupportedCommand(
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import {
|
|||||||
toCircuitDeviceRowPatchValues,
|
toCircuitDeviceRowPatchValues,
|
||||||
type CircuitDeviceRowPatchInput,
|
type CircuitDeviceRowPatchInput,
|
||||||
} from "./circuit-device-row.persistence.js";
|
} from "./circuit-device-row.persistence.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
||||||
|
|
||||||
@@ -34,99 +33,95 @@ export class CircuitDeviceRowProjectCommandRepository
|
|||||||
executeUpdate(input: ExecuteCircuitDeviceRowUpdateCommandInput) {
|
executeUpdate(input: ExecuteCircuitDeviceRowUpdateCommandInput) {
|
||||||
assertCircuitDeviceRowUpdateProjectCommand(input.command);
|
assertCircuitDeviceRowUpdateProjectCommand(input.command);
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransactionWithAppliedForward(
|
||||||
const current = tx
|
this.database,
|
||||||
.select()
|
input,
|
||||||
.from(circuitDeviceRows)
|
(tx) => this.applyCommand(tx, input)
|
||||||
.where(eq(circuitDeviceRows.id, input.command.payload.rowId))
|
);
|
||||||
.get();
|
}
|
||||||
if (!current) {
|
|
||||||
throw new Error("Invalid device row id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const circuit = tx
|
private applyCommand(
|
||||||
.select({ circuitListId: circuits.circuitListId })
|
tx: AppDatabase,
|
||||||
.from(circuits)
|
input: ExecuteCircuitDeviceRowUpdateCommandInput
|
||||||
.where(eq(circuits.id, current.circuitId))
|
) {
|
||||||
.get();
|
const current = tx
|
||||||
const owningList = circuit
|
.select()
|
||||||
? tx
|
.from(circuitDeviceRows)
|
||||||
.select({ id: circuitLists.id })
|
.where(eq(circuitDeviceRows.id, input.command.payload.rowId))
|
||||||
.from(circuitLists)
|
.get();
|
||||||
.where(
|
if (!current) {
|
||||||
and(
|
throw new Error("Invalid device row id.");
|
||||||
eq(circuitLists.id, circuit.circuitListId),
|
}
|
||||||
eq(circuitLists.projectId, input.projectId)
|
|
||||||
)
|
const circuit = tx
|
||||||
|
.select({ circuitListId: circuits.circuitListId })
|
||||||
|
.from(circuits)
|
||||||
|
.where(eq(circuits.id, current.circuitId))
|
||||||
|
.get();
|
||||||
|
const owningList = circuit
|
||||||
|
? tx
|
||||||
|
.select({ id: circuitLists.id })
|
||||||
|
.from(circuitLists)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(circuitLists.id, circuit.circuitListId),
|
||||||
|
eq(circuitLists.projectId, input.projectId)
|
||||||
)
|
)
|
||||||
.get()
|
)
|
||||||
: null;
|
.get()
|
||||||
if (!owningList) {
|
: null;
|
||||||
throw new Error("Circuit device row does not belong to project.");
|
if (!owningList) {
|
||||||
}
|
throw new Error("Circuit device row does not belong to project.");
|
||||||
|
}
|
||||||
|
|
||||||
const patch = Object.fromEntries(
|
const patch = Object.fromEntries(
|
||||||
input.command.payload.changes.map((change) => [
|
input.command.payload.changes.map((change) => [
|
||||||
change.field,
|
change.field,
|
||||||
change.value,
|
change.value,
|
||||||
])
|
])
|
||||||
) as CircuitDeviceRowPatchInput;
|
) as CircuitDeviceRowPatchInput;
|
||||||
this.assertLinkedProjectDevice(tx, input.projectId, patch);
|
this.assertLinkedProjectDevice(tx, input.projectId, patch);
|
||||||
this.assertRoom(tx, input.projectId, patch);
|
this.assertRoom(tx, input.projectId, patch);
|
||||||
|
|
||||||
const overriddenFields = deriveOverriddenFieldsForLocalEdit(
|
const overriddenFields = deriveOverriddenFieldsForLocalEdit(
|
||||||
current,
|
current,
|
||||||
patch
|
patch
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
overriddenFields !== undefined &&
|
overriddenFields !== undefined &&
|
||||||
patch.overriddenFields === undefined
|
patch.overriddenFields === undefined
|
||||||
) {
|
) {
|
||||||
patch.overriddenFields = overriddenFields;
|
patch.overriddenFields = overriddenFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appliedForward = createCircuitDeviceRowUpdateProjectCommand(
|
const appliedForward = createCircuitDeviceRowUpdateProjectCommand(
|
||||||
current.id,
|
current.id,
|
||||||
patch as CircuitDeviceRowUpdatePatch
|
patch as CircuitDeviceRowUpdatePatch
|
||||||
);
|
);
|
||||||
const inversePatch = Object.fromEntries(
|
const inversePatch = Object.fromEntries(
|
||||||
appliedForward.payload.changes.map((change) => [
|
appliedForward.payload.changes.map((change) => [
|
||||||
change.field,
|
change.field,
|
||||||
getCircuitDeviceRowFieldValue(current, change.field),
|
getCircuitDeviceRowFieldValue(current, change.field),
|
||||||
])
|
])
|
||||||
) as CircuitDeviceRowUpdatePatch;
|
) as CircuitDeviceRowUpdatePatch;
|
||||||
const inverse = createCircuitDeviceRowUpdateProjectCommand(
|
const inverse = createCircuitDeviceRowUpdateProjectCommand(
|
||||||
current.id,
|
current.id,
|
||||||
inversePatch
|
inversePatch
|
||||||
);
|
);
|
||||||
|
|
||||||
const update = tx
|
const update = tx
|
||||||
.update(circuitDeviceRows)
|
.update(circuitDeviceRows)
|
||||||
.set(toCircuitDeviceRowPatchValues(patch))
|
.set(toCircuitDeviceRowPatchValues(patch))
|
||||||
.where(eq(circuitDeviceRows.id, current.id))
|
.where(eq(circuitDeviceRows.id, current.id))
|
||||||
.run();
|
.run();
|
||||||
if (update.changes !== 1) {
|
if (update.changes !== 1) {
|
||||||
throw new Error("Circuit device row changed before command execution.");
|
throw new Error("Circuit device row changed before command execution.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const revision = appendProjectRevision(tx, {
|
return {
|
||||||
projectId: input.projectId,
|
forward: appliedForward,
|
||||||
expectedRevision: input.expectedRevision,
|
inverse,
|
||||||
source: input.source,
|
};
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: appliedForward,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertLinkedProjectDevice(
|
private assertLinkedProjectDevice(
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import {
|
|||||||
assertCircuitDeviceRowReferencesInProject,
|
assertCircuitDeviceRowReferencesInProject,
|
||||||
toCircuitDeviceRowSnapshot,
|
toCircuitDeviceRowSnapshot,
|
||||||
} from "./circuit-device-row-structure.persistence.js";
|
} from "./circuit-device-row-structure.persistence.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
export class CircuitDeviceRowStructureProjectCommandRepository
|
export class CircuitDeviceRowStructureProjectCommandRepository
|
||||||
implements CircuitDeviceRowStructureProjectCommandStore
|
implements CircuitDeviceRowStructureProjectCommandStore
|
||||||
@@ -30,30 +29,17 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
|||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
execute(input: ExecuteCircuitDeviceRowStructureCommandInput) {
|
execute(input: ExecuteCircuitDeviceRowStructureCommandInput) {
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const inverse = this.applyCommand(
|
this.database,
|
||||||
tx,
|
input,
|
||||||
input.projectId,
|
(tx) =>
|
||||||
input.source,
|
this.applyCommand(
|
||||||
input.command
|
tx,
|
||||||
);
|
input.projectId,
|
||||||
const revision = appendProjectRevision(tx, {
|
input.source,
|
||||||
projectId: input.projectId,
|
input.command
|
||||||
expectedRevision: input.expectedRevision,
|
)
|
||||||
source: input.source,
|
);
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyCommand(
|
private applyCommand(
|
||||||
|
|||||||
@@ -1,38 +1,18 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import { and, asc, eq, inArray } from "drizzle-orm";
|
import { and, asc, eq, inArray } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import {
|
|
||||||
toCircuitDeviceRowCreateValues,
|
|
||||||
toCircuitDeviceRowUpdateValues,
|
|
||||||
type CircuitDeviceRowCreateInput,
|
|
||||||
type CircuitDeviceRowUpdateInput,
|
|
||||||
} from "./circuit-device-row.persistence.js";
|
|
||||||
export type {
|
|
||||||
CircuitDeviceRowCreateInput,
|
|
||||||
CircuitDeviceRowUpdateInput,
|
|
||||||
} from "./circuit-device-row.persistence.js";
|
|
||||||
export { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
|
|
||||||
|
|
||||||
export class CircuitDeviceRowRepository {
|
export class CircuitDeviceRowRepository {
|
||||||
async findById(rowId: string) {
|
constructor(private readonly database: AppDatabase) {}
|
||||||
const [row] = await db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId)).limit(1);
|
|
||||||
return row ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async countByCircuit(circuitId: string) {
|
|
||||||
const rows = await db.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows).where(eq(circuitDeviceRows.circuitId, circuitId));
|
|
||||||
return rows.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listByCircuitList(circuitIds: string[]) {
|
async listByCircuitList(circuitIds: string[]) {
|
||||||
if (!circuitIds.length) {
|
if (!circuitIds.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return db
|
return this.database
|
||||||
.select()
|
.select()
|
||||||
.from(circuitDeviceRows)
|
.from(circuitDeviceRows)
|
||||||
.where(inArray(circuitDeviceRows.circuitId, circuitIds))
|
.where(inArray(circuitDeviceRows.circuitId, circuitIds))
|
||||||
@@ -40,7 +20,7 @@ export class CircuitDeviceRowRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listLinkedByProjectDevice(projectId: string, projectDeviceId: string) {
|
async listLinkedByProjectDevice(projectId: string, projectDeviceId: string) {
|
||||||
return db
|
return this.database
|
||||||
.select({
|
.select({
|
||||||
id: circuitDeviceRows.id,
|
id: circuitDeviceRows.id,
|
||||||
circuitId: circuitDeviceRows.circuitId,
|
circuitId: circuitDeviceRows.circuitId,
|
||||||
@@ -83,34 +63,4 @@ export class CircuitDeviceRowRepository {
|
|||||||
.orderBy(asc(distributionBoards.name), asc(circuits.equipmentIdentifier), asc(circuitDeviceRows.sortOrder));
|
.orderBy(asc(distributionBoards.name), asc(circuits.equipmentIdentifier), asc(circuitDeviceRows.sortOrder));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(input: CircuitDeviceRowCreateInput) {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
await db.insert(circuitDeviceRows).values(toCircuitDeviceRowCreateValues(id, input));
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(
|
|
||||||
rowId: string,
|
|
||||||
input: CircuitDeviceRowUpdateInput
|
|
||||||
) {
|
|
||||||
await db
|
|
||||||
.update(circuitDeviceRows)
|
|
||||||
.set(toCircuitDeviceRowUpdateValues(input))
|
|
||||||
.where(eq(circuitDeviceRows.id, rowId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(rowId: string) {
|
|
||||||
await db.delete(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
|
|
||||||
await db
|
|
||||||
.update(circuitDeviceRows)
|
|
||||||
.set({
|
|
||||||
circuitId: targetCircuitId,
|
|
||||||
sortOrder,
|
|
||||||
})
|
|
||||||
.where(eq(circuitDeviceRows.id, rowId));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,17 @@
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
|
||||||
export class CircuitListRepository {
|
export class CircuitListRepository {
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
async listByProject(projectId: string) {
|
async listByProject(projectId: string) {
|
||||||
|
const db = this.database;
|
||||||
return db.select().from(circuitLists).where(eq(circuitLists.projectId, projectId));
|
return db.select().from(circuitLists).where(eq(circuitLists.projectId, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createForDistributionBoard(input: {
|
|
||||||
projectId: string;
|
|
||||||
distributionBoardId: string;
|
|
||||||
name: string;
|
|
||||||
}) {
|
|
||||||
const entry = {
|
|
||||||
id: input.distributionBoardId,
|
|
||||||
projectId: input.projectId,
|
|
||||||
distributionBoardId: input.distributionBoardId,
|
|
||||||
name: input.name,
|
|
||||||
};
|
|
||||||
await db.insert(circuitLists).values(entry);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByDistributionBoardId(projectId: string, distributionBoardId: string) {
|
|
||||||
const [row] = await db
|
|
||||||
.select()
|
|
||||||
.from(circuitLists)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(circuitLists.projectId, projectId),
|
|
||||||
eq(circuitLists.distributionBoardId, distributionBoardId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
return row ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async existsInProject(projectId: string, circuitListId: string) {
|
|
||||||
const [row] = await db
|
|
||||||
.select({ id: circuitLists.id })
|
|
||||||
.from(circuitLists)
|
|
||||||
.where(and(eq(circuitLists.projectId, projectId), eq(circuitLists.id, circuitListId)))
|
|
||||||
.limit(1);
|
|
||||||
return Boolean(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(projectId: string, circuitListId: string) {
|
async findById(projectId: string, circuitListId: string) {
|
||||||
|
const db = this.database;
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(circuitLists)
|
.from(circuitLists)
|
||||||
@@ -53,13 +19,4 @@ export class CircuitListRepository {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByIdByListIdOnly(circuitListId: string) {
|
|
||||||
const [row] = await db
|
|
||||||
.select()
|
|
||||||
.from(circuitLists)
|
|
||||||
.where(eq(circuitLists.id, circuitListId))
|
|
||||||
.limit(1);
|
|
||||||
return row ?? null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ import {
|
|||||||
toCircuitPatchValues,
|
toCircuitPatchValues,
|
||||||
type CircuitPatchPersistenceInput,
|
type CircuitPatchPersistenceInput,
|
||||||
} from "./circuit.persistence.js";
|
} from "./circuit.persistence.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
type CircuitRow = typeof circuits.$inferSelect;
|
type CircuitRow = typeof circuits.$inferSelect;
|
||||||
|
|
||||||
@@ -31,81 +30,74 @@ export class CircuitProjectCommandRepository
|
|||||||
executeUpdate(input: ExecuteCircuitUpdateCommandInput) {
|
executeUpdate(input: ExecuteCircuitUpdateCommandInput) {
|
||||||
assertCircuitUpdateProjectCommand(input.command);
|
assertCircuitUpdateProjectCommand(input.command);
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const current = tx
|
this.database,
|
||||||
.select()
|
input,
|
||||||
.from(circuits)
|
(tx) => this.applyCommand(tx, input)
|
||||||
.where(eq(circuits.id, input.command.payload.circuitId))
|
);
|
||||||
.get();
|
}
|
||||||
if (!current) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const owningList = tx
|
private applyCommand(
|
||||||
.select({ id: circuitLists.id })
|
tx: AppDatabase,
|
||||||
.from(circuitLists)
|
input: ExecuteCircuitUpdateCommandInput
|
||||||
.where(
|
) {
|
||||||
and(
|
const current = tx
|
||||||
eq(circuitLists.id, current.circuitListId),
|
.select()
|
||||||
eq(circuitLists.projectId, input.projectId)
|
.from(circuits)
|
||||||
)
|
.where(eq(circuits.id, input.command.payload.circuitId))
|
||||||
|
.get();
|
||||||
|
if (!current) {
|
||||||
|
throw new Error("Invalid circuit id.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const owningList = tx
|
||||||
|
.select({ id: circuitLists.id })
|
||||||
|
.from(circuitLists)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(circuitLists.id, current.circuitListId),
|
||||||
|
eq(circuitLists.projectId, input.projectId)
|
||||||
)
|
)
|
||||||
.get();
|
)
|
||||||
if (!owningList) {
|
.get();
|
||||||
throw new Error("Circuit does not belong to project.");
|
if (!owningList) {
|
||||||
}
|
throw new Error("Circuit does not belong to project.");
|
||||||
|
}
|
||||||
|
|
||||||
const patch = Object.fromEntries(
|
const patch = Object.fromEntries(
|
||||||
input.command.payload.changes.map((change) => [
|
input.command.payload.changes.map((change) => [
|
||||||
change.field,
|
change.field,
|
||||||
change.value,
|
change.value,
|
||||||
])
|
])
|
||||||
) as CircuitPatchPersistenceInput;
|
) as CircuitPatchPersistenceInput;
|
||||||
this.assertSectionInCircuitList(tx, current, patch.sectionId);
|
this.assertSectionInCircuitList(tx, current, patch.sectionId);
|
||||||
this.assertUniqueEquipmentIdentifier(
|
this.assertUniqueEquipmentIdentifier(
|
||||||
tx,
|
tx,
|
||||||
current,
|
current,
|
||||||
patch.equipmentIdentifier
|
patch.equipmentIdentifier
|
||||||
);
|
);
|
||||||
|
|
||||||
const inversePatch = Object.fromEntries(
|
const inversePatch = Object.fromEntries(
|
||||||
input.command.payload.changes.map((change) => [
|
input.command.payload.changes.map((change) => [
|
||||||
change.field,
|
change.field,
|
||||||
getCircuitFieldValue(current, change.field),
|
getCircuitFieldValue(current, change.field),
|
||||||
])
|
])
|
||||||
) as CircuitUpdatePatch;
|
) as CircuitUpdatePatch;
|
||||||
const inverse = createCircuitUpdateProjectCommand(
|
const inverse = createCircuitUpdateProjectCommand(
|
||||||
current.id,
|
current.id,
|
||||||
inversePatch
|
inversePatch
|
||||||
);
|
);
|
||||||
|
|
||||||
const update = tx
|
const update = tx
|
||||||
.update(circuits)
|
.update(circuits)
|
||||||
.set(toCircuitPatchValues(patch))
|
.set(toCircuitPatchValues(patch))
|
||||||
.where(eq(circuits.id, current.id))
|
.where(eq(circuits.id, current.id))
|
||||||
.run();
|
.run();
|
||||||
if (update.changes !== 1) {
|
if (update.changes !== 1) {
|
||||||
throw new Error("Circuit changed before command execution.");
|
throw new Error("Circuit changed before command execution.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const revision = appendProjectRevision(tx, {
|
return inverse;
|
||||||
projectId: input.projectId,
|
|
||||||
expectedRevision: input.expectedRevision,
|
|
||||||
source: input.source,
|
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertSectionInCircuitList(
|
private assertSectionInCircuitList(
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import type { AppDatabase } from "../database-context.js";
|
|||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
export class CircuitSectionRenumberProjectCommandRepository
|
export class CircuitSectionRenumberProjectCommandRepository
|
||||||
implements CircuitSectionRenumberProjectCommandStore
|
implements CircuitSectionRenumberProjectCommandStore
|
||||||
@@ -23,190 +22,184 @@ export class CircuitSectionRenumberProjectCommandRepository
|
|||||||
execute(input: ExecuteCircuitSectionRenumberCommandInput) {
|
execute(input: ExecuteCircuitSectionRenumberCommandInput) {
|
||||||
assertCircuitSectionRenumberProjectCommand(input.command);
|
assertCircuitSectionRenumberProjectCommand(input.command);
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const section = tx
|
this.database,
|
||||||
.select({
|
input,
|
||||||
id: circuitSections.id,
|
(tx) => this.applyCommand(tx, input)
|
||||||
circuitListId: circuitSections.circuitListId,
|
);
|
||||||
projectId: circuitLists.projectId,
|
}
|
||||||
})
|
|
||||||
.from(circuitSections)
|
|
||||||
.innerJoin(
|
|
||||||
circuitLists,
|
|
||||||
eq(circuitLists.id, circuitSections.circuitListId)
|
|
||||||
)
|
|
||||||
.where(eq(circuitSections.id, input.command.payload.sectionId))
|
|
||||||
.get();
|
|
||||||
if (!section || section.projectId !== input.projectId) {
|
|
||||||
throw new Error(
|
|
||||||
"Circuit section does not belong to project."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sectionCircuits = tx
|
private applyCommand(
|
||||||
.select({
|
tx: AppDatabase,
|
||||||
id: circuits.id,
|
input: ExecuteCircuitSectionRenumberCommandInput
|
||||||
circuitListId: circuits.circuitListId,
|
) {
|
||||||
equipmentIdentifier: circuits.equipmentIdentifier,
|
const section = tx
|
||||||
})
|
.select({
|
||||||
.from(circuits)
|
id: circuitSections.id,
|
||||||
.where(eq(circuits.sectionId, section.id))
|
circuitListId: circuitSections.circuitListId,
|
||||||
.all();
|
projectId: circuitLists.projectId,
|
||||||
const assignments = input.command.payload.assignments;
|
})
|
||||||
|
.from(circuitSections)
|
||||||
|
.innerJoin(
|
||||||
|
circuitLists,
|
||||||
|
eq(circuitLists.id, circuitSections.circuitListId)
|
||||||
|
)
|
||||||
|
.where(eq(circuitSections.id, input.command.payload.sectionId))
|
||||||
|
.get();
|
||||||
|
if (!section || section.projectId !== input.projectId) {
|
||||||
|
throw new Error(
|
||||||
|
"Circuit section does not belong to project."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionCircuits = tx
|
||||||
|
.select({
|
||||||
|
id: circuits.id,
|
||||||
|
circuitListId: circuits.circuitListId,
|
||||||
|
equipmentIdentifier: circuits.equipmentIdentifier,
|
||||||
|
})
|
||||||
|
.from(circuits)
|
||||||
|
.where(eq(circuits.sectionId, section.id))
|
||||||
|
.all();
|
||||||
|
const assignments = input.command.payload.assignments;
|
||||||
|
if (
|
||||||
|
sectionCircuits.length !== assignments.length ||
|
||||||
|
sectionCircuits.some(
|
||||||
|
(circuit) =>
|
||||||
|
circuit.circuitListId !== section.circuitListId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Circuit renumber must include every circuit in the section."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const circuitsById = new Map(
|
||||||
|
sectionCircuits.map((circuit) => [circuit.id, circuit])
|
||||||
|
);
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
const circuit = circuitsById.get(assignment.circuitId);
|
||||||
if (
|
if (
|
||||||
sectionCircuits.length !== assignments.length ||
|
!circuit ||
|
||||||
sectionCircuits.some(
|
circuit.equipmentIdentifier !==
|
||||||
(circuit) =>
|
assignment.expectedEquipmentIdentifier
|
||||||
circuit.circuitListId !== section.circuitListId
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Circuit renumber must include every circuit in the section."
|
"Circuit changed before section renumber execution."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const circuitsById = new Map(
|
}
|
||||||
sectionCircuits.map((circuit) => [circuit.id, circuit])
|
|
||||||
);
|
|
||||||
for (const assignment of assignments) {
|
|
||||||
const circuit = circuitsById.get(assignment.circuitId);
|
|
||||||
if (
|
|
||||||
!circuit ||
|
|
||||||
circuit.equipmentIdentifier !==
|
|
||||||
assignment.expectedEquipmentIdentifier
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"Circuit changed before section renumber execution."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const listCircuits = tx
|
const listCircuits = tx
|
||||||
.select({
|
.select({
|
||||||
id: circuits.id,
|
id: circuits.id,
|
||||||
equipmentIdentifier: circuits.equipmentIdentifier,
|
equipmentIdentifier: circuits.equipmentIdentifier,
|
||||||
})
|
})
|
||||||
.from(circuits)
|
.from(circuits)
|
||||||
.where(eq(circuits.circuitListId, section.circuitListId))
|
.where(eq(circuits.circuitListId, section.circuitListId))
|
||||||
.all();
|
.all();
|
||||||
const sectionCircuitIds = new Set(
|
const sectionCircuitIds = new Set(
|
||||||
assignments.map((assignment) => assignment.circuitId)
|
assignments.map((assignment) => assignment.circuitId)
|
||||||
|
);
|
||||||
|
const targetIdentifiers = new Set(
|
||||||
|
assignments.map(
|
||||||
|
(assignment) => assignment.targetEquipmentIdentifier
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const conflictingCircuit = listCircuits.find(
|
||||||
|
(circuit) =>
|
||||||
|
!sectionCircuitIds.has(circuit.id) &&
|
||||||
|
targetIdentifiers.has(circuit.equipmentIdentifier)
|
||||||
|
);
|
||||||
|
if (conflictingCircuit) {
|
||||||
|
throw new Error(
|
||||||
|
"Target equipment identifier already exists in circuit list."
|
||||||
);
|
);
|
||||||
const targetIdentifiers = new Set(
|
}
|
||||||
assignments.map(
|
|
||||||
(assignment) => assignment.targetEquipmentIdentifier
|
const inverse = createCircuitSectionRenumberProjectCommand(
|
||||||
|
section.id,
|
||||||
|
assignments.map((assignment) => ({
|
||||||
|
circuitId: assignment.circuitId,
|
||||||
|
expectedEquipmentIdentifier:
|
||||||
|
assignment.targetEquipmentIdentifier,
|
||||||
|
targetEquipmentIdentifier:
|
||||||
|
assignment.expectedEquipmentIdentifier,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const temporaryIdentifiers = this.createTemporaryIdentifiers(
|
||||||
|
section.id,
|
||||||
|
assignments,
|
||||||
|
listCircuits.map((circuit) => circuit.equipmentIdentifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
if (
|
||||||
|
assignment.expectedEquipmentIdentifier ===
|
||||||
|
assignment.targetEquipmentIdentifier
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const temporaryIdentifier = temporaryIdentifiers.get(
|
||||||
|
assignment.circuitId
|
||||||
|
)!;
|
||||||
|
const updated = tx
|
||||||
|
.update(circuits)
|
||||||
|
.set({ equipmentIdentifier: temporaryIdentifier })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(circuits.id, assignment.circuitId),
|
||||||
|
eq(circuits.sectionId, section.id),
|
||||||
|
eq(circuits.circuitListId, section.circuitListId),
|
||||||
|
eq(
|
||||||
|
circuits.equipmentIdentifier,
|
||||||
|
assignment.expectedEquipmentIdentifier
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
.run();
|
||||||
const conflictingCircuit = listCircuits.find(
|
if (updated.changes !== 1) {
|
||||||
(circuit) =>
|
|
||||||
!sectionCircuitIds.has(circuit.id) &&
|
|
||||||
targetIdentifiers.has(circuit.equipmentIdentifier)
|
|
||||||
);
|
|
||||||
if (conflictingCircuit) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Target equipment identifier already exists in circuit list."
|
"Circuit changed during section renumber execution."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const inverse = createCircuitSectionRenumberProjectCommand(
|
for (const assignment of assignments) {
|
||||||
section.id,
|
if (
|
||||||
assignments.map((assignment) => ({
|
assignment.expectedEquipmentIdentifier ===
|
||||||
circuitId: assignment.circuitId,
|
assignment.targetEquipmentIdentifier
|
||||||
expectedEquipmentIdentifier:
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const temporaryIdentifier = temporaryIdentifiers.get(
|
||||||
|
assignment.circuitId
|
||||||
|
)!;
|
||||||
|
const updated = tx
|
||||||
|
.update(circuits)
|
||||||
|
.set({
|
||||||
|
equipmentIdentifier:
|
||||||
assignment.targetEquipmentIdentifier,
|
assignment.targetEquipmentIdentifier,
|
||||||
targetEquipmentIdentifier:
|
})
|
||||||
assignment.expectedEquipmentIdentifier,
|
.where(
|
||||||
}))
|
and(
|
||||||
);
|
eq(circuits.id, assignment.circuitId),
|
||||||
const temporaryIdentifiers = this.createTemporaryIdentifiers(
|
eq(circuits.sectionId, section.id),
|
||||||
section.id,
|
eq(circuits.circuitListId, section.circuitListId),
|
||||||
assignments,
|
eq(
|
||||||
listCircuits.map((circuit) => circuit.equipmentIdentifier)
|
circuits.equipmentIdentifier,
|
||||||
);
|
temporaryIdentifier
|
||||||
|
|
||||||
for (const assignment of assignments) {
|
|
||||||
if (
|
|
||||||
assignment.expectedEquipmentIdentifier ===
|
|
||||||
assignment.targetEquipmentIdentifier
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const temporaryIdentifier = temporaryIdentifiers.get(
|
|
||||||
assignment.circuitId
|
|
||||||
)!;
|
|
||||||
const updated = tx
|
|
||||||
.update(circuits)
|
|
||||||
.set({ equipmentIdentifier: temporaryIdentifier })
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(circuits.id, assignment.circuitId),
|
|
||||||
eq(circuits.sectionId, section.id),
|
|
||||||
eq(circuits.circuitListId, section.circuitListId),
|
|
||||||
eq(
|
|
||||||
circuits.equipmentIdentifier,
|
|
||||||
assignment.expectedEquipmentIdentifier
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.run();
|
)
|
||||||
if (updated.changes !== 1) {
|
.run();
|
||||||
throw new Error(
|
if (updated.changes !== 1) {
|
||||||
"Circuit changed during section renumber execution."
|
throw new Error(
|
||||||
);
|
"Circuit changed during final section renumber execution."
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const assignment of assignments) {
|
return inverse;
|
||||||
if (
|
|
||||||
assignment.expectedEquipmentIdentifier ===
|
|
||||||
assignment.targetEquipmentIdentifier
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const temporaryIdentifier = temporaryIdentifiers.get(
|
|
||||||
assignment.circuitId
|
|
||||||
)!;
|
|
||||||
const updated = tx
|
|
||||||
.update(circuits)
|
|
||||||
.set({
|
|
||||||
equipmentIdentifier:
|
|
||||||
assignment.targetEquipmentIdentifier,
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(circuits.id, assignment.circuitId),
|
|
||||||
eq(circuits.sectionId, section.id),
|
|
||||||
eq(circuits.circuitListId, section.circuitListId),
|
|
||||||
eq(
|
|
||||||
circuits.equipmentIdentifier,
|
|
||||||
temporaryIdentifier
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
if (updated.changes !== 1) {
|
|
||||||
throw new Error(
|
|
||||||
"Circuit changed during final section renumber execution."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const revision = appendProjectRevision(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
expectedRevision: input.expectedRevision,
|
|
||||||
source: input.source,
|
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTemporaryIdentifiers(
|
private createTemporaryIdentifiers(
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import type { AppDatabase } from "../database-context.js";
|
|||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
type ReorderHistoryCommand =
|
type ReorderHistoryCommand =
|
||||||
| CircuitSectionReorderProjectCommand
|
| CircuitSectionReorderProjectCommand
|
||||||
@@ -36,54 +35,48 @@ export class CircuitSectionReorderProjectCommandRepository
|
|||||||
execute(input: ExecuteCircuitSectionReorderCommandInput) {
|
execute(input: ExecuteCircuitSectionReorderCommandInput) {
|
||||||
this.assertSupportedCommand(input.command);
|
this.assertSupportedCommand(input.command);
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const commandSections = this.getCommandSections(input.command);
|
this.database,
|
||||||
const inverseSections = commandSections.map((section) => {
|
input,
|
||||||
this.assertCompleteCurrentSection(
|
(tx) => this.applyCommand(tx, input)
|
||||||
tx,
|
);
|
||||||
input.projectId,
|
}
|
||||||
section
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
sectionId: section.sectionId,
|
|
||||||
assignments: section.assignments.map((assignment) => ({
|
|
||||||
circuitId: assignment.circuitId,
|
|
||||||
expectedSortOrder: assignment.targetSortOrder,
|
|
||||||
targetSortOrder: assignment.expectedSortOrder,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const section of commandSections) {
|
private applyCommand(
|
||||||
this.applyAssignments(tx, section);
|
tx: AppDatabase,
|
||||||
}
|
input: ExecuteCircuitSectionReorderCommandInput
|
||||||
|
) {
|
||||||
const inverse =
|
const commandSections = this.getCommandSections(input.command);
|
||||||
input.command.type === circuitSectionReorderCommandType
|
const inverseSections = commandSections.map((section) => {
|
||||||
? createCircuitSectionReorderProjectCommand(
|
this.assertCompleteCurrentSection(
|
||||||
inverseSections[0].sectionId,
|
tx,
|
||||||
inverseSections[0].assignments
|
input.projectId,
|
||||||
)
|
section
|
||||||
: createCircuitSectionsReorderProjectCommand(
|
);
|
||||||
inverseSections
|
return {
|
||||||
);
|
sectionId: section.sectionId,
|
||||||
const revision = appendProjectRevision(tx, {
|
assignments: section.assignments.map((assignment) => ({
|
||||||
projectId: input.projectId,
|
circuitId: assignment.circuitId,
|
||||||
expectedRevision: input.expectedRevision,
|
expectedSortOrder: assignment.targetSortOrder,
|
||||||
source: input.source,
|
targetSortOrder: assignment.expectedSortOrder,
|
||||||
description: input.description,
|
})),
|
||||||
actorId: input.actorId,
|
};
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const section of commandSections) {
|
||||||
|
this.applyAssignments(tx, section);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inverse =
|
||||||
|
input.command.type === circuitSectionReorderCommandType
|
||||||
|
? createCircuitSectionReorderProjectCommand(
|
||||||
|
inverseSections[0].sectionId,
|
||||||
|
inverseSections[0].assignments
|
||||||
|
)
|
||||||
|
: createCircuitSectionsReorderProjectCommand(
|
||||||
|
inverseSections
|
||||||
|
);
|
||||||
|
return inverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertSupportedCommand(
|
private assertSupportedCommand(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
import { and, asc, eq } from "drizzle-orm";
|
||||||
import { defaultCircuitSectionDefinitions } from "../../domain/models/distribution-board-structure-project-command.model.js";
|
import { defaultCircuitSectionDefinitions } from "../../domain/models/distribution-board-structure-project-command.model.js";
|
||||||
import { db } from "../client.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
|
|
||||||
export function createDefaultCircuitSectionValues(circuitListId: string) {
|
export function createDefaultCircuitSectionValues(circuitListId: string) {
|
||||||
@@ -13,12 +13,16 @@ export function createDefaultCircuitSectionValues(circuitListId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CircuitSectionRepository {
|
export class CircuitSectionRepository {
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
async findById(sectionId: string) {
|
async findById(sectionId: string) {
|
||||||
|
const db = this.database;
|
||||||
const [row] = await db.select().from(circuitSections).where(eq(circuitSections.id, sectionId)).limit(1);
|
const [row] = await db.select().from(circuitSections).where(eq(circuitSections.id, sectionId)).limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listByCircuitList(circuitListId: string) {
|
async listByCircuitList(circuitListId: string) {
|
||||||
|
const db = this.database;
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
.from(circuitSections)
|
.from(circuitSections)
|
||||||
@@ -27,6 +31,7 @@ export class CircuitSectionRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createDefaults(circuitListId: string) {
|
async createDefaults(circuitListId: string) {
|
||||||
|
const db = this.database;
|
||||||
for (const entry of createDefaultCircuitSectionValues(circuitListId)) {
|
for (const entry of createDefaultCircuitSectionValues(circuitListId)) {
|
||||||
const existing = await db
|
const existing = await db
|
||||||
.select({ id: circuitSections.id })
|
.select({ id: circuitSections.id })
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ import {
|
|||||||
assertCircuitDeviceRowReferencesInProject,
|
assertCircuitDeviceRowReferencesInProject,
|
||||||
toCircuitDeviceRowSnapshot,
|
toCircuitDeviceRowSnapshot,
|
||||||
} from "./circuit-device-row-structure.persistence.js";
|
} from "./circuit-device-row-structure.persistence.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
export class CircuitStructureProjectCommandRepository
|
export class CircuitStructureProjectCommandRepository
|
||||||
implements CircuitStructureProjectCommandStore
|
implements CircuitStructureProjectCommandStore
|
||||||
@@ -31,30 +30,17 @@ export class CircuitStructureProjectCommandRepository
|
|||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
execute(input: ExecuteCircuitStructureCommandInput) {
|
execute(input: ExecuteCircuitStructureCommandInput) {
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const inverse = this.applyCommand(
|
this.database,
|
||||||
tx,
|
input,
|
||||||
input.projectId,
|
(tx) =>
|
||||||
input.source,
|
this.applyCommand(
|
||||||
input.command
|
tx,
|
||||||
);
|
input.projectId,
|
||||||
const revision = appendProjectRevision(tx, {
|
input.source,
|
||||||
projectId: input.projectId,
|
input.command
|
||||||
expectedRevision: input.expectedRevision,
|
)
|
||||||
source: input.source,
|
);
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyCommand(
|
private applyCommand(
|
||||||
|
|||||||
@@ -1,26 +1,12 @@
|
|||||||
import crypto from "node:crypto";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { and, asc, eq, ne } from "drizzle-orm";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { db } from "../client.js";
|
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import {
|
|
||||||
toCircuitCreateValues,
|
|
||||||
type CircuitCreatePersistenceInput,
|
|
||||||
} from "./circuit.persistence.js";
|
|
||||||
|
|
||||||
export type {
|
|
||||||
CircuitCreatePersistenceInput,
|
|
||||||
} from "./circuit.persistence.js";
|
|
||||||
export {
|
|
||||||
toCircuitCreateValues,
|
|
||||||
} from "./circuit.persistence.js";
|
|
||||||
|
|
||||||
export class CircuitRepository {
|
export class CircuitRepository {
|
||||||
async findById(circuitId: string) {
|
constructor(private readonly database: AppDatabase) {}
|
||||||
const [row] = await db.select().from(circuits).where(eq(circuits.id, circuitId)).limit(1);
|
|
||||||
return row ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listByCircuitList(circuitListId: string) {
|
async listByCircuitList(circuitListId: string) {
|
||||||
|
const db = this.database;
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
.from(circuits)
|
.from(circuits)
|
||||||
@@ -28,85 +14,12 @@ export class CircuitRepository {
|
|||||||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(input: CircuitCreatePersistenceInput) {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
await db.insert(circuits).values(toCircuitCreateValues(id, input));
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(
|
|
||||||
circuitId: string,
|
|
||||||
input: {
|
|
||||||
sectionId: string;
|
|
||||||
equipmentIdentifier: string;
|
|
||||||
displayName?: string;
|
|
||||||
sortOrder: number;
|
|
||||||
protectionType?: string;
|
|
||||||
protectionRatedCurrent?: number;
|
|
||||||
protectionCharacteristic?: string;
|
|
||||||
cableType?: string;
|
|
||||||
cableCrossSection?: string;
|
|
||||||
cableLength?: number;
|
|
||||||
rcdAssignment?: string;
|
|
||||||
terminalDesignation?: string;
|
|
||||||
voltage?: number;
|
|
||||||
controlRequirement?: string;
|
|
||||||
status?: string;
|
|
||||||
isReserve: boolean;
|
|
||||||
remark?: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
await db
|
|
||||||
.update(circuits)
|
|
||||||
.set({
|
|
||||||
sectionId: input.sectionId,
|
|
||||||
equipmentIdentifier: input.equipmentIdentifier,
|
|
||||||
displayName: input.displayName ?? null,
|
|
||||||
sortOrder: input.sortOrder,
|
|
||||||
protectionType: input.protectionType ?? null,
|
|
||||||
protectionRatedCurrent: input.protectionRatedCurrent ?? null,
|
|
||||||
protectionCharacteristic: input.protectionCharacteristic ?? null,
|
|
||||||
cableType: input.cableType ?? null,
|
|
||||||
cableCrossSection: input.cableCrossSection ?? null,
|
|
||||||
cableLength: input.cableLength ?? null,
|
|
||||||
rcdAssignment: input.rcdAssignment ?? null,
|
|
||||||
terminalDesignation: input.terminalDesignation ?? null,
|
|
||||||
voltage: input.voltage ?? null,
|
|
||||||
controlRequirement: input.controlRequirement ?? null,
|
|
||||||
status: input.status ?? null,
|
|
||||||
isReserve: input.isReserve ? 1 : 0,
|
|
||||||
remark: input.remark ?? null,
|
|
||||||
})
|
|
||||||
.where(eq(circuits.id, circuitId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(circuitId: string) {
|
|
||||||
await db.delete(circuits).where(eq(circuits.id, circuitId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async existsByEquipmentIdentifier(circuitListId: string, equipmentIdentifier: string, excludeCircuitId?: string) {
|
|
||||||
const rows = await db
|
|
||||||
.select({ id: circuits.id })
|
|
||||||
.from(circuits)
|
|
||||||
.where(
|
|
||||||
excludeCircuitId
|
|
||||||
? and(
|
|
||||||
eq(circuits.circuitListId, circuitListId),
|
|
||||||
eq(circuits.equipmentIdentifier, equipmentIdentifier),
|
|
||||||
ne(circuits.id, excludeCircuitId)
|
|
||||||
)
|
|
||||||
: and(eq(circuits.circuitListId, circuitListId), eq(circuits.equipmentIdentifier, equipmentIdentifier))
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
return Boolean(rows.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listBySection(sectionId: string) {
|
async listBySection(sectionId: string) {
|
||||||
|
const db = this.database;
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
.from(circuits)
|
.from(circuits)
|
||||||
.where(eq(circuits.sectionId, sectionId))
|
.where(eq(circuits.sectionId, sectionId))
|
||||||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import { circuitSections } from "../schema/circuit-sections.js";
|
|||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
export class DistributionBoardStructureProjectCommandRepository
|
export class DistributionBoardStructureProjectCommandRepository
|
||||||
implements DistributionBoardStructureProjectCommandStore
|
implements DistributionBoardStructureProjectCommandStore
|
||||||
@@ -28,29 +27,12 @@ export class DistributionBoardStructureProjectCommandRepository
|
|||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
execute(input: ExecuteDistributionBoardStructureCommandInput) {
|
execute(input: ExecuteDistributionBoardStructureCommandInput) {
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const inverse = this.applyCommand(
|
this.database,
|
||||||
tx,
|
input,
|
||||||
input.projectId,
|
(tx) =>
|
||||||
input.command
|
this.applyCommand(tx, input.projectId, input.command)
|
||||||
);
|
);
|
||||||
const revision = appendProjectRevision(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
expectedRevision: input.expectedRevision,
|
|
||||||
source: input.source,
|
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyCommand(
|
private applyCommand(
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import crypto from "node:crypto";
|
import { eq } from "drizzle-orm";
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { createDefaultCircuitSectionValues } from "./circuit-section.repository.js";
|
|
||||||
|
|
||||||
export class DistributionBoardRepository {
|
export class DistributionBoardRepository {
|
||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
@@ -16,44 +12,4 @@ export class DistributionBoardRepository {
|
|||||||
.where(eq(distributionBoards.projectId, projectId));
|
.where(eq(distributionBoards.projectId, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(projectId: string, name: string) {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const board = { id, projectId, name };
|
|
||||||
await this.database.insert(distributionBoards).values(board);
|
|
||||||
return board;
|
|
||||||
}
|
|
||||||
|
|
||||||
createWithCircuitListAndDefaultSections(projectId: string, name: string) {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const board = { id, projectId, name };
|
|
||||||
const circuitList = {
|
|
||||||
id,
|
|
||||||
projectId,
|
|
||||||
distributionBoardId: id,
|
|
||||||
name: `${name} Stromkreisliste`,
|
|
||||||
};
|
|
||||||
const sections = createDefaultCircuitSectionValues(id);
|
|
||||||
|
|
||||||
this.database.transaction((tx) => {
|
|
||||||
tx.insert(distributionBoards).values(board).run();
|
|
||||||
tx.insert(circuitLists).values(circuitList).run();
|
|
||||||
tx.insert(circuitSections).values(sections).run();
|
|
||||||
});
|
|
||||||
|
|
||||||
return board;
|
|
||||||
}
|
|
||||||
|
|
||||||
async existsInProject(projectId: string, distributionBoardId: string) {
|
|
||||||
const [row] = await this.database
|
|
||||||
.select({ id: distributionBoards.id })
|
|
||||||
.from(distributionBoards)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(distributionBoards.projectId, projectId),
|
|
||||||
eq(distributionBoards.id, distributionBoardId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
return Boolean(row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,16 @@
|
|||||||
import crypto from "node:crypto";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { db } from "../client.js";
|
|
||||||
import { floors } from "../schema/floors.js";
|
import { floors } from "../schema/floors.js";
|
||||||
|
|
||||||
export class FloorRepository {
|
export class FloorRepository {
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
async listByProject(projectId: string) {
|
async listByProject(projectId: string) {
|
||||||
|
const db = this.database;
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
.from(floors)
|
.from(floors)
|
||||||
.where(eq(floors.projectId, projectId))
|
.where(eq(floors.projectId, projectId))
|
||||||
.orderBy(asc(floors.sortOrder), asc(floors.name));
|
.orderBy(asc(floors.sortOrder), asc(floors.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(projectId: string, name: string) {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const existing = await this.listByProject(projectId);
|
|
||||||
const floor = {
|
|
||||||
id,
|
|
||||||
projectId,
|
|
||||||
name,
|
|
||||||
sortOrder: existing.length,
|
|
||||||
};
|
|
||||||
await db.insert(floors).values(floor);
|
|
||||||
return floor;
|
|
||||||
}
|
|
||||||
|
|
||||||
async existsInProject(projectId: string, floorId: string) {
|
|
||||||
const [row] = await db
|
|
||||||
.select({ id: floors.id })
|
|
||||||
.from(floors)
|
|
||||||
.where(and(eq(floors.projectId, projectId), eq(floors.id, floorId)))
|
|
||||||
.limit(1);
|
|
||||||
return Boolean(row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { globalDevices } from "../schema/global-devices.js";
|
import { globalDevices } from "../schema/global-devices.js";
|
||||||
import type {
|
import type {
|
||||||
CreateGlobalDeviceInput,
|
CreateGlobalDeviceInput,
|
||||||
@@ -8,11 +8,15 @@ import type {
|
|||||||
} from "../../shared/validation/global-device.schemas.js";
|
} from "../../shared/validation/global-device.schemas.js";
|
||||||
|
|
||||||
export class GlobalDeviceRepository {
|
export class GlobalDeviceRepository {
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
async list() {
|
async list() {
|
||||||
|
const db = this.database;
|
||||||
return db.select().from(globalDevices);
|
return db.select().from(globalDevices);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(input: CreateGlobalDeviceInput) {
|
async create(input: CreateGlobalDeviceInput) {
|
||||||
|
const db = this.database;
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
await db.insert(globalDevices).values({
|
await db.insert(globalDevices).values({
|
||||||
id,
|
id,
|
||||||
@@ -31,6 +35,7 @@ export class GlobalDeviceRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(globalDeviceId: string, input: UpdateGlobalDeviceInput) {
|
async update(globalDeviceId: string, input: UpdateGlobalDeviceInput) {
|
||||||
|
const db = this.database;
|
||||||
await db
|
await db
|
||||||
.update(globalDevices)
|
.update(globalDevices)
|
||||||
.set({
|
.set({
|
||||||
@@ -49,11 +54,13 @@ export class GlobalDeviceRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findById(globalDeviceId: string) {
|
async findById(globalDeviceId: string) {
|
||||||
|
const db = this.database;
|
||||||
const [row] = await db.select().from(globalDevices).where(eq(globalDevices.id, globalDeviceId)).limit(1);
|
const [row] = await db.select().from(globalDevices).where(eq(globalDevices.id, globalDeviceId)).limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(globalDeviceId: string) {
|
async delete(globalDeviceId: string) {
|
||||||
|
const db = this.database;
|
||||||
await db.delete(globalDevices).where(eq(globalDevices.id, globalDeviceId));
|
await db.delete(globalDevices).where(eq(globalDevices.id, globalDeviceId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,17 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { eq, inArray, isNull } from "drizzle-orm";
|
import { eq, inArray, isNull } from "drizzle-orm";
|
||||||
|
import type {
|
||||||
|
LegacyMigrationCircuitInput,
|
||||||
|
LegacyMigrationReportInput,
|
||||||
|
} from "../../domain/ports/legacy-consumer-migration.store.js";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { consumers } from "../schema/consumers.js";
|
import { consumers } from "../schema/consumers.js";
|
||||||
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
|
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
|
||||||
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
|
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
|
||||||
import {
|
import { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
|
||||||
toCircuitDeviceRowCreateValues,
|
import { toCircuitCreateValues } from "./circuit.persistence.js";
|
||||||
type CircuitDeviceRowCreateInput,
|
|
||||||
} from "./circuit-device-row.persistence.js";
|
|
||||||
import {
|
|
||||||
toCircuitCreateValues,
|
|
||||||
type CircuitCreatePersistenceInput,
|
|
||||||
} from "./circuit.persistence.js";
|
|
||||||
|
|
||||||
export interface LegacyMigrationCircuitPersistenceInput {
|
|
||||||
circuit: CircuitCreatePersistenceInput;
|
|
||||||
deviceRows: Array<
|
|
||||||
Omit<CircuitDeviceRowCreateInput, "circuitId"> & { legacyConsumerId: string }
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LegacyMigrationReportPersistenceInput {
|
|
||||||
legacyConsumerCount: number;
|
|
||||||
createdCircuitCount: number;
|
|
||||||
createdDeviceRowCount: number;
|
|
||||||
duplicateGroupedCount: number;
|
|
||||||
generatedIdentifierCount: number;
|
|
||||||
unassignedRowCount: number;
|
|
||||||
warningsJson: string;
|
|
||||||
generatedIdentifiersJson: string;
|
|
||||||
duplicateGroupsJson: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LegacyConsumerMigrationRepository {
|
export class LegacyConsumerMigrationRepository {
|
||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
@@ -69,8 +48,8 @@ export class LegacyConsumerMigrationRepository {
|
|||||||
|
|
||||||
persistCircuitListMigration(input: {
|
persistCircuitListMigration(input: {
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
circuits: LegacyMigrationCircuitPersistenceInput[];
|
circuits: LegacyMigrationCircuitInput[];
|
||||||
report: LegacyMigrationReportPersistenceInput;
|
report: LegacyMigrationReportInput;
|
||||||
}) {
|
}) {
|
||||||
if (input.circuits.some((entry) => entry.circuit.circuitListId !== input.circuitListId)) {
|
if (input.circuits.some((entry) => entry.circuit.circuitListId !== input.circuitListId)) {
|
||||||
throw new Error("All migrated circuits must belong to the target circuit list.");
|
throw new Error("All migrated circuits must belong to the target circuit list.");
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import type { SerializedProjectCommand } from "../../domain/models/project-command.model.js";
|
||||||
|
import type {
|
||||||
|
AppendedProjectRevision,
|
||||||
|
ProjectRevisionSource,
|
||||||
|
} from "../../domain/ports/project-revision.store.js";
|
||||||
|
import type { AppDatabase } from "../database-context.js";
|
||||||
|
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
||||||
|
import { appendProjectRevision } from "./project-revision.persistence.js";
|
||||||
|
|
||||||
|
export interface ProjectCommandTransactionInput<
|
||||||
|
Command extends SerializedProjectCommand<unknown>,
|
||||||
|
> {
|
||||||
|
projectId: string;
|
||||||
|
expectedRevision: number;
|
||||||
|
source: ProjectRevisionSource;
|
||||||
|
description?: string;
|
||||||
|
actorId?: string;
|
||||||
|
historyTargetChangeSetId?: string;
|
||||||
|
command: Command;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppliedProjectCommand<
|
||||||
|
Forward extends SerializedProjectCommand<unknown>,
|
||||||
|
Inverse extends SerializedProjectCommand<unknown>,
|
||||||
|
> {
|
||||||
|
forward: Forward;
|
||||||
|
inverse: Inverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executeProjectCommandTransaction<
|
||||||
|
Command extends SerializedProjectCommand<unknown>,
|
||||||
|
Inverse extends SerializedProjectCommand<unknown>,
|
||||||
|
>(
|
||||||
|
database: AppDatabase,
|
||||||
|
input: ProjectCommandTransactionInput<Command>,
|
||||||
|
applyCommand: (transaction: AppDatabase) => Inverse
|
||||||
|
): { revision: AppendedProjectRevision; inverse: Inverse } {
|
||||||
|
return executeAppliedProjectCommandTransaction(
|
||||||
|
database,
|
||||||
|
input,
|
||||||
|
(transaction) => ({
|
||||||
|
forward: input.command,
|
||||||
|
inverse: applyCommand(transaction),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executeProjectCommandTransactionWithAppliedForward<
|
||||||
|
Command extends SerializedProjectCommand<unknown>,
|
||||||
|
Forward extends SerializedProjectCommand<unknown>,
|
||||||
|
Inverse extends SerializedProjectCommand<unknown>,
|
||||||
|
>(
|
||||||
|
database: AppDatabase,
|
||||||
|
input: ProjectCommandTransactionInput<Command>,
|
||||||
|
applyCommand: (
|
||||||
|
transaction: AppDatabase
|
||||||
|
) => AppliedProjectCommand<Forward, Inverse>
|
||||||
|
): { revision: AppendedProjectRevision; inverse: Inverse } {
|
||||||
|
return executeAppliedProjectCommandTransaction(
|
||||||
|
database,
|
||||||
|
input,
|
||||||
|
applyCommand
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeAppliedProjectCommandTransaction<
|
||||||
|
Command extends SerializedProjectCommand<unknown>,
|
||||||
|
Forward extends SerializedProjectCommand<unknown>,
|
||||||
|
Inverse extends SerializedProjectCommand<unknown>,
|
||||||
|
>(
|
||||||
|
database: AppDatabase,
|
||||||
|
input: ProjectCommandTransactionInput<Command>,
|
||||||
|
applyCommand: (
|
||||||
|
transaction: AppDatabase
|
||||||
|
) => AppliedProjectCommand<Forward, Inverse>
|
||||||
|
): { revision: AppendedProjectRevision; inverse: Inverse } {
|
||||||
|
return database.transaction((tx) => {
|
||||||
|
const { forward, inverse } = applyCommand(tx);
|
||||||
|
const revision = appendProjectRevision(tx, {
|
||||||
|
projectId: input.projectId,
|
||||||
|
expectedRevision: input.expectedRevision,
|
||||||
|
source: input.source,
|
||||||
|
description: input.description,
|
||||||
|
actorId: input.actorId,
|
||||||
|
forward,
|
||||||
|
inverse,
|
||||||
|
});
|
||||||
|
applyProjectHistoryTransition(tx, {
|
||||||
|
projectId: input.projectId,
|
||||||
|
source: input.source,
|
||||||
|
recordedChangeSetId: revision.changeSetId,
|
||||||
|
targetChangeSetId: input.historyTargetChangeSetId,
|
||||||
|
});
|
||||||
|
return { revision, inverse };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -12,8 +12,7 @@ import type {
|
|||||||
} from "../../domain/ports/project-device-project-command.store.js";
|
} from "../../domain/ports/project-device-project-command.store.js";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
type ProjectDeviceRow = typeof projectDevices.$inferSelect;
|
type ProjectDeviceRow = typeof projectDevices.$inferSelect;
|
||||||
|
|
||||||
@@ -25,76 +24,70 @@ export class ProjectDeviceProjectCommandRepository
|
|||||||
executeUpdate(input: ExecuteProjectDeviceUpdateCommandInput) {
|
executeUpdate(input: ExecuteProjectDeviceUpdateCommandInput) {
|
||||||
assertProjectDeviceUpdateProjectCommand(input.command);
|
assertProjectDeviceUpdateProjectCommand(input.command);
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const current = tx
|
this.database,
|
||||||
.select()
|
input,
|
||||||
.from(projectDevices)
|
(tx) => this.applyCommand(tx, input)
|
||||||
.where(
|
);
|
||||||
and(
|
}
|
||||||
eq(
|
|
||||||
projectDevices.id,
|
|
||||||
input.command.payload.projectDeviceId
|
|
||||||
),
|
|
||||||
eq(projectDevices.projectId, input.projectId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.get();
|
|
||||||
if (!current) {
|
|
||||||
throw new Error(
|
|
||||||
"Project device does not belong to project."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = Object.fromEntries(
|
private applyCommand(
|
||||||
input.command.payload.changes.map((change) => [
|
tx: AppDatabase,
|
||||||
change.field,
|
input: ExecuteProjectDeviceUpdateCommandInput
|
||||||
change.value,
|
) {
|
||||||
])
|
const current = tx
|
||||||
) as ProjectDeviceUpdatePatch;
|
.select()
|
||||||
const inversePatch = Object.fromEntries(
|
.from(projectDevices)
|
||||||
input.command.payload.changes.map((change) => [
|
.where(
|
||||||
change.field,
|
and(
|
||||||
getProjectDeviceFieldValue(current, change.field),
|
eq(
|
||||||
])
|
projectDevices.id,
|
||||||
) as ProjectDeviceUpdatePatch;
|
input.command.payload.projectDeviceId
|
||||||
const inverse = createProjectDeviceUpdateProjectCommand(
|
),
|
||||||
current.id,
|
eq(projectDevices.projectId, input.projectId)
|
||||||
inversePatch
|
)
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (!current) {
|
||||||
|
throw new Error(
|
||||||
|
"Project device does not belong to project."
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updated = tx
|
const patch = Object.fromEntries(
|
||||||
.update(projectDevices)
|
input.command.payload.changes.map((change) => [
|
||||||
.set(patch)
|
change.field,
|
||||||
.where(
|
change.value,
|
||||||
and(
|
])
|
||||||
eq(projectDevices.id, current.id),
|
) as ProjectDeviceUpdatePatch;
|
||||||
eq(projectDevices.projectId, input.projectId)
|
const inversePatch = Object.fromEntries(
|
||||||
)
|
input.command.payload.changes.map((change) => [
|
||||||
|
change.field,
|
||||||
|
getProjectDeviceFieldValue(current, change.field),
|
||||||
|
])
|
||||||
|
) as ProjectDeviceUpdatePatch;
|
||||||
|
const inverse = createProjectDeviceUpdateProjectCommand(
|
||||||
|
current.id,
|
||||||
|
inversePatch
|
||||||
|
);
|
||||||
|
|
||||||
|
const updated = tx
|
||||||
|
.update(projectDevices)
|
||||||
|
.set(patch)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(projectDevices.id, current.id),
|
||||||
|
eq(projectDevices.projectId, input.projectId)
|
||||||
)
|
)
|
||||||
.run();
|
)
|
||||||
if (updated.changes !== 1) {
|
.run();
|
||||||
throw new Error(
|
if (updated.changes !== 1) {
|
||||||
"Project device changed before command execution."
|
throw new Error(
|
||||||
);
|
"Project device changed before command execution."
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const revision = appendProjectRevision(tx, {
|
return inverse;
|
||||||
projectId: input.projectId,
|
|
||||||
expectedRevision: input.expectedRevision,
|
|
||||||
source: input.source,
|
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
|||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
||||||
|
|
||||||
@@ -28,116 +27,110 @@ export class ProjectDeviceRowSyncProjectCommandRepository
|
|||||||
execute(input: ExecuteProjectDeviceRowSyncCommandInput) {
|
execute(input: ExecuteProjectDeviceRowSyncCommandInput) {
|
||||||
assertProjectDeviceRowSyncProjectCommand(input.command);
|
assertProjectDeviceRowSyncProjectCommand(input.command);
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const projectDevice = tx
|
this.database,
|
||||||
.select({ id: projectDevices.id })
|
input,
|
||||||
.from(projectDevices)
|
(tx) => this.applyCommand(tx, input)
|
||||||
.where(
|
);
|
||||||
and(
|
}
|
||||||
eq(
|
|
||||||
projectDevices.id,
|
|
||||||
input.command.payload.projectDeviceId
|
|
||||||
),
|
|
||||||
eq(projectDevices.projectId, input.projectId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.get();
|
|
||||||
if (!projectDevice) {
|
|
||||||
throw new Error(
|
|
||||||
"Project device does not belong to project."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowIds = input.command.payload.rows.map(
|
private applyCommand(
|
||||||
(row) => row.rowId
|
tx: AppDatabase,
|
||||||
|
input: ExecuteProjectDeviceRowSyncCommandInput
|
||||||
|
) {
|
||||||
|
const projectDevice = tx
|
||||||
|
.select({ id: projectDevices.id })
|
||||||
|
.from(projectDevices)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
projectDevices.id,
|
||||||
|
input.command.payload.projectDeviceId
|
||||||
|
),
|
||||||
|
eq(projectDevices.projectId, input.projectId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (!projectDevice) {
|
||||||
|
throw new Error(
|
||||||
|
"Project device does not belong to project."
|
||||||
);
|
);
|
||||||
const persistedRows = tx
|
}
|
||||||
.select({
|
|
||||||
row: circuitDeviceRows,
|
const rowIds = input.command.payload.rows.map(
|
||||||
projectId: circuitLists.projectId,
|
(row) => row.rowId
|
||||||
})
|
);
|
||||||
.from(circuitDeviceRows)
|
const persistedRows = tx
|
||||||
.innerJoin(
|
.select({
|
||||||
circuits,
|
row: circuitDeviceRows,
|
||||||
eq(circuits.id, circuitDeviceRows.circuitId)
|
projectId: circuitLists.projectId,
|
||||||
)
|
})
|
||||||
.innerJoin(
|
.from(circuitDeviceRows)
|
||||||
circuitLists,
|
.innerJoin(
|
||||||
eq(circuitLists.id, circuits.circuitListId)
|
circuits,
|
||||||
)
|
eq(circuits.id, circuitDeviceRows.circuitId)
|
||||||
.where(inArray(circuitDeviceRows.id, rowIds))
|
)
|
||||||
.all();
|
.innerJoin(
|
||||||
|
circuitLists,
|
||||||
|
eq(circuitLists.id, circuits.circuitListId)
|
||||||
|
)
|
||||||
|
.where(inArray(circuitDeviceRows.id, rowIds))
|
||||||
|
.all();
|
||||||
|
if (
|
||||||
|
persistedRows.length !== rowIds.length ||
|
||||||
|
persistedRows.some(
|
||||||
|
(persisted) => persisted.projectId !== input.projectId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"One or more sync rows do not belong to project."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistedById = new Map(
|
||||||
|
persistedRows.map((persisted) => [
|
||||||
|
persisted.row.id,
|
||||||
|
persisted.row,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
for (const assignment of input.command.payload.rows) {
|
||||||
|
const persisted = persistedById.get(assignment.rowId);
|
||||||
if (
|
if (
|
||||||
persistedRows.length !== rowIds.length ||
|
!persisted ||
|
||||||
persistedRows.some(
|
!snapshotMatchesRow(assignment.expected, persisted)
|
||||||
(persisted) => persisted.projectId !== input.projectId
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"One or more sync rows do not belong to project."
|
"Project-device row changed before sync execution."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const persistedById = new Map(
|
const inverse = createProjectDeviceRowSyncProjectCommand(
|
||||||
persistedRows.map((persisted) => [
|
projectDevice.id,
|
||||||
persisted.row.id,
|
invertProjectDeviceRowSyncOperation(
|
||||||
persisted.row,
|
input.command.payload.operation
|
||||||
])
|
),
|
||||||
);
|
input.command.payload.rows.map((row) => ({
|
||||||
for (const assignment of input.command.payload.rows) {
|
rowId: row.rowId,
|
||||||
const persisted = persistedById.get(assignment.rowId);
|
expected: row.target,
|
||||||
if (
|
target: row.expected,
|
||||||
!persisted ||
|
}))
|
||||||
!snapshotMatchesRow(assignment.expected, persisted)
|
);
|
||||||
) {
|
|
||||||
throw new Error(
|
for (const assignment of input.command.payload.rows) {
|
||||||
"Project-device row changed before sync execution."
|
const updated = tx
|
||||||
);
|
.update(circuitDeviceRows)
|
||||||
}
|
.set(assignment.target)
|
||||||
|
.where(eq(circuitDeviceRows.id, assignment.rowId))
|
||||||
|
.run();
|
||||||
|
if (updated.changes !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
"Project-device row changed during sync execution."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const inverse = createProjectDeviceRowSyncProjectCommand(
|
return inverse;
|
||||||
projectDevice.id,
|
|
||||||
invertProjectDeviceRowSyncOperation(
|
|
||||||
input.command.payload.operation
|
|
||||||
),
|
|
||||||
input.command.payload.rows.map((row) => ({
|
|
||||||
rowId: row.rowId,
|
|
||||||
expected: row.target,
|
|
||||||
target: row.expected,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const assignment of input.command.payload.rows) {
|
|
||||||
const updated = tx
|
|
||||||
.update(circuitDeviceRows)
|
|
||||||
.set(assignment.target)
|
|
||||||
.where(eq(circuitDeviceRows.id, assignment.rowId))
|
|
||||||
.run();
|
|
||||||
if (updated.changes !== 1) {
|
|
||||||
throw new Error(
|
|
||||||
"Project-device row changed during sync execution."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const revision = appendProjectRevision(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
expectedRevision: input.expectedRevision,
|
|
||||||
source: input.source,
|
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ import { circuits } from "../schema/circuits.js";
|
|||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js";
|
import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
const circuitDeviceRowSnapshotFields = [
|
const circuitDeviceRowSnapshotFields = [
|
||||||
"id",
|
"id",
|
||||||
@@ -60,30 +59,17 @@ export class ProjectDeviceStructureProjectCommandRepository
|
|||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
execute(input: ExecuteProjectDeviceStructureCommandInput) {
|
execute(input: ExecuteProjectDeviceStructureCommandInput) {
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const inverse = this.applyCommand(
|
this.database,
|
||||||
tx,
|
input,
|
||||||
input.projectId,
|
(tx) =>
|
||||||
input.source,
|
this.applyCommand(
|
||||||
input.command
|
tx,
|
||||||
);
|
input.projectId,
|
||||||
const revision = appendProjectRevision(tx, {
|
input.source,
|
||||||
projectId: input.projectId,
|
input.command
|
||||||
expectedRevision: input.expectedRevision,
|
)
|
||||||
source: input.source,
|
);
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyCommand(
|
private applyCommand(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
import { calculateRowTotalPower } from "../../domain/calculations/circuit-power-calculation.js";
|
import { calculateRowTotalPower } from "../../domain/calculations/circuit-power-calculation.js";
|
||||||
|
|
||||||
@@ -11,12 +11,16 @@ function withTotalPower(row: typeof projectDevices.$inferSelect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectDeviceRepository {
|
export class ProjectDeviceRepository {
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
async listByProject(projectId: string) {
|
async listByProject(projectId: string) {
|
||||||
|
const db = this.database;
|
||||||
const rows = await db.select().from(projectDevices).where(eq(projectDevices.projectId, projectId));
|
const rows = await db.select().from(projectDevices).where(eq(projectDevices.projectId, projectId));
|
||||||
return rows.map(withTotalPower);
|
return rows.map(withTotalPower);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(projectId: string, projectDeviceId: string) {
|
async findById(projectId: string, projectDeviceId: string) {
|
||||||
|
const db = this.database;
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(projectDevices)
|
.from(projectDevices)
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
assertProjectFloorDeleteProjectCommand,
|
||||||
|
assertProjectFloorInsertProjectCommand,
|
||||||
|
assertProjectRoomDeleteProjectCommand,
|
||||||
|
assertProjectRoomInsertProjectCommand,
|
||||||
|
createProjectFloorDeleteProjectCommand,
|
||||||
|
createProjectFloorInsertProjectCommand,
|
||||||
|
createProjectRoomDeleteProjectCommand,
|
||||||
|
createProjectRoomInsertProjectCommand,
|
||||||
|
projectFloorDeleteCommandType,
|
||||||
|
projectFloorInsertCommandType,
|
||||||
|
projectRoomDeleteCommandType,
|
||||||
|
projectRoomInsertCommandType,
|
||||||
|
type ProjectFloorSnapshot,
|
||||||
|
type ProjectLocationStructureProjectCommand,
|
||||||
|
type ProjectRoomSnapshot,
|
||||||
|
} from "../../domain/models/project-location-structure-project-command.model.js";
|
||||||
|
import type {
|
||||||
|
ExecuteProjectLocationStructureCommandInput,
|
||||||
|
ProjectLocationStructureProjectCommandStore,
|
||||||
|
} from "../../domain/ports/project-location-structure-project-command.store.js";
|
||||||
|
import type { AppDatabase } from "../database-context.js";
|
||||||
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
|
import { consumers } from "../schema/consumers.js";
|
||||||
|
import { floors } from "../schema/floors.js";
|
||||||
|
import { projects } from "../schema/projects.js";
|
||||||
|
import { rooms } from "../schema/rooms.js";
|
||||||
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
|
|
||||||
|
export class ProjectLocationStructureProjectCommandRepository
|
||||||
|
implements ProjectLocationStructureProjectCommandStore
|
||||||
|
{
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
|
execute(input: ExecuteProjectLocationStructureCommandInput) {
|
||||||
|
return executeProjectCommandTransaction(
|
||||||
|
this.database,
|
||||||
|
input,
|
||||||
|
(tx) =>
|
||||||
|
this.applyCommand(tx, input.projectId, input.command)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyCommand(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
command: ProjectLocationStructureProjectCommand
|
||||||
|
): ProjectLocationStructureProjectCommand {
|
||||||
|
switch (command.type) {
|
||||||
|
case projectFloorInsertCommandType:
|
||||||
|
assertProjectFloorInsertProjectCommand(command);
|
||||||
|
return this.insertFloor(
|
||||||
|
database,
|
||||||
|
projectId,
|
||||||
|
command.payload.floor
|
||||||
|
);
|
||||||
|
case projectFloorDeleteCommandType:
|
||||||
|
assertProjectFloorDeleteProjectCommand(command);
|
||||||
|
return this.deleteFloor(
|
||||||
|
database,
|
||||||
|
projectId,
|
||||||
|
command.payload.floor
|
||||||
|
);
|
||||||
|
case projectRoomInsertCommandType:
|
||||||
|
assertProjectRoomInsertProjectCommand(command);
|
||||||
|
return this.insertRoom(
|
||||||
|
database,
|
||||||
|
projectId,
|
||||||
|
command.payload.room
|
||||||
|
);
|
||||||
|
case projectRoomDeleteCommandType:
|
||||||
|
assertProjectRoomDeleteProjectCommand(command);
|
||||||
|
return this.deleteRoom(
|
||||||
|
database,
|
||||||
|
projectId,
|
||||||
|
command.payload.room
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private insertFloor(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
floor: ProjectFloorSnapshot
|
||||||
|
) {
|
||||||
|
this.assertProjectExists(database, projectId);
|
||||||
|
if (floor.projectId !== projectId) {
|
||||||
|
throw new Error("Project-floor snapshot does not belong to project.");
|
||||||
|
}
|
||||||
|
const existing = database
|
||||||
|
.select({ id: floors.id })
|
||||||
|
.from(floors)
|
||||||
|
.where(eq(floors.id, floor.id))
|
||||||
|
.get();
|
||||||
|
if (existing) {
|
||||||
|
throw new Error("Project-floor id already exists.");
|
||||||
|
}
|
||||||
|
database.insert(floors).values(floor).run();
|
||||||
|
return createProjectFloorDeleteProjectCommand(floor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteFloor(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
expected: ProjectFloorSnapshot
|
||||||
|
) {
|
||||||
|
const persisted = database
|
||||||
|
.select()
|
||||||
|
.from(floors)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(floors.id, expected.id),
|
||||||
|
eq(floors.projectId, projectId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (!persisted || !sameRecord(expected, persisted)) {
|
||||||
|
throw new Error("Project floor changed before deletion.");
|
||||||
|
}
|
||||||
|
const referencedRoom = database
|
||||||
|
.select({ id: rooms.id })
|
||||||
|
.from(rooms)
|
||||||
|
.where(eq(rooms.floorId, expected.id))
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
if (referencedRoom) {
|
||||||
|
throw new Error(
|
||||||
|
"A project floor with assigned rooms cannot be removed by history."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const deleted = database
|
||||||
|
.delete(floors)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(floors.id, expected.id),
|
||||||
|
eq(floors.projectId, projectId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
if (deleted.changes !== 1) {
|
||||||
|
throw new Error("Project floor could not be deleted.");
|
||||||
|
}
|
||||||
|
return createProjectFloorInsertProjectCommand(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private insertRoom(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
room: ProjectRoomSnapshot
|
||||||
|
) {
|
||||||
|
this.assertProjectExists(database, projectId);
|
||||||
|
if (room.projectId !== projectId) {
|
||||||
|
throw new Error("Project-room snapshot does not belong to project.");
|
||||||
|
}
|
||||||
|
if (room.floorId) {
|
||||||
|
const floor = database
|
||||||
|
.select({ id: floors.id })
|
||||||
|
.from(floors)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(floors.id, room.floorId),
|
||||||
|
eq(floors.projectId, projectId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (!floor) {
|
||||||
|
throw new Error("Project room references a foreign floor.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const existing = database
|
||||||
|
.select({ id: rooms.id })
|
||||||
|
.from(rooms)
|
||||||
|
.where(eq(rooms.id, room.id))
|
||||||
|
.get();
|
||||||
|
if (existing) {
|
||||||
|
throw new Error("Project-room id already exists.");
|
||||||
|
}
|
||||||
|
database.insert(rooms).values(room).run();
|
||||||
|
return createProjectRoomDeleteProjectCommand(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteRoom(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
expected: ProjectRoomSnapshot
|
||||||
|
) {
|
||||||
|
const persisted = database
|
||||||
|
.select()
|
||||||
|
.from(rooms)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(rooms.id, expected.id),
|
||||||
|
eq(rooms.projectId, projectId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (!persisted || !sameRecord(expected, persisted)) {
|
||||||
|
throw new Error("Project room changed before deletion.");
|
||||||
|
}
|
||||||
|
const referencedDeviceRow = database
|
||||||
|
.select({ id: circuitDeviceRows.id })
|
||||||
|
.from(circuitDeviceRows)
|
||||||
|
.where(eq(circuitDeviceRows.roomId, expected.id))
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
const referencedLegacyConsumer = database
|
||||||
|
.select({ id: consumers.id })
|
||||||
|
.from(consumers)
|
||||||
|
.where(eq(consumers.roomId, expected.id))
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
if (referencedDeviceRow || referencedLegacyConsumer) {
|
||||||
|
throw new Error(
|
||||||
|
"A referenced project room cannot be removed by history."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const deleted = database
|
||||||
|
.delete(rooms)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(rooms.id, expected.id),
|
||||||
|
eq(rooms.projectId, projectId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
if (deleted.changes !== 1) {
|
||||||
|
throw new Error("Project room could not be deleted.");
|
||||||
|
}
|
||||||
|
return createProjectRoomInsertProjectCommand(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertProjectExists(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string
|
||||||
|
) {
|
||||||
|
const project = database
|
||||||
|
.select({ id: projects.id })
|
||||||
|
.from(projects)
|
||||||
|
.where(eq(projects.id, projectId))
|
||||||
|
.get();
|
||||||
|
if (!project) {
|
||||||
|
throw new Error("Project does not exist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameRecord(
|
||||||
|
expected: object,
|
||||||
|
actual: object
|
||||||
|
) {
|
||||||
|
const expectedEntries = Object.entries(expected);
|
||||||
|
const actualRecord = actual as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
expectedEntries.length === Object.keys(actual).length &&
|
||||||
|
expectedEntries.every(([key, value]) => actualRecord[key] === value)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { ProjectRevisionConflictError } from "../../domain/errors/project-revision-conflict.error.js";
|
|
||||||
import type {
|
|
||||||
AppendProjectRevisionInput,
|
|
||||||
AppendedProjectRevision,
|
|
||||||
ProjectRevisionStore,
|
|
||||||
} from "../../domain/ports/project-revision.store.js";
|
|
||||||
import type { AppDatabase } from "../database-context.js";
|
|
||||||
import { projects } from "../schema/projects.js";
|
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
export { ProjectRevisionConflictError };
|
|
||||||
|
|
||||||
export class ProjectRevisionRepository implements ProjectRevisionStore {
|
|
||||||
constructor(private readonly database: AppDatabase) {}
|
|
||||||
|
|
||||||
getCurrentRevision(projectId: string) {
|
|
||||||
const project = this.database
|
|
||||||
.select({ currentRevision: projects.currentRevision })
|
|
||||||
.from(projects)
|
|
||||||
.where(eq(projects.id, projectId))
|
|
||||||
.get();
|
|
||||||
return project?.currentRevision ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
append(input: AppendProjectRevisionInput): AppendedProjectRevision {
|
|
||||||
return this.database.transaction((tx) =>
|
|
||||||
appendProjectRevision(tx, input)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,7 @@ import type {
|
|||||||
} from "../../domain/ports/project-settings-project-command.store.js";
|
} from "../../domain/ports/project-settings-project-command.store.js";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
export class ProjectSettingsProjectCommandRepository
|
export class ProjectSettingsProjectCommandRepository
|
||||||
implements ProjectSettingsProjectCommandStore
|
implements ProjectSettingsProjectCommandStore
|
||||||
@@ -19,53 +18,47 @@ export class ProjectSettingsProjectCommandRepository
|
|||||||
|
|
||||||
executeUpdate(input: ExecuteProjectSettingsUpdateCommandInput) {
|
executeUpdate(input: ExecuteProjectSettingsUpdateCommandInput) {
|
||||||
assertProjectSettingsUpdateProjectCommand(input.command);
|
assertProjectSettingsUpdateProjectCommand(input.command);
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const current = tx
|
this.database,
|
||||||
.select()
|
input,
|
||||||
.from(projects)
|
(tx) => this.applyCommand(tx, input)
|
||||||
.where(eq(projects.id, input.projectId))
|
);
|
||||||
.get();
|
}
|
||||||
if (!current) {
|
|
||||||
throw new Error("Project not found.");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
current.singlePhaseVoltageV ===
|
|
||||||
input.command.payload.singlePhaseVoltageV &&
|
|
||||||
current.threePhaseVoltageV ===
|
|
||||||
input.command.payload.threePhaseVoltageV
|
|
||||||
) {
|
|
||||||
throw new Error("Project settings did not change.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const inverse = createProjectSettingsUpdateProjectCommand({
|
private applyCommand(
|
||||||
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
tx: AppDatabase,
|
||||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
input: ExecuteProjectSettingsUpdateCommandInput
|
||||||
});
|
) {
|
||||||
const updated = tx
|
const current = tx
|
||||||
.update(projects)
|
.select()
|
||||||
.set(input.command.payload)
|
.from(projects)
|
||||||
.where(eq(projects.id, input.projectId))
|
.where(eq(projects.id, input.projectId))
|
||||||
.run();
|
.get();
|
||||||
if (updated.changes !== 1) {
|
if (!current) {
|
||||||
throw new Error("Project changed before settings update.");
|
throw new Error("Project not found.");
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
current.singlePhaseVoltageV ===
|
||||||
|
input.command.payload.singlePhaseVoltageV &&
|
||||||
|
current.threePhaseVoltageV ===
|
||||||
|
input.command.payload.threePhaseVoltageV
|
||||||
|
) {
|
||||||
|
throw new Error("Project settings did not change.");
|
||||||
|
}
|
||||||
|
|
||||||
const revision = appendProjectRevision(tx, {
|
const inverse = createProjectSettingsUpdateProjectCommand({
|
||||||
projectId: input.projectId,
|
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
||||||
expectedRevision: input.expectedRevision,
|
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||||
source: input.source,
|
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
});
|
||||||
|
const updated = tx
|
||||||
|
.update(projects)
|
||||||
|
.set(input.command.payload)
|
||||||
|
.where(eq(projects.id, input.projectId))
|
||||||
|
.run();
|
||||||
|
if (updated.changes !== 1) {
|
||||||
|
throw new Error("Project changed before settings update.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return inverse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migrat
|
|||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { rooms } from "../schema/rooms.js";
|
import { rooms } from "../schema/rooms.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
import {
|
import {
|
||||||
hashProjectStatePayload,
|
hashProjectStatePayload,
|
||||||
readProjectStateSnapshot,
|
readProjectStateSnapshot,
|
||||||
@@ -43,49 +42,43 @@ export class ProjectStateRestoreCommandRepository
|
|||||||
throw new Error("Restore state belongs to a different project.");
|
throw new Error("Restore state belongs to a different project.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return executeProjectCommandTransaction(
|
||||||
const current = readProjectStateSnapshot(tx, input.projectId);
|
this.database,
|
||||||
if (!current) {
|
input,
|
||||||
throw new Error("Project not found.");
|
(tx) => this.applyCommand(tx, input)
|
||||||
}
|
);
|
||||||
if (
|
}
|
||||||
current.payloadSha256 !==
|
|
||||||
input.command.payload.expectedStateSha256
|
|
||||||
) {
|
|
||||||
throw new ProjectStateConflictError(
|
|
||||||
input.projectId,
|
|
||||||
input.command.payload.expectedStateSha256,
|
|
||||||
current.payloadSha256
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetPayloadJson = serializeProjectStateSnapshot(
|
private applyCommand(
|
||||||
input.command.payload.targetState
|
tx: AppDatabase,
|
||||||
);
|
input: ExecuteProjectStateRestoreCommandInput
|
||||||
const inverse = createProjectStateRestoreCommand(
|
) {
|
||||||
hashProjectStatePayload(targetPayloadJson),
|
const current = readProjectStateSnapshot(tx, input.projectId);
|
||||||
current.state
|
if (!current) {
|
||||||
|
throw new Error("Project not found.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
current.payloadSha256 !==
|
||||||
|
input.command.payload.expectedStateSha256
|
||||||
|
) {
|
||||||
|
throw new ProjectStateConflictError(
|
||||||
|
input.projectId,
|
||||||
|
input.command.payload.expectedStateSha256,
|
||||||
|
current.payloadSha256
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
replaceProjectState(tx, input.command.payload.targetState);
|
const targetPayloadJson = serializeProjectStateSnapshot(
|
||||||
|
input.command.payload.targetState
|
||||||
|
);
|
||||||
|
const inverse = createProjectStateRestoreCommand(
|
||||||
|
hashProjectStatePayload(targetPayloadJson),
|
||||||
|
current.state
|
||||||
|
);
|
||||||
|
|
||||||
const revision = appendProjectRevision(tx, {
|
replaceProjectState(tx, input.command.payload.targetState);
|
||||||
projectId: input.projectId,
|
|
||||||
expectedRevision: input.expectedRevision,
|
return inverse;
|
||||||
source: input.source,
|
|
||||||
description: input.description,
|
|
||||||
actorId: input.actorId,
|
|
||||||
forward: input.command,
|
|
||||||
inverse,
|
|
||||||
});
|
|
||||||
applyProjectHistoryTransition(tx, {
|
|
||||||
projectId: input.projectId,
|
|
||||||
source: input.source,
|
|
||||||
recordedChangeSetId: revision.changeSetId,
|
|
||||||
targetChangeSetId: input.historyTargetChangeSetId,
|
|
||||||
});
|
|
||||||
return { revision, inverse };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import type {
|
import type {
|
||||||
CreateProjectInput,
|
CreateProjectInput,
|
||||||
} from "../../shared/validation/project-structure.schemas.js";
|
} from "../../shared/validation/project-structure.schemas.js";
|
||||||
|
|
||||||
export class ProjectRepository {
|
export class ProjectRepository {
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
async list() {
|
async list() {
|
||||||
|
const db = this.database;
|
||||||
return db.select().from(projects);
|
return db.select().from(projects);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(input: CreateProjectInput) {
|
async create(input: CreateProjectInput) {
|
||||||
|
const db = this.database;
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const project = {
|
const project = {
|
||||||
id,
|
id,
|
||||||
@@ -25,11 +29,8 @@ export class ProjectRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findById(projectId: string) {
|
async findById(projectId: string) {
|
||||||
|
const db = this.database;
|
||||||
const [row] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
|
const [row] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(projectId: string) {
|
|
||||||
await db.delete(projects).where(eq(projects.id, projectId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,16 @@
|
|||||||
import crypto from "node:crypto";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { db } from "../client.js";
|
|
||||||
import { rooms } from "../schema/rooms.js";
|
import { rooms } from "../schema/rooms.js";
|
||||||
import type { CreateRoomInput } from "../../shared/validation/project-structure.schemas.js";
|
|
||||||
|
|
||||||
export class RoomRepository {
|
export class RoomRepository {
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
async listByProject(projectId: string) {
|
async listByProject(projectId: string) {
|
||||||
|
const db = this.database;
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
.from(rooms)
|
.from(rooms)
|
||||||
.where(eq(rooms.projectId, projectId))
|
.where(eq(rooms.projectId, projectId))
|
||||||
.orderBy(asc(rooms.roomNumber), asc(rooms.roomName));
|
.orderBy(asc(rooms.roomNumber), asc(rooms.roomName));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(projectId: string, input: CreateRoomInput) {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const room = {
|
|
||||||
id,
|
|
||||||
projectId,
|
|
||||||
floorId: input.floorId ?? null,
|
|
||||||
roomNumber: input.roomNumber,
|
|
||||||
roomName: input.roomName,
|
|
||||||
};
|
|
||||||
await db.insert(rooms).values(room);
|
|
||||||
return room;
|
|
||||||
}
|
|
||||||
|
|
||||||
async existsInProject(projectId: string, roomId: string) {
|
|
||||||
const [row] = await db
|
|
||||||
.select({ id: rooms.id })
|
|
||||||
.from(rooms)
|
|
||||||
.where(and(eq(rooms.projectId, projectId), eq(rooms.id, roomId)))
|
|
||||||
.limit(1);
|
|
||||||
return Boolean(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(roomId: string) {
|
|
||||||
const [row] = await db.select().from(rooms).where(eq(rooms.id, roomId)).limit(1);
|
|
||||||
return row ?? null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
|
||||||
|
export const projectFloorInsertCommandType = "project-floor.insert" as const;
|
||||||
|
export const projectFloorDeleteCommandType = "project-floor.delete" as const;
|
||||||
|
export const projectRoomInsertCommandType = "project-room.insert" as const;
|
||||||
|
export const projectRoomDeleteCommandType = "project-room.delete" as const;
|
||||||
|
export const projectLocationStructureCommandSchemaVersion = 1 as const;
|
||||||
|
|
||||||
|
export interface ProjectFloorSnapshot {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectRoomSnapshot {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
floorId: string | null;
|
||||||
|
roomNumber: string;
|
||||||
|
roomName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectFloorStructureCommandPayload {
|
||||||
|
floor: ProjectFloorSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectRoomStructureCommandPayload {
|
||||||
|
room: ProjectRoomSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectFloorInsertProjectCommand
|
||||||
|
extends SerializedProjectCommand<ProjectFloorStructureCommandPayload> {
|
||||||
|
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||||
|
type: typeof projectFloorInsertCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectFloorDeleteProjectCommand
|
||||||
|
extends SerializedProjectCommand<ProjectFloorStructureCommandPayload> {
|
||||||
|
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||||
|
type: typeof projectFloorDeleteCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectRoomInsertProjectCommand
|
||||||
|
extends SerializedProjectCommand<ProjectRoomStructureCommandPayload> {
|
||||||
|
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||||
|
type: typeof projectRoomInsertCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectRoomDeleteProjectCommand
|
||||||
|
extends SerializedProjectCommand<ProjectRoomStructureCommandPayload> {
|
||||||
|
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||||
|
type: typeof projectRoomDeleteCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectLocationStructureProjectCommand =
|
||||||
|
| ProjectFloorInsertProjectCommand
|
||||||
|
| ProjectFloorDeleteProjectCommand
|
||||||
|
| ProjectRoomInsertProjectCommand
|
||||||
|
| ProjectRoomDeleteProjectCommand;
|
||||||
|
|
||||||
|
export function createProjectFloorSnapshot(
|
||||||
|
projectId: string,
|
||||||
|
name: string,
|
||||||
|
sortOrder: number
|
||||||
|
): ProjectFloorSnapshot {
|
||||||
|
const floor: ProjectFloorSnapshot = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
projectId,
|
||||||
|
name: name.trim(),
|
||||||
|
sortOrder,
|
||||||
|
};
|
||||||
|
assertProjectFloorSnapshot(floor);
|
||||||
|
return floor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectRoomSnapshot(
|
||||||
|
projectId: string,
|
||||||
|
input: {
|
||||||
|
floorId?: string;
|
||||||
|
roomNumber: string;
|
||||||
|
roomName: string;
|
||||||
|
}
|
||||||
|
): ProjectRoomSnapshot {
|
||||||
|
const room: ProjectRoomSnapshot = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
projectId,
|
||||||
|
floorId: input.floorId?.trim() || null,
|
||||||
|
roomNumber: input.roomNumber.trim(),
|
||||||
|
roomName: input.roomName.trim(),
|
||||||
|
};
|
||||||
|
assertProjectRoomSnapshot(room);
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectFloorInsertProjectCommand(
|
||||||
|
floor: ProjectFloorSnapshot
|
||||||
|
): ProjectFloorInsertProjectCommand {
|
||||||
|
assertProjectFloorSnapshot(floor);
|
||||||
|
return {
|
||||||
|
schemaVersion: projectLocationStructureCommandSchemaVersion,
|
||||||
|
type: projectFloorInsertCommandType,
|
||||||
|
payload: { floor },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectFloorDeleteProjectCommand(
|
||||||
|
floor: ProjectFloorSnapshot
|
||||||
|
): ProjectFloorDeleteProjectCommand {
|
||||||
|
assertProjectFloorSnapshot(floor);
|
||||||
|
return {
|
||||||
|
schemaVersion: projectLocationStructureCommandSchemaVersion,
|
||||||
|
type: projectFloorDeleteCommandType,
|
||||||
|
payload: { floor },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectRoomInsertProjectCommand(
|
||||||
|
room: ProjectRoomSnapshot
|
||||||
|
): ProjectRoomInsertProjectCommand {
|
||||||
|
assertProjectRoomSnapshot(room);
|
||||||
|
return {
|
||||||
|
schemaVersion: projectLocationStructureCommandSchemaVersion,
|
||||||
|
type: projectRoomInsertCommandType,
|
||||||
|
payload: { room },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectRoomDeleteProjectCommand(
|
||||||
|
room: ProjectRoomSnapshot
|
||||||
|
): ProjectRoomDeleteProjectCommand {
|
||||||
|
assertProjectRoomSnapshot(room);
|
||||||
|
return {
|
||||||
|
schemaVersion: projectLocationStructureCommandSchemaVersion,
|
||||||
|
type: projectRoomDeleteCommandType,
|
||||||
|
payload: { room },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertProjectFloorInsertProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>
|
||||||
|
): asserts command is ProjectFloorInsertProjectCommand {
|
||||||
|
assertProjectFloorStructureProjectCommand(
|
||||||
|
command,
|
||||||
|
projectFloorInsertCommandType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertProjectFloorDeleteProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>
|
||||||
|
): asserts command is ProjectFloorDeleteProjectCommand {
|
||||||
|
assertProjectFloorStructureProjectCommand(
|
||||||
|
command,
|
||||||
|
projectFloorDeleteCommandType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertProjectRoomInsertProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>
|
||||||
|
): asserts command is ProjectRoomInsertProjectCommand {
|
||||||
|
assertProjectRoomStructureProjectCommand(
|
||||||
|
command,
|
||||||
|
projectRoomInsertCommandType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertProjectRoomDeleteProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>
|
||||||
|
): asserts command is ProjectRoomDeleteProjectCommand {
|
||||||
|
assertProjectRoomStructureProjectCommand(
|
||||||
|
command,
|
||||||
|
projectRoomDeleteCommandType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertProjectFloorSnapshot(
|
||||||
|
floor: unknown
|
||||||
|
): asserts floor is ProjectFloorSnapshot {
|
||||||
|
if (!isPlainObject(floor) || Object.keys(floor).length !== 4) {
|
||||||
|
throw new Error("Project-floor snapshot is invalid.");
|
||||||
|
}
|
||||||
|
assertNormalizedNonEmptyString(floor.id, "floor.id");
|
||||||
|
assertNormalizedNonEmptyString(floor.projectId, "floor.projectId");
|
||||||
|
assertNormalizedNonEmptyString(floor.name, "floor.name");
|
||||||
|
if (
|
||||||
|
typeof floor.sortOrder !== "number" ||
|
||||||
|
!Number.isSafeInteger(floor.sortOrder) ||
|
||||||
|
floor.sortOrder < 0
|
||||||
|
) {
|
||||||
|
throw new Error("floor.sortOrder must be a non-negative integer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertProjectRoomSnapshot(
|
||||||
|
room: unknown
|
||||||
|
): asserts room is ProjectRoomSnapshot {
|
||||||
|
if (!isPlainObject(room) || Object.keys(room).length !== 5) {
|
||||||
|
throw new Error("Project-room snapshot is invalid.");
|
||||||
|
}
|
||||||
|
assertNormalizedNonEmptyString(room.id, "room.id");
|
||||||
|
assertNormalizedNonEmptyString(room.projectId, "room.projectId");
|
||||||
|
if (room.floorId !== null) {
|
||||||
|
assertNormalizedNonEmptyString(room.floorId, "room.floorId");
|
||||||
|
}
|
||||||
|
assertNormalizedNonEmptyString(room.roomNumber, "room.roomNumber");
|
||||||
|
assertNormalizedNonEmptyString(room.roomName, "room.roomName");
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertProjectFloorStructureProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>,
|
||||||
|
expectedType:
|
||||||
|
| typeof projectFloorInsertCommandType
|
||||||
|
| typeof projectFloorDeleteCommandType
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
command.schemaVersion !== projectLocationStructureCommandSchemaVersion ||
|
||||||
|
command.type !== expectedType ||
|
||||||
|
!isPlainObject(command.payload) ||
|
||||||
|
Object.keys(command.payload).length !== 1
|
||||||
|
) {
|
||||||
|
throw new Error("Unsupported project-floor structure command.");
|
||||||
|
}
|
||||||
|
assertProjectFloorSnapshot(command.payload.floor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertProjectRoomStructureProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>,
|
||||||
|
expectedType:
|
||||||
|
| typeof projectRoomInsertCommandType
|
||||||
|
| typeof projectRoomDeleteCommandType
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
command.schemaVersion !== projectLocationStructureCommandSchemaVersion ||
|
||||||
|
command.type !== expectedType ||
|
||||||
|
!isPlainObject(command.payload) ||
|
||||||
|
Object.keys(command.payload).length !== 1
|
||||||
|
) {
|
||||||
|
throw new Error("Unsupported project-room structure command.");
|
||||||
|
}
|
||||||
|
assertProjectRoomSnapshot(command.payload.room);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNormalizedNonEmptyString(
|
||||||
|
value: unknown,
|
||||||
|
field: string
|
||||||
|
): asserts value is string {
|
||||||
|
if (
|
||||||
|
typeof value !== "string" ||
|
||||||
|
!value ||
|
||||||
|
value !== value.trim()
|
||||||
|
) {
|
||||||
|
throw new Error(`${field} must be a normalized non-empty string.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
export interface LegacyConsumerMigrationCircuitListReader {
|
||||||
|
findById(
|
||||||
|
projectId: string,
|
||||||
|
circuitListId: string
|
||||||
|
): Promise<{ id: string } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationSection {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationSectionStore {
|
||||||
|
createDefaults(circuitListId: string): Promise<void>;
|
||||||
|
listByCircuitList(
|
||||||
|
circuitListId: string
|
||||||
|
): Promise<LegacyConsumerMigrationSection[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationCircuitReader {
|
||||||
|
listByCircuitList(circuitListId: string): Promise<
|
||||||
|
Array<{
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationRoomReader {
|
||||||
|
listByProject(projectId: string): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
roomNumber: string;
|
||||||
|
roomName: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerSource {
|
||||||
|
id: string;
|
||||||
|
projectDeviceId: string | null;
|
||||||
|
roomId: string | null;
|
||||||
|
circuitNumber: string | null;
|
||||||
|
description: string | null;
|
||||||
|
name: string;
|
||||||
|
category: string | null;
|
||||||
|
deviceType: string | null;
|
||||||
|
phaseType: string | null;
|
||||||
|
tradeOrCostGroup: string | null;
|
||||||
|
protectionType: string | null;
|
||||||
|
protectionRatedCurrent: number | null;
|
||||||
|
protectionCharacteristic: string | null;
|
||||||
|
cableType: string | null;
|
||||||
|
cableCrossSection: string | null;
|
||||||
|
comment: string | null;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV: number | null;
|
||||||
|
phaseCount: number | null;
|
||||||
|
powerFactor: number | null;
|
||||||
|
note: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyMigrationDeviceRowInput {
|
||||||
|
linkedProjectDeviceId?: string;
|
||||||
|
legacyConsumerId: string;
|
||||||
|
sortOrder: number;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
phaseType?: string;
|
||||||
|
connectionKind?: string;
|
||||||
|
costGroup?: string;
|
||||||
|
category?: string;
|
||||||
|
level?: string;
|
||||||
|
roomId?: string;
|
||||||
|
roomNumberSnapshot?: string;
|
||||||
|
roomNameSnapshot?: string;
|
||||||
|
quantity: number;
|
||||||
|
powerPerUnit: number;
|
||||||
|
simultaneityFactor: number;
|
||||||
|
cosPhi?: number;
|
||||||
|
remark?: string;
|
||||||
|
overriddenFields?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyMigrationCircuitInput {
|
||||||
|
circuit: {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName?: string;
|
||||||
|
sortOrder: number;
|
||||||
|
protectionType?: string;
|
||||||
|
protectionRatedCurrent?: number;
|
||||||
|
protectionCharacteristic?: string;
|
||||||
|
cableType?: string;
|
||||||
|
cableCrossSection?: string;
|
||||||
|
cableLength?: number;
|
||||||
|
voltage?: number;
|
||||||
|
controlRequirement?: string;
|
||||||
|
remark?: string;
|
||||||
|
rcdAssignment?: string;
|
||||||
|
terminalDesignation?: string;
|
||||||
|
status?: string;
|
||||||
|
isReserve?: boolean;
|
||||||
|
};
|
||||||
|
deviceRows: LegacyMigrationDeviceRowInput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyMigrationReportInput {
|
||||||
|
legacyConsumerCount: number;
|
||||||
|
createdCircuitCount: number;
|
||||||
|
createdDeviceRowCount: number;
|
||||||
|
duplicateGroupedCount: number;
|
||||||
|
generatedIdentifierCount: number;
|
||||||
|
unassignedRowCount: number;
|
||||||
|
warningsJson: string;
|
||||||
|
generatedIdentifiersJson: string;
|
||||||
|
duplicateGroupsJson: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationStore {
|
||||||
|
listSourceConsumersByCircuitList(
|
||||||
|
circuitListId: string
|
||||||
|
): Promise<LegacyConsumerSource[]>;
|
||||||
|
listMigratedConsumerIds(circuitListId: string): Promise<string[]>;
|
||||||
|
persistCircuitListMigration(input: {
|
||||||
|
circuitListId: string;
|
||||||
|
circuits: LegacyMigrationCircuitInput[];
|
||||||
|
report: LegacyMigrationReportInput;
|
||||||
|
}): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationDependencies {
|
||||||
|
circuitListReader: LegacyConsumerMigrationCircuitListReader;
|
||||||
|
sectionStore: LegacyConsumerMigrationSectionStore;
|
||||||
|
circuitReader: LegacyConsumerMigrationCircuitReader;
|
||||||
|
roomReader: LegacyConsumerMigrationRoomReader;
|
||||||
|
migrationStore: LegacyConsumerMigrationStore;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { ProjectLocationStructureProjectCommand } from "../models/project-location-structure-project-command.model.js";
|
||||||
|
import type {
|
||||||
|
AppendedProjectRevision,
|
||||||
|
ProjectRevisionSource,
|
||||||
|
} from "./project-revision.store.js";
|
||||||
|
|
||||||
|
export interface ExecuteProjectLocationStructureCommandInput {
|
||||||
|
projectId: string;
|
||||||
|
expectedRevision: number;
|
||||||
|
source: ProjectRevisionSource;
|
||||||
|
description?: string;
|
||||||
|
actorId?: string;
|
||||||
|
historyTargetChangeSetId?: string;
|
||||||
|
command: ProjectLocationStructureProjectCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutedProjectLocationStructureCommand {
|
||||||
|
revision: AppendedProjectRevision;
|
||||||
|
inverse: ProjectLocationStructureProjectCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectLocationStructureProjectCommandStore {
|
||||||
|
execute(
|
||||||
|
input: ExecuteProjectLocationStructureCommandInput
|
||||||
|
): ExecutedProjectLocationStructureCommand;
|
||||||
|
}
|
||||||
@@ -24,8 +24,3 @@ export interface AppendedProjectRevision {
|
|||||||
revisionNumber: number;
|
revisionNumber: number;
|
||||||
createdAtIso: string;
|
createdAtIso: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectRevisionStore {
|
|
||||||
getCurrentRevision(projectId: string): number | null;
|
|
||||||
append(input: AppendProjectRevisionInput): AppendedProjectRevision;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
export interface CircuitNumberingSectionReader {
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
findById(
|
||||||
|
sectionId: string
|
||||||
|
): Promise<{ prefix: string } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CircuitNumberingCircuitReader {
|
||||||
|
listBySection(
|
||||||
|
sectionId: string
|
||||||
|
): Promise<Array<{ equipmentIdentifier: string }>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CircuitNumberingDependencies {
|
||||||
|
sectionRepository: CircuitNumberingSectionReader;
|
||||||
|
circuitRepository: CircuitNumberingCircuitReader;
|
||||||
|
}
|
||||||
|
|
||||||
function parseSuffix(equipmentIdentifier: string, prefix: string): number | null {
|
function parseSuffix(equipmentIdentifier: string, prefix: string): number | null {
|
||||||
if (!equipmentIdentifier.startsWith(prefix)) {
|
if (!equipmentIdentifier.startsWith(prefix)) {
|
||||||
@@ -13,15 +27,12 @@ function parseSuffix(equipmentIdentifier: string, prefix: string): number | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CircuitNumberingService {
|
export class CircuitNumberingService {
|
||||||
private readonly sectionRepository: Pick<CircuitSectionRepository, "findById">;
|
private readonly sectionRepository: CircuitNumberingSectionReader;
|
||||||
private readonly circuitRepository: Pick<CircuitRepository, "listBySection">;
|
private readonly circuitRepository: CircuitNumberingCircuitReader;
|
||||||
|
|
||||||
constructor(deps?: {
|
constructor(deps: CircuitNumberingDependencies) {
|
||||||
sectionRepository?: Pick<CircuitSectionRepository, "findById">;
|
this.sectionRepository = deps.sectionRepository;
|
||||||
circuitRepository?: Pick<CircuitRepository, "listBySection">;
|
this.circuitRepository = deps.circuitRepository;
|
||||||
}) {
|
|
||||||
this.sectionRepository = deps?.sectionRepository ?? new CircuitSectionRepository();
|
|
||||||
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextIdentifier(sectionId: string) {
|
async getNextIdentifier(sectionId: string) {
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
|
||||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
|
||||||
import {
|
|
||||||
LegacyConsumerMigrationRepository,
|
|
||||||
type LegacyMigrationCircuitPersistenceInput,
|
|
||||||
} from "../../db/repositories/legacy-consumer-migration.repository.js";
|
|
||||||
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
|
||||||
import type { LegacyMigrationReport } from "../models/circuit-tree.model.js";
|
import type { LegacyMigrationReport } from "../models/circuit-tree.model.js";
|
||||||
|
import type {
|
||||||
|
LegacyConsumerMigrationDependencies,
|
||||||
|
LegacyConsumerSource,
|
||||||
|
LegacyMigrationCircuitInput,
|
||||||
|
} from "../ports/legacy-consumer-migration.store.js";
|
||||||
import {
|
import {
|
||||||
inferSectionKeyFromEquipmentIdentifier,
|
inferSectionKeyFromEquipmentIdentifier,
|
||||||
inferSectionKeyFromLegacyInput,
|
inferSectionKeyFromLegacyInput,
|
||||||
normalizeCircuitNumber,
|
normalizeCircuitNumber,
|
||||||
} from "./legacy-consumer-migration-planner.js";
|
} from "./legacy-consumer-migration-planner.js";
|
||||||
|
|
||||||
type LegacyConsumerRow = Awaited<
|
function parseEquipmentSequence(
|
||||||
ReturnType<
|
equipmentIdentifier: string,
|
||||||
LegacyConsumerMigrationRepository["listSourceConsumersByCircuitList"]
|
prefix: string
|
||||||
>
|
): number | null {
|
||||||
>[number];
|
|
||||||
|
|
||||||
function parseEquipmentSequence(equipmentIdentifier: string, prefix: string): number | null {
|
|
||||||
if (!equipmentIdentifier.startsWith(prefix)) {
|
if (!equipmentIdentifier.startsWith(prefix)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -30,43 +24,45 @@ function parseEquipmentSequence(equipmentIdentifier: string, prefix: string): nu
|
|||||||
return Number(suffix);
|
return Number(suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class LegacyConsumerMigrationService {
|
export class LegacyConsumerMigrationService {
|
||||||
private readonly circuitListRepository = new CircuitListRepository();
|
|
||||||
private readonly sectionRepository = new CircuitSectionRepository();
|
|
||||||
private readonly circuitRepository = new CircuitRepository();
|
|
||||||
private readonly roomRepository = new RoomRepository();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly migrationRepository: LegacyConsumerMigrationRepository
|
private readonly dependencies: LegacyConsumerMigrationDependencies
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async migrateCircuitList(projectId: string, circuitListId: string): Promise<LegacyMigrationReport> {
|
async migrateCircuitList(
|
||||||
|
projectId: string,
|
||||||
|
circuitListId: string
|
||||||
|
): Promise<LegacyMigrationReport> {
|
||||||
// Migration is additive: legacy consumers are preserved, and circuit-first entities are created
|
// Migration is additive: legacy consumers are preserved, and circuit-first entities are created
|
||||||
// with mapping records so transition remains auditable and reversible.
|
// with mapping records so transition remains auditable and reversible.
|
||||||
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
const list = await this.dependencies.circuitListReader.findById(
|
||||||
|
projectId,
|
||||||
|
circuitListId
|
||||||
|
);
|
||||||
if (!list) {
|
if (!list) {
|
||||||
throw new Error("Circuit list not found in project.");
|
throw new Error("Circuit list not found in project.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sectionRepository.createDefaults(circuitListId);
|
await this.dependencies.sectionStore.createDefaults(circuitListId);
|
||||||
const sections = await this.sectionRepository.listByCircuitList(circuitListId);
|
const sections =
|
||||||
|
await this.dependencies.sectionStore.listByCircuitList(circuitListId);
|
||||||
const sectionByKey = new Map(sections.map((section) => [section.key, section]));
|
const sectionByKey = new Map(sections.map((section) => [section.key, section]));
|
||||||
const unassignedSection = sectionByKey.get("unassigned");
|
const unassignedSection = sectionByKey.get("unassigned");
|
||||||
if (!unassignedSection) {
|
if (!unassignedSection) {
|
||||||
throw new Error("Unassigned section is required.");
|
throw new Error("Unassigned section is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCircuits = await this.circuitRepository.listByCircuitList(circuitListId);
|
const existingCircuits =
|
||||||
|
await this.dependencies.circuitReader.listByCircuitList(circuitListId);
|
||||||
const usedEquipmentIdentifiers = new Set(
|
const usedEquipmentIdentifiers = new Set(
|
||||||
existingCircuits.map((circuit) => circuit.equipmentIdentifier.toUpperCase())
|
existingCircuits.map((circuit) => circuit.equipmentIdentifier.toUpperCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
const legacyConsumers =
|
const legacyConsumers =
|
||||||
await this.migrationRepository.listSourceConsumersByCircuitList(
|
await this.dependencies.migrationStore.listSourceConsumersByCircuitList(
|
||||||
circuitListId
|
circuitListId
|
||||||
);
|
);
|
||||||
const rooms = await this.roomRepository.listByProject(projectId);
|
const rooms = await this.dependencies.roomReader.listByProject(projectId);
|
||||||
const roomById = new Map(rooms.map((room) => [room.id, room]));
|
const roomById = new Map(rooms.map((room) => [room.id, room]));
|
||||||
|
|
||||||
const report: LegacyMigrationReport = {
|
const report: LegacyMigrationReport = {
|
||||||
@@ -82,14 +78,16 @@ export class LegacyConsumerMigrationService {
|
|||||||
|
|
||||||
// Idempotency guard: skip consumers already mapped in previous migration run.
|
// Idempotency guard: skip consumers already mapped in previous migration run.
|
||||||
const migratedConsumerIds = new Set(
|
const migratedConsumerIds = new Set(
|
||||||
await this.migrationRepository.listMigratedConsumerIds(circuitListId)
|
await this.dependencies.migrationStore.listMigratedConsumerIds(
|
||||||
|
circuitListId
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const consumersToMigrate = legacyConsumers.filter((consumer) => !migratedConsumerIds.has(consumer.id));
|
const consumersToMigrate = legacyConsumers.filter((consumer) => !migratedConsumerIds.has(consumer.id));
|
||||||
|
|
||||||
// Legacy rows are grouped by normalized circuit number so duplicates become
|
// Legacy rows are grouped by normalized circuit number so duplicates become
|
||||||
// multiple device rows within one circuit instead of duplicate circuits.
|
// multiple device rows within one circuit instead of duplicate circuits.
|
||||||
const byNormalizedCircuitNumber = new Map<string, LegacyConsumerRow[]>();
|
const byNormalizedCircuitNumber = new Map<string, LegacyConsumerSource[]>();
|
||||||
const withoutNormalizedCircuitNumber: LegacyConsumerRow[] = [];
|
const withoutNormalizedCircuitNumber: LegacyConsumerSource[] = [];
|
||||||
for (const consumer of consumersToMigrate) {
|
for (const consumer of consumersToMigrate) {
|
||||||
const normalized = normalizeCircuitNumber(consumer.circuitNumber ?? null);
|
const normalized = normalizeCircuitNumber(consumer.circuitNumber ?? null);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -110,7 +108,7 @@ export class LegacyConsumerMigrationService {
|
|||||||
|
|
||||||
const groups: Array<{
|
const groups: Array<{
|
||||||
equipmentIdentifier: string | null;
|
equipmentIdentifier: string | null;
|
||||||
consumers: LegacyConsumerRow[];
|
consumers: LegacyConsumerSource[];
|
||||||
inferredSectionKey: string | null;
|
inferredSectionKey: string | null;
|
||||||
isGeneratedIdentifier: boolean;
|
isGeneratedIdentifier: boolean;
|
||||||
}> = [];
|
}> = [];
|
||||||
@@ -152,7 +150,7 @@ export class LegacyConsumerMigrationService {
|
|||||||
maxBySectionPrefix.set(section.prefix.toUpperCase(), Math.max(current, sequence));
|
maxBySectionPrefix.set(section.prefix.toUpperCase(), Math.max(current, sequence));
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrationCircuits: LegacyMigrationCircuitPersistenceInput[] = [];
|
const migrationCircuits: LegacyMigrationCircuitInput[] = [];
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const representative = group.consumers[0];
|
const representative = group.consumers[0];
|
||||||
let section = group.inferredSectionKey ? sectionByKey.get(group.inferredSectionKey) : null;
|
let section = group.inferredSectionKey ? sectionByKey.get(group.inferredSectionKey) : null;
|
||||||
@@ -181,7 +179,7 @@ export class LegacyConsumerMigrationService {
|
|||||||
|
|
||||||
usedEquipmentIdentifiers.add(equipmentIdentifier.toUpperCase());
|
usedEquipmentIdentifiers.add(equipmentIdentifier.toUpperCase());
|
||||||
|
|
||||||
const deviceRows: LegacyMigrationCircuitPersistenceInput["deviceRows"] = [];
|
const deviceRows: LegacyMigrationCircuitInput["deviceRows"] = [];
|
||||||
const circuitSortOrder = nextSortOrder;
|
const circuitSortOrder = nextSortOrder;
|
||||||
nextSortOrder += 10;
|
nextSortOrder += 10;
|
||||||
|
|
||||||
@@ -250,7 +248,7 @@ export class LegacyConsumerMigrationService {
|
|||||||
report.warnings.push(`Consumer ${row.consumerId} was migrated into unassigned section.`);
|
report.warnings.push(`Consumer ${row.consumerId} was migrated into unassigned section.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.migrationRepository.persistCircuitListMigration({
|
this.dependencies.migrationStore.persistCircuitListMigration({
|
||||||
circuitListId,
|
circuitListId,
|
||||||
circuits: migrationCircuits,
|
circuits: migrationCircuits,
|
||||||
report: {
|
report: {
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ import {
|
|||||||
assertSerializedProjectCommand,
|
assertSerializedProjectCommand,
|
||||||
type SerializedProjectCommand,
|
type SerializedProjectCommand,
|
||||||
} from "../models/project-command.model.js";
|
} from "../models/project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertProjectFloorDeleteProjectCommand,
|
||||||
|
assertProjectFloorInsertProjectCommand,
|
||||||
|
assertProjectRoomDeleteProjectCommand,
|
||||||
|
assertProjectRoomInsertProjectCommand,
|
||||||
|
projectFloorDeleteCommandType,
|
||||||
|
projectFloorInsertCommandType,
|
||||||
|
projectRoomDeleteCommandType,
|
||||||
|
projectRoomInsertCommandType,
|
||||||
|
} from "../models/project-location-structure-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
assertProjectStateRestoreCommand,
|
assertProjectStateRestoreCommand,
|
||||||
projectStateRestoreCommandType,
|
projectStateRestoreCommandType,
|
||||||
@@ -87,6 +97,7 @@ import type {
|
|||||||
ProjectHistoryDirection,
|
ProjectHistoryDirection,
|
||||||
ProjectHistoryStore,
|
ProjectHistoryStore,
|
||||||
} from "../ports/project-history.store.js";
|
} from "../ports/project-history.store.js";
|
||||||
|
import type { ProjectLocationStructureProjectCommandStore } from "../ports/project-location-structure-project-command.store.js";
|
||||||
import type { ProjectDeviceProjectCommandStore } from "../ports/project-device-project-command.store.js";
|
import type { ProjectDeviceProjectCommandStore } from "../ports/project-device-project-command.store.js";
|
||||||
import type { ProjectDeviceRowSyncProjectCommandStore } from "../ports/project-device-row-sync-project-command.store.js";
|
import type { ProjectDeviceRowSyncProjectCommandStore } from "../ports/project-device-row-sync-project-command.store.js";
|
||||||
import type { ProjectDeviceStructureProjectCommandStore } from "../ports/project-device-structure-project-command.store.js";
|
import type { ProjectDeviceStructureProjectCommandStore } from "../ports/project-device-structure-project-command.store.js";
|
||||||
@@ -112,6 +123,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||||||
private readonly deviceRowMoveStore: CircuitDeviceRowMoveProjectCommandStore,
|
private readonly deviceRowMoveStore: CircuitDeviceRowMoveProjectCommandStore,
|
||||||
private readonly circuitStructureStore: CircuitStructureProjectCommandStore,
|
private readonly circuitStructureStore: CircuitStructureProjectCommandStore,
|
||||||
private readonly distributionBoardStructureStore: DistributionBoardStructureProjectCommandStore,
|
private readonly distributionBoardStructureStore: DistributionBoardStructureProjectCommandStore,
|
||||||
|
private readonly projectLocationStructureStore: ProjectLocationStructureProjectCommandStore,
|
||||||
private readonly circuitSectionReorderStore: CircuitSectionReorderProjectCommandStore,
|
private readonly circuitSectionReorderStore: CircuitSectionReorderProjectCommandStore,
|
||||||
private readonly circuitSectionRenumberStore: CircuitSectionRenumberProjectCommandStore,
|
private readonly circuitSectionRenumberStore: CircuitSectionRenumberProjectCommandStore,
|
||||||
private readonly projectDeviceStructureStore: ProjectDeviceStructureProjectCommandStore,
|
private readonly projectDeviceStructureStore: ProjectDeviceStructureProjectCommandStore,
|
||||||
@@ -273,6 +285,34 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||||||
command: input.command,
|
command: input.command,
|
||||||
}).revision;
|
}).revision;
|
||||||
}
|
}
|
||||||
|
case projectFloorInsertCommandType: {
|
||||||
|
assertProjectFloorInsertProjectCommand(input.command);
|
||||||
|
return this.projectLocationStructureStore.execute({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
|
case projectFloorDeleteCommandType: {
|
||||||
|
assertProjectFloorDeleteProjectCommand(input.command);
|
||||||
|
return this.projectLocationStructureStore.execute({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
|
case projectRoomInsertCommandType: {
|
||||||
|
assertProjectRoomInsertProjectCommand(input.command);
|
||||||
|
return this.projectLocationStructureStore.execute({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
|
case projectRoomDeleteCommandType: {
|
||||||
|
assertProjectRoomDeleteProjectCommand(input.command);
|
||||||
|
return this.projectLocationStructureStore.execute({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
case circuitSectionReorderCommandType: {
|
case circuitSectionReorderCommandType: {
|
||||||
assertCircuitSectionReorderProjectCommand(input.command);
|
assertCircuitSectionReorderProjectCommand(input.command);
|
||||||
return this.circuitSectionReorderStore.execute({
|
return this.circuitSectionReorderStore.execute({
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
|
||||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
|
||||||
import {
|
import {
|
||||||
createProjectDeviceRowSyncProjectCommand,
|
createProjectDeviceRowSyncProjectCommand,
|
||||||
projectDeviceSyncRowSnapshotFields,
|
projectDeviceSyncRowSnapshotFields,
|
||||||
@@ -20,15 +18,44 @@ export {
|
|||||||
serializeOverriddenFields,
|
serializeOverriddenFields,
|
||||||
} from "./project-device-overrides.js";
|
} from "./project-device-overrides.js";
|
||||||
|
|
||||||
type ProjectDevice = NonNullable<Awaited<ReturnType<ProjectDeviceRepository["findById"]>>>;
|
export type ProjectDeviceSyncSource = {
|
||||||
type LinkedRow = Awaited<ReturnType<CircuitDeviceRowRepository["listLinkedByProjectDevice"]>>[number];
|
id: string;
|
||||||
|
} & Pick<ProjectDeviceSyncRowSnapshot, ProjectDeviceSyncField>;
|
||||||
|
|
||||||
type SyncDependencies = {
|
export type LinkedProjectDeviceRow = ProjectDeviceSyncRowSnapshot & {
|
||||||
projectDeviceRepository: Pick<ProjectDeviceRepository, "findById">;
|
id: string;
|
||||||
deviceRowRepository: Pick<CircuitDeviceRowRepository, "listLinkedByProjectDevice">;
|
circuitId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
circuitDisplayName: string | null;
|
||||||
|
circuitListId: string;
|
||||||
|
circuitListName: string;
|
||||||
|
distributionBoardId: string;
|
||||||
|
distributionBoardName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function sourceValue(projectDevice: ProjectDevice, field: ProjectDeviceSyncField) {
|
export interface ProjectDeviceSyncSourceReader {
|
||||||
|
findById(
|
||||||
|
projectId: string,
|
||||||
|
projectDeviceId: string
|
||||||
|
): Promise<ProjectDeviceSyncSource | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkedProjectDeviceRowReader {
|
||||||
|
listLinkedByProjectDevice(
|
||||||
|
projectId: string,
|
||||||
|
projectDeviceId: string
|
||||||
|
): Promise<LinkedProjectDeviceRow[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectDeviceSyncDependencies {
|
||||||
|
projectDeviceRepository: ProjectDeviceSyncSourceReader;
|
||||||
|
deviceRowRepository: LinkedProjectDeviceRowReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceValue(
|
||||||
|
projectDevice: ProjectDeviceSyncSource,
|
||||||
|
field: ProjectDeviceSyncField
|
||||||
|
) {
|
||||||
return projectDevice[field];
|
return projectDevice[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +63,10 @@ function valuesEqual(left: unknown, right: unknown) {
|
|||||||
return (left ?? null) === (right ?? null);
|
return (left ?? null) === (right ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDifferences(projectDevice: ProjectDevice, row: LinkedRow) {
|
function buildDifferences(
|
||||||
|
projectDevice: ProjectDeviceSyncSource,
|
||||||
|
row: LinkedProjectDeviceRow
|
||||||
|
) {
|
||||||
return projectDeviceSyncFields
|
return projectDeviceSyncFields
|
||||||
.filter((field) => !valuesEqual(row[field], sourceValue(projectDevice, field)))
|
.filter((field) => !valuesEqual(row[field], sourceValue(projectDevice, field)))
|
||||||
.map((field) => ({
|
.map((field) => ({
|
||||||
@@ -48,12 +78,12 @@ function buildDifferences(projectDevice: ProjectDevice, row: LinkedRow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectDeviceSyncService {
|
export class ProjectDeviceSyncService {
|
||||||
private readonly projectDeviceRepository: SyncDependencies["projectDeviceRepository"];
|
private readonly projectDeviceRepository: ProjectDeviceSyncSourceReader;
|
||||||
private readonly deviceRowRepository: SyncDependencies["deviceRowRepository"];
|
private readonly deviceRowRepository: LinkedProjectDeviceRowReader;
|
||||||
|
|
||||||
constructor(deps?: Partial<SyncDependencies>) {
|
constructor(deps: ProjectDeviceSyncDependencies) {
|
||||||
this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository();
|
this.projectDeviceRepository = deps.projectDeviceRepository;
|
||||||
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
|
this.deviceRowRepository = deps.deviceRowRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPreview(projectId: string, projectDeviceId: string) {
|
async getPreview(projectId: string, projectDeviceId: string) {
|
||||||
@@ -154,10 +184,15 @@ export class ProjectDeviceSyncService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveSelectedRows(linkedRows: LinkedRow[], rowIds: string[]) {
|
private resolveSelectedRows(
|
||||||
|
linkedRows: LinkedProjectDeviceRow[],
|
||||||
|
rowIds: string[]
|
||||||
|
) {
|
||||||
const uniqueRowIds = [...new Set(rowIds)];
|
const uniqueRowIds = [...new Set(rowIds)];
|
||||||
const byId = new Map(linkedRows.map((row) => [row.id, row]));
|
const byId = new Map(linkedRows.map((row) => [row.id, row]));
|
||||||
const selectedRows = uniqueRowIds.map((rowId) => byId.get(rowId)).filter(Boolean) as LinkedRow[];
|
const selectedRows = uniqueRowIds
|
||||||
|
.map((rowId) => byId.get(rowId))
|
||||||
|
.filter(Boolean) as LinkedProjectDeviceRow[];
|
||||||
if (selectedRows.length !== uniqueRowIds.length) {
|
if (selectedRows.length !== uniqueRowIds.length) {
|
||||||
throw new Error("One or more rows are not linked to this project device.");
|
throw new Error("One or more rows are not linked to this project device.");
|
||||||
}
|
}
|
||||||
@@ -166,7 +201,9 @@ export class ProjectDeviceSyncService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toSyncSnapshot(row: LinkedRow): ProjectDeviceSyncRowSnapshot {
|
function toSyncSnapshot(
|
||||||
|
row: LinkedProjectDeviceRow
|
||||||
|
): ProjectDeviceSyncRowSnapshot {
|
||||||
return {
|
return {
|
||||||
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ export interface FloorDto {
|
|||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectFloorCommandResultDto
|
||||||
|
extends ProjectCommandResultDto {
|
||||||
|
floor: FloorDto;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RoomDto {
|
export interface RoomDto {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -146,6 +151,11 @@ export interface ProjectDeviceDto {
|
|||||||
voltageV: number | null;
|
voltageV: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectRoomCommandResultDto
|
||||||
|
extends ProjectCommandResultDto {
|
||||||
|
room: RoomDto;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectDeviceCommandResultDto
|
export interface ProjectDeviceCommandResultDto
|
||||||
extends ProjectCommandResultDto {
|
extends ProjectCommandResultDto {
|
||||||
projectDevice: ProjectDeviceDto;
|
projectDevice: ProjectDeviceDto;
|
||||||
|
|||||||
+26
-10
@@ -12,11 +12,13 @@ import type {
|
|||||||
ProjectCommandResultDto,
|
ProjectCommandResultDto,
|
||||||
ProjectDeviceCommandResultDto,
|
ProjectDeviceCommandResultDto,
|
||||||
ProjectDeviceDto,
|
ProjectDeviceDto,
|
||||||
|
ProjectFloorCommandResultDto,
|
||||||
ProjectHistoryStateDto,
|
ProjectHistoryStateDto,
|
||||||
ProjectRevisionPageDto,
|
ProjectRevisionPageDto,
|
||||||
ProjectSnapshotMetadataDto,
|
ProjectSnapshotMetadataDto,
|
||||||
ProjectSnapshotRestoreResultDto,
|
ProjectSnapshotRestoreResultDto,
|
||||||
ProjectSettingsCommandResultDto,
|
ProjectSettingsCommandResultDto,
|
||||||
|
ProjectRoomCommandResultDto,
|
||||||
ProjectDeviceSyncCommandResultDto,
|
ProjectDeviceSyncCommandResultDto,
|
||||||
ProjectDeviceSyncPreviewDto,
|
ProjectDeviceSyncPreviewDto,
|
||||||
ProjectDto,
|
ProjectDto,
|
||||||
@@ -454,22 +456,36 @@ export function listFloors(projectId: string) {
|
|||||||
return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
|
return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFloor(projectId: string, input: CreateFloorInput) {
|
export function createFloor(
|
||||||
return request<FloorDto>(`/api/projects/${projectId}/floors`, {
|
projectId: string,
|
||||||
method: "POST",
|
input: CreateFloorInput,
|
||||||
body: JSON.stringify(input),
|
expectedRevision: number
|
||||||
});
|
) {
|
||||||
|
return request<ProjectFloorCommandResultDto>(
|
||||||
|
`/api/projects/${projectId}/floors`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ ...input, expectedRevision }),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listRooms(projectId: string) {
|
export function listRooms(projectId: string) {
|
||||||
return request<RoomDto[]>(`/api/projects/${projectId}/rooms`);
|
return request<RoomDto[]>(`/api/projects/${projectId}/rooms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRoom(projectId: string, input: CreateRoomInput) {
|
export function createRoom(
|
||||||
return request<RoomDto>(`/api/projects/${projectId}/rooms`, {
|
projectId: string,
|
||||||
method: "POST",
|
input: CreateRoomInput,
|
||||||
body: JSON.stringify(input),
|
expectedRevision: number
|
||||||
});
|
) {
|
||||||
|
return request<ProjectRoomCommandResultDto>(
|
||||||
|
`/api/projects/${projectId}/rooms`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ ...input, expectedRevision }),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listGlobalDevices() {
|
export function listGlobalDevices() {
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ const commandTypeLabels: Record<string, string> = {
|
|||||||
"project.update-settings": "Projekteinstellungen bearbeitet",
|
"project.update-settings": "Projekteinstellungen bearbeitet",
|
||||||
"distribution-board.insert": "Verteilung angelegt",
|
"distribution-board.insert": "Verteilung angelegt",
|
||||||
"distribution-board.delete": "Verteilung entfernt",
|
"distribution-board.delete": "Verteilung entfernt",
|
||||||
|
"project-floor.insert": "Geschoss angelegt",
|
||||||
|
"project-floor.delete": "Geschoss entfernt",
|
||||||
|
"project-room.insert": "Raum angelegt",
|
||||||
|
"project-room.delete": "Raum entfernt",
|
||||||
"project.restore-state": "Projektstand wiederhergestellt",
|
"project.restore-state": "Projektstand wiederhergestellt",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { db } from "../../db/client.js";
|
||||||
|
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
||||||
|
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||||
|
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||||
|
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||||
|
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
||||||
|
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
||||||
|
import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js";
|
||||||
|
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
||||||
|
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
||||||
|
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
||||||
|
|
||||||
|
export const circuitDeviceRowRepository =
|
||||||
|
new CircuitDeviceRowRepository(db);
|
||||||
|
export const circuitListRepository = new CircuitListRepository(db);
|
||||||
|
export const circuitSectionRepository = new CircuitSectionRepository(db);
|
||||||
|
export const circuitRepository = new CircuitRepository(db);
|
||||||
|
export const distributionBoardRepository =
|
||||||
|
new DistributionBoardRepository(db);
|
||||||
|
export const floorRepository = new FloorRepository(db);
|
||||||
|
export const globalDeviceRepository = new GlobalDeviceRepository(db);
|
||||||
|
export const projectDeviceRepository = new ProjectDeviceRepository(db);
|
||||||
|
export const projectRepository = new ProjectRepository(db);
|
||||||
|
export const roomRepository = new RoomRepository(db);
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
import { CircuitNumberingService } from "../../domain/services/circuit-numbering.service.js";
|
import { CircuitNumberingService } from "../../domain/services/circuit-numbering.service.js";
|
||||||
|
import {
|
||||||
|
circuitRepository,
|
||||||
|
circuitSectionRepository,
|
||||||
|
} from "./application-repositories.js";
|
||||||
|
|
||||||
export const circuitNumberingService = new CircuitNumberingService();
|
export const circuitNumberingService = new CircuitNumberingService({
|
||||||
|
sectionRepository: circuitSectionRepository,
|
||||||
|
circuitRepository,
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { CircuitSectionRenumberProjectCommandRepository } from "../../db/reposit
|
|||||||
import { CircuitStructureProjectCommandRepository } from "../../db/repositories/circuit-structure-project-command.repository.js";
|
import { CircuitStructureProjectCommandRepository } from "../../db/repositories/circuit-structure-project-command.repository.js";
|
||||||
import { DistributionBoardStructureProjectCommandRepository } from "../../db/repositories/distribution-board-structure-project-command.repository.js";
|
import { DistributionBoardStructureProjectCommandRepository } from "../../db/repositories/distribution-board-structure-project-command.repository.js";
|
||||||
import { ProjectHistoryRepository } from "../../db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../../db/repositories/project-history.repository.js";
|
||||||
|
import { ProjectLocationStructureProjectCommandRepository } from "../../db/repositories/project-location-structure-project-command.repository.js";
|
||||||
import { ProjectDeviceProjectCommandRepository } from "../../db/repositories/project-device-project-command.repository.js";
|
import { ProjectDeviceProjectCommandRepository } from "../../db/repositories/project-device-project-command.repository.js";
|
||||||
import { ProjectDeviceRowSyncProjectCommandRepository } from "../../db/repositories/project-device-row-sync-project-command.repository.js";
|
import { ProjectDeviceRowSyncProjectCommandRepository } from "../../db/repositories/project-device-row-sync-project-command.repository.js";
|
||||||
import { ProjectDeviceStructureProjectCommandRepository } from "../../db/repositories/project-device-structure-project-command.repository.js";
|
import { ProjectDeviceStructureProjectCommandRepository } from "../../db/repositories/project-device-structure-project-command.repository.js";
|
||||||
@@ -26,6 +27,8 @@ export const circuitStructureProjectCommandStore =
|
|||||||
new CircuitStructureProjectCommandRepository(db);
|
new CircuitStructureProjectCommandRepository(db);
|
||||||
export const distributionBoardStructureProjectCommandStore =
|
export const distributionBoardStructureProjectCommandStore =
|
||||||
new DistributionBoardStructureProjectCommandRepository(db);
|
new DistributionBoardStructureProjectCommandRepository(db);
|
||||||
|
export const projectLocationStructureProjectCommandStore =
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(db);
|
||||||
export const circuitSectionReorderProjectCommandStore =
|
export const circuitSectionReorderProjectCommandStore =
|
||||||
new CircuitSectionReorderProjectCommandRepository(db);
|
new CircuitSectionReorderProjectCommandRepository(db);
|
||||||
export const circuitSectionRenumberProjectCommandStore =
|
export const circuitSectionRenumberProjectCommandStore =
|
||||||
@@ -48,6 +51,7 @@ export const projectCommandService = new ProjectCommandService(
|
|||||||
circuitDeviceRowMoveProjectCommandStore,
|
circuitDeviceRowMoveProjectCommandStore,
|
||||||
circuitStructureProjectCommandStore,
|
circuitStructureProjectCommandStore,
|
||||||
distributionBoardStructureProjectCommandStore,
|
distributionBoardStructureProjectCommandStore,
|
||||||
|
projectLocationStructureProjectCommandStore,
|
||||||
circuitSectionReorderProjectCommandStore,
|
circuitSectionReorderProjectCommandStore,
|
||||||
circuitSectionRenumberProjectCommandStore,
|
circuitSectionRenumberProjectCommandStore,
|
||||||
projectDeviceStructureProjectCommandStore,
|
projectDeviceStructureProjectCommandStore,
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
import { ProjectDeviceSyncService } from "../../domain/services/project-device-sync.service.js";
|
import { ProjectDeviceSyncService } from "../../domain/services/project-device-sync.service.js";
|
||||||
|
import {
|
||||||
|
circuitDeviceRowRepository,
|
||||||
|
projectDeviceRepository,
|
||||||
|
} from "./application-repositories.js";
|
||||||
|
|
||||||
export const projectDeviceSyncService = new ProjectDeviceSyncService();
|
export const projectDeviceSyncService = new ProjectDeviceSyncService({
|
||||||
|
projectDeviceRepository,
|
||||||
|
deviceRowRepository: circuitDeviceRowRepository,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
import { circuitListRepository } from "../composition/application-repositories.js";
|
||||||
|
|
||||||
const circuitListRepository = new CircuitListRepository();
|
|
||||||
|
|
||||||
export async function listCircuitListsByProject(req: Request, res: Response) {
|
export async function listCircuitListsByProject(req: Request, res: Response) {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
|
||||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
|
||||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
|
||||||
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
|
||||||
import {
|
import {
|
||||||
calculateCircuitTotalPower,
|
calculateCircuitTotalPower,
|
||||||
calculateRowTotalPower,
|
calculateRowTotalPower,
|
||||||
} from "../../domain/calculations/circuit-power-calculation.js";
|
} from "../../domain/calculations/circuit-power-calculation.js";
|
||||||
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
|
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
|
||||||
|
import {
|
||||||
const circuitListRepository = new CircuitListRepository();
|
circuitDeviceRowRepository,
|
||||||
const circuitSectionRepository = new CircuitSectionRepository();
|
circuitListRepository,
|
||||||
const circuitRepository = new CircuitRepository();
|
circuitRepository,
|
||||||
const circuitDeviceRowRepository = new CircuitDeviceRowRepository();
|
circuitSectionRepository,
|
||||||
const projectRepository = new ProjectRepository();
|
projectRepository,
|
||||||
|
} from "../composition/application-repositories.js";
|
||||||
|
|
||||||
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
||||||
if (!(error instanceof Error)) {
|
if (!(error instanceof Error)) {
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { db } from "../../db/client.js";
|
|
||||||
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
|
||||||
import {
|
import {
|
||||||
createDistributionBoardInsertProjectCommand,
|
createDistributionBoardInsertProjectCommand,
|
||||||
createDistributionBoardStructureSnapshot,
|
createDistributionBoardStructureSnapshot,
|
||||||
} from "../../domain/models/distribution-board-structure-project-command.model.js";
|
} from "../../domain/models/distribution-board-structure-project-command.model.js";
|
||||||
import { createDistributionBoardSchema } from "../../shared/validation/project-structure.schemas.js";
|
import { createDistributionBoardSchema } from "../../shared/validation/project-structure.schemas.js";
|
||||||
import { projectCommandService } from "../composition/project-command-stores.js";
|
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||||
|
import { distributionBoardRepository } from "../composition/application-repositories.js";
|
||||||
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||||
|
|
||||||
const distributionBoardRepository = new DistributionBoardRepository(db);
|
|
||||||
|
|
||||||
export async function listDistributionBoardsByProject(req: Request, res: Response) {
|
export async function listDistributionBoardsByProject(req: Request, res: Response) {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
if (typeof projectId !== "string") {
|
if (typeof projectId !== "string") {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
import {
|
||||||
|
createProjectFloorInsertProjectCommand,
|
||||||
|
createProjectFloorSnapshot,
|
||||||
|
} from "../../domain/models/project-location-structure-project-command.model.js";
|
||||||
import { createFloorSchema } from "../../shared/validation/project-structure.schemas.js";
|
import { createFloorSchema } from "../../shared/validation/project-structure.schemas.js";
|
||||||
|
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||||
const floorRepository = new FloorRepository();
|
import { floorRepository } from "../composition/application-repositories.js";
|
||||||
|
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||||
|
|
||||||
export async function listFloorsByProject(req: Request, res: Response) {
|
export async function listFloorsByProject(req: Request, res: Response) {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
@@ -25,6 +29,24 @@ export async function createFloor(req: Request, res: Response) {
|
|||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
}
|
}
|
||||||
|
|
||||||
const floor = await floorRepository.create(projectId, parsed.data.name);
|
const existingFloors = await floorRepository.listByProject(projectId);
|
||||||
return res.status(201).json(floor);
|
const nextSortOrder = existingFloors.length
|
||||||
|
? Math.max(...existingFloors.map((floor) => floor.sortOrder)) + 1
|
||||||
|
: 0;
|
||||||
|
const floor = createProjectFloorSnapshot(
|
||||||
|
projectId,
|
||||||
|
parsed.data.name,
|
||||||
|
nextSortOrder
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const result = projectCommandService.executeUser({
|
||||||
|
projectId,
|
||||||
|
expectedRevision: parsed.data.expectedRevision,
|
||||||
|
description: "Geschoss anlegen",
|
||||||
|
command: createProjectFloorInsertProjectCommand(floor),
|
||||||
|
});
|
||||||
|
return res.status(201).json({ ...result, floor });
|
||||||
|
} catch (error) {
|
||||||
|
return respondWithProjectCommandError(error, res);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js";
|
|
||||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
|
||||||
import {
|
import {
|
||||||
createGlobalDeviceSchema,
|
createGlobalDeviceSchema,
|
||||||
updateGlobalDeviceSchema,
|
updateGlobalDeviceSchema,
|
||||||
} from "../../shared/validation/global-device.schemas.js";
|
} from "../../shared/validation/global-device.schemas.js";
|
||||||
|
import {
|
||||||
const globalDeviceRepository = new GlobalDeviceRepository();
|
globalDeviceRepository,
|
||||||
const projectDeviceRepository = new ProjectDeviceRepository();
|
projectDeviceRepository,
|
||||||
|
} from "../composition/application-repositories.js";
|
||||||
|
|
||||||
export async function listGlobalDevices(_req: Request, res: Response) {
|
export async function listGlobalDevices(_req: Request, res: Response) {
|
||||||
const rows = await globalDeviceRepository.list();
|
const rows = await globalDeviceRepository.list();
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js";
|
|
||||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
|
||||||
import { createProjectDeviceUpdateProjectCommand } from "../../domain/models/project-device-project-command.model.js";
|
import { createProjectDeviceUpdateProjectCommand } from "../../domain/models/project-device-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
createProjectDeviceDeleteProjectCommand,
|
createProjectDeviceDeleteProjectCommand,
|
||||||
@@ -10,6 +8,10 @@ import {
|
|||||||
} from "../../domain/models/project-device-structure-project-command.model.js";
|
} from "../../domain/models/project-device-structure-project-command.model.js";
|
||||||
import { projectDeviceSyncService } from "../composition/project-device-sync-service.js";
|
import { projectDeviceSyncService } from "../composition/project-device-sync-service.js";
|
||||||
import { projectCommandService } from "../composition/project-command-stores.js";
|
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||||
|
import {
|
||||||
|
globalDeviceRepository,
|
||||||
|
projectDeviceRepository,
|
||||||
|
} from "../composition/application-repositories.js";
|
||||||
import {
|
import {
|
||||||
createProjectDeviceCommandSchema,
|
createProjectDeviceCommandSchema,
|
||||||
disconnectProjectDeviceRowsSchema,
|
disconnectProjectDeviceRowsSchema,
|
||||||
@@ -20,8 +22,6 @@ import {
|
|||||||
import type { CreateProjectDeviceInput } from "../../shared/validation/project-device.schemas.js";
|
import type { CreateProjectDeviceInput } from "../../shared/validation/project-device.schemas.js";
|
||||||
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||||
|
|
||||||
const globalDeviceRepository = new GlobalDeviceRepository();
|
|
||||||
const projectDeviceRepository = new ProjectDeviceRepository();
|
|
||||||
export async function listProjectDevicesByProject(req: Request, res: Response) {
|
export async function listProjectDevicesByProject(req: Request, res: Response) {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
if (typeof projectId !== "string") {
|
if (typeof projectId !== "string") {
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
|
||||||
import {
|
import {
|
||||||
createProjectSchema,
|
createProjectSchema,
|
||||||
updateProjectSettingsSchema,
|
updateProjectSettingsSchema,
|
||||||
} from "../../shared/validation/project-structure.schemas.js";
|
} from "../../shared/validation/project-structure.schemas.js";
|
||||||
import { createProjectSettingsUpdateProjectCommand } from "../../domain/models/project-settings-project-command.model.js";
|
import { createProjectSettingsUpdateProjectCommand } from "../../domain/models/project-settings-project-command.model.js";
|
||||||
import { projectCommandService } from "../composition/project-command-stores.js";
|
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||||
|
import { projectRepository } from "../composition/application-repositories.js";
|
||||||
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||||
|
|
||||||
const projectRepository = new ProjectRepository();
|
|
||||||
|
|
||||||
export async function listProjects(_req: Request, res: Response) {
|
export async function listProjects(_req: Request, res: Response) {
|
||||||
const result = await projectRepository.list();
|
const result = await projectRepository.list();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
import {
|
||||||
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
createProjectRoomInsertProjectCommand,
|
||||||
|
createProjectRoomSnapshot,
|
||||||
|
} from "../../domain/models/project-location-structure-project-command.model.js";
|
||||||
import { createRoomSchema } from "../../shared/validation/project-structure.schemas.js";
|
import { createRoomSchema } from "../../shared/validation/project-structure.schemas.js";
|
||||||
|
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||||
const floorRepository = new FloorRepository();
|
import { roomRepository } from "../composition/application-repositories.js";
|
||||||
const roomRepository = new RoomRepository();
|
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||||
|
|
||||||
export async function listRoomsByProject(req: Request, res: Response) {
|
export async function listRoomsByProject(req: Request, res: Response) {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
@@ -27,13 +29,16 @@ export async function createRoom(req: Request, res: Response) {
|
|||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.data.floorId) {
|
const room = createProjectRoomSnapshot(projectId, parsed.data);
|
||||||
const hasValidFloor = await floorRepository.existsInProject(projectId, parsed.data.floorId);
|
try {
|
||||||
if (!hasValidFloor) {
|
const result = projectCommandService.executeUser({
|
||||||
return res.status(400).json({ error: "Floor does not belong to the provided project." });
|
projectId,
|
||||||
}
|
expectedRevision: parsed.data.expectedRevision,
|
||||||
|
description: "Raum anlegen",
|
||||||
|
command: createProjectRoomInsertProjectCommand(room),
|
||||||
|
});
|
||||||
|
return res.status(201).json({ ...result, room });
|
||||||
|
} catch (error) {
|
||||||
|
return respondWithProjectCommandError(error, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = await roomRepository.create(projectId, parsed.data);
|
|
||||||
return res.status(201).json(room);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,21 @@ export const createDistributionBoardSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export const createFloorSchema = z.object({
|
export const createFloorSchema = z
|
||||||
name: z.string().min(1),
|
.object({
|
||||||
});
|
expectedRevision: expectedProjectRevisionSchema,
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const createRoomSchema = z.object({
|
export const createRoomSchema = z
|
||||||
floorId: z.string().min(1).optional(),
|
.object({
|
||||||
roomNumber: z.string().min(1),
|
expectedRevision: expectedProjectRevisionSchema,
|
||||||
roomName: z.string().min(1),
|
floorId: z.string().trim().min(1).optional(),
|
||||||
});
|
roomNumber: z.string().trim().min(1),
|
||||||
|
roomName: z.string().trim().min(1),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
|
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
|
||||||
export type UpdateProjectSettingsInput = z.infer<
|
export type UpdateProjectSettingsInput = z.infer<
|
||||||
@@ -39,5 +45,3 @@ export type UpdateProjectSettingsInput = z.infer<
|
|||||||
export type CreateDistributionBoardInput = z.infer<
|
export type CreateDistributionBoardInput = z.infer<
|
||||||
typeof createDistributionBoardSchema
|
typeof createDistributionBoardSchema
|
||||||
>;
|
>;
|
||||||
export type CreateFloorInput = z.infer<typeof createFloorSchema>;
|
|
||||||
export type CreateRoomInput = z.infer<typeof createRoomSchema>;
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { CircuitDeviceRowMoveProjectCommandRepository } from "../src/db/repositories/circuit-device-row-move-project-command.repository.js";
|
import { CircuitDeviceRowMoveProjectCommandRepository } from "../src/db/repositories/circuit-device-row-move-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
@@ -39,7 +39,7 @@ function createTestDatabase(): TestFixture {
|
|||||||
{ id: "project-2", name: "Other project" },
|
{ id: "project-2", name: "Other project" },
|
||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
const boards = new DistributionBoardRepository(context.db);
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = boards.createWithCircuitListAndDefaultSections(
|
const board = boards.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { CircuitDeviceRowProjectCommandRepository } from "../src/db/repositories/circuit-device-row-project-command.repository.js";
|
import { CircuitDeviceRowProjectCommandRepository } from "../src/db/repositories/circuit-device-row-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
@@ -34,7 +34,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const boards = new DistributionBoardRepository(context.db);
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = boards.createWithCircuitListAndDefaultSections(
|
const board = boards.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { CircuitDeviceRowStructureProjectCommandRepository } from "../src/db/repositories/circuit-device-row-structure-project-command.repository.js";
|
import { CircuitDeviceRowStructureProjectCommandRepository } from "../src/db/repositories/circuit-device-row-structure-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
@@ -36,7 +36,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const boards = new DistributionBoardRepository(context.db);
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = boards.createWithCircuitListAndDefaultSections(
|
const board = boards.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe("circuit numbering service", () => {
|
|||||||
const service = new CircuitNumberingService({
|
const service = new CircuitNumberingService({
|
||||||
sectionRepository: {
|
sectionRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
return { id: "s1", prefix: "-2F" } as never;
|
return { prefix: "-2F" };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
@@ -18,7 +18,7 @@ describe("circuit numbering service", () => {
|
|||||||
{ equipmentIdentifier: "-2F5" },
|
{ equipmentIdentifier: "-2F5" },
|
||||||
{ equipmentIdentifier: "-2FX" },
|
{ equipmentIdentifier: "-2FX" },
|
||||||
{ equipmentIdentifier: "-1F9" },
|
{ equipmentIdentifier: "-1F9" },
|
||||||
] as never[];
|
];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-project-command.repository.js";
|
import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
@@ -38,7 +38,7 @@ function createTestFixture(): TestFixture {
|
|||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const repository = new DistributionBoardRepository(context.db);
|
const repository = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = repository.createWithCircuitListAndDefaultSections(
|
const board = repository.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/repositories/circuit-section-renumber-project-command.repository.js";
|
import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/repositories/circuit-section-renumber-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
@@ -35,7 +35,7 @@ function createTestDatabase(): TestFixture {
|
|||||||
{ id: "project-2", name: "Other project" },
|
{ id: "project-2", name: "Other project" },
|
||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
const boards = new DistributionBoardRepository(context.db);
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = boards.createWithCircuitListAndDefaultSections(
|
const board = boards.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { CircuitSectionReorderProjectCommandRepository } from "../src/db/repositories/circuit-section-reorder-project-command.repository.js";
|
import { CircuitSectionReorderProjectCommandRepository } from "../src/db/repositories/circuit-section-reorder-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
@@ -37,7 +37,7 @@ function createTestDatabase(): TestFixture {
|
|||||||
{ id: "project-2", name: "Other project" },
|
{ id: "project-2", name: "Other project" },
|
||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
const boards = new DistributionBoardRepository(context.db);
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = boards.createWithCircuitListAndDefaultSections(
|
const board = boards.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
@@ -45,7 +45,7 @@ function createTestDatabase(): TestFixture {
|
|||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const boards = new DistributionBoardRepository(context.db);
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = boards.createWithCircuitListAndDefaultSections(
|
const board = boards.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import {
|
|||||||
verifyDatabaseFile,
|
verifyDatabaseFile,
|
||||||
} from "../src/db/database-backup.js";
|
} from "../src/db/database-backup.js";
|
||||||
import { createDatabaseContext } from "../src/db/database-context.js";
|
import { createDatabaseContext } from "../src/db/database-context.js";
|
||||||
|
import { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js";
|
||||||
|
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
||||||
|
import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js";
|
||||||
|
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
||||||
|
import { FloorRepository } from "../src/db/repositories/floor.repository.js";
|
||||||
|
import { GlobalDeviceRepository } from "../src/db/repositories/global-device.repository.js";
|
||||||
|
import { ProjectDeviceRepository } from "../src/db/repositories/project-device.repository.js";
|
||||||
|
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
||||||
|
import { RoomRepository } from "../src/db/repositories/room.repository.js";
|
||||||
import { projects } from "../src/db/schema/projects.js";
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
|
|
||||||
describe("database backup and restore", () => {
|
describe("database backup and restore", () => {
|
||||||
@@ -86,3 +95,71 @@ describe("database backup and restore", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("application repository database contexts", () => {
|
||||||
|
it("keep reads and writes inside their injected database", async () => {
|
||||||
|
const first = createDatabaseContext(":memory:");
|
||||||
|
const second = createDatabaseContext(":memory:");
|
||||||
|
try {
|
||||||
|
for (const context of [first, second]) {
|
||||||
|
migrate(context.db, {
|
||||||
|
migrationsFolder: path.resolve("src", "db", "migrations"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstProjects = new ProjectRepository(first.db);
|
||||||
|
const secondProjects = new ProjectRepository(second.db);
|
||||||
|
const project = await firstProjects.create({ name: "Nur in Datenbank A" });
|
||||||
|
|
||||||
|
assert.equal((await firstProjects.list()).length, 1);
|
||||||
|
assert.deepEqual(await secondProjects.list(), []);
|
||||||
|
|
||||||
|
const firstGlobalDevices = new GlobalDeviceRepository(first.db);
|
||||||
|
const secondGlobalDevices = new GlobalDeviceRepository(second.db);
|
||||||
|
await firstGlobalDevices.create({
|
||||||
|
name: "Steckdose",
|
||||||
|
displayName: "Steckdose",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 0.2,
|
||||||
|
demandFactor: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal((await firstGlobalDevices.list()).length, 1);
|
||||||
|
assert.deepEqual(await secondGlobalDevices.list(), []);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
await new CircuitListRepository(first.db).listByProject(project.id),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await new CircuitSectionRepository(first.db).listByCircuitList(
|
||||||
|
"unknown-list"
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await new CircuitRepository(first.db).listByCircuitList("unknown-list"),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await new CircuitDeviceRowRepository(first.db).listByCircuitList([]),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await new FloorRepository(first.db).listByProject(project.id),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await new RoomRepository(first.db).listByProject(project.id),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await new ProjectDeviceRepository(first.db).listByProject(project.id),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
first.close();
|
||||||
|
second.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { describe, it } from "node:test";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
||||||
import {
|
|
||||||
createDatabaseContext,
|
|
||||||
type DatabaseContext,
|
|
||||||
} from "../src/db/database-context.js";
|
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
|
||||||
import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
|
||||||
import { distributionBoards } from "../src/db/schema/distribution-boards.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();
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("distribution board repository integration", () => {
|
|
||||||
it("commits board, circuit list and default sections together", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
const repository = new DistributionBoardRepository(context.db);
|
|
||||||
const board = repository.createWithCircuitListAndDefaultSections(
|
|
||||||
"project-1",
|
|
||||||
"UV-01"
|
|
||||||
);
|
|
||||||
|
|
||||||
const persistedBoards = context.db
|
|
||||||
.select()
|
|
||||||
.from(distributionBoards)
|
|
||||||
.where(eq(distributionBoards.id, board.id))
|
|
||||||
.all();
|
|
||||||
const persistedLists = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuitLists)
|
|
||||||
.where(eq(circuitLists.distributionBoardId, board.id))
|
|
||||||
.all();
|
|
||||||
const persistedSections = context.db
|
|
||||||
.select()
|
|
||||||
.from(circuitSections)
|
|
||||||
.where(eq(circuitSections.circuitListId, board.id))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
assert.deepEqual(persistedBoards, [board]);
|
|
||||||
assert.equal(persistedLists.length, 1);
|
|
||||||
assert.equal(persistedLists[0].name, "UV-01 Stromkreisliste");
|
|
||||||
assert.deepEqual(
|
|
||||||
persistedSections
|
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
|
||||||
.map((section) => [section.key, section.prefix]),
|
|
||||||
[
|
|
||||||
["lighting", "-1F"],
|
|
||||||
["single_phase", "-2F"],
|
|
||||||
["three_phase", "-3F"],
|
|
||||||
["unassigned", "-UF"],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rolls back board and circuit list when default section creation fails", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
context.sqlite.exec(`
|
|
||||||
CREATE TRIGGER fail_default_section_insert
|
|
||||||
BEFORE INSERT ON circuit_sections
|
|
||||||
BEGIN
|
|
||||||
SELECT RAISE(ABORT, 'forced section failure');
|
|
||||||
END;
|
|
||||||
`);
|
|
||||||
const repository = new DistributionBoardRepository(context.db);
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() =>
|
|
||||||
repository.createWithCircuitListAndDefaultSections(
|
|
||||||
"project-1",
|
|
||||||
"UV rollback"
|
|
||||||
),
|
|
||||||
/forced section failure/
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(context.db.select().from(distributionBoards).all().length, 0);
|
|
||||||
assert.equal(context.db.select().from(circuitLists).all().length, 0);
|
|
||||||
assert.equal(context.db.select().from(circuitSections).all().length, 0);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
createDatabaseContext,
|
createDatabaseContext,
|
||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js";
|
import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
@@ -23,7 +23,7 @@ function createTestDatabase() {
|
|||||||
migrationsFolder: path.resolve("src", "db", "migrations"),
|
migrationsFolder: path.resolve("src", "db", "migrations"),
|
||||||
});
|
});
|
||||||
context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run();
|
context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run();
|
||||||
const board = new DistributionBoardRepository(
|
const board = new DistributionBoardFixtureRepository(
|
||||||
context.db
|
context.db
|
||||||
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||||
const [section] = context.db
|
const [section] = context.db
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import type {
|
||||||
|
LegacyConsumerMigrationDependencies,
|
||||||
|
LegacyMigrationCircuitInput,
|
||||||
|
LegacyMigrationReportInput,
|
||||||
|
} from "../src/domain/ports/legacy-consumer-migration.store.js";
|
||||||
|
import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js";
|
||||||
|
|
||||||
|
describe("legacy consumer migration service", () => {
|
||||||
|
it("plans the migration through injected readers and persists one complete batch", async () => {
|
||||||
|
let defaultsCreatedFor: string | null = null;
|
||||||
|
let persisted:
|
||||||
|
| {
|
||||||
|
circuitListId: string;
|
||||||
|
circuits: LegacyMigrationCircuitInput[];
|
||||||
|
report: LegacyMigrationReportInput;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
const dependencies: LegacyConsumerMigrationDependencies = {
|
||||||
|
circuitListReader: {
|
||||||
|
async findById(projectId, circuitListId) {
|
||||||
|
assert.equal(projectId, "project-1");
|
||||||
|
assert.equal(circuitListId, "list-1");
|
||||||
|
return { id: circuitListId };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sectionStore: {
|
||||||
|
async createDefaults(circuitListId) {
|
||||||
|
defaultsCreatedFor = circuitListId;
|
||||||
|
},
|
||||||
|
async listByCircuitList() {
|
||||||
|
return [
|
||||||
|
{ id: "section-1", key: "single_phase", prefix: "-2F" },
|
||||||
|
{ id: "section-2", key: "unassigned", prefix: "-XF" },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
circuitReader: {
|
||||||
|
async listByCircuitList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
sectionId: "section-1",
|
||||||
|
equipmentIdentifier: "-2F2",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
roomReader: {
|
||||||
|
async listByProject() {
|
||||||
|
return [
|
||||||
|
{ id: "room-1", roomNumber: "1.01", roomName: "Besprechung" },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
migrationStore: {
|
||||||
|
async listSourceConsumersByCircuitList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "consumer-1",
|
||||||
|
projectDeviceId: null,
|
||||||
|
roomId: "room-1",
|
||||||
|
circuitNumber: null,
|
||||||
|
description: "Steckdosen Besprechung",
|
||||||
|
name: "Steckdose",
|
||||||
|
category: null,
|
||||||
|
deviceType: null,
|
||||||
|
phaseType: null,
|
||||||
|
tradeOrCostGroup: "KG 440",
|
||||||
|
protectionType: null,
|
||||||
|
protectionRatedCurrent: null,
|
||||||
|
protectionCharacteristic: null,
|
||||||
|
cableType: null,
|
||||||
|
cableCrossSection: null,
|
||||||
|
comment: null,
|
||||||
|
quantity: 2,
|
||||||
|
installedPowerPerUnitKw: 0.2,
|
||||||
|
demandFactor: 0.8,
|
||||||
|
voltageV: 230,
|
||||||
|
phaseCount: 1,
|
||||||
|
powerFactor: 0.95,
|
||||||
|
note: "Bestand",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
async listMigratedConsumerIds() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
persistCircuitListMigration(input) {
|
||||||
|
persisted = input;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const report = await new LegacyConsumerMigrationService(
|
||||||
|
dependencies
|
||||||
|
).migrateCircuitList("project-1", "list-1");
|
||||||
|
|
||||||
|
assert.equal(defaultsCreatedFor, "list-1");
|
||||||
|
assert.deepEqual(report.generatedIdentifiers, ["-2F3"]);
|
||||||
|
assert.equal(report.createdCircuitCount, 1);
|
||||||
|
assert.equal(report.createdDeviceRowCount, 1);
|
||||||
|
assert.equal(persisted?.circuitListId, "list-1");
|
||||||
|
assert.equal(persisted?.circuits[0].circuit.sectionId, "section-1");
|
||||||
|
assert.equal(
|
||||||
|
persisted?.circuits[0].circuit.equipmentIdentifier,
|
||||||
|
"-2F3"
|
||||||
|
);
|
||||||
|
assert.deepEqual(persisted?.circuits[0].deviceRows[0], {
|
||||||
|
linkedProjectDeviceId: undefined,
|
||||||
|
legacyConsumerId: "consumer-1",
|
||||||
|
sortOrder: 10,
|
||||||
|
name: "Steckdose",
|
||||||
|
displayName: "Steckdosen Besprechung",
|
||||||
|
phaseType: undefined,
|
||||||
|
connectionKind: undefined,
|
||||||
|
costGroup: "KG 440",
|
||||||
|
category: undefined,
|
||||||
|
roomId: "room-1",
|
||||||
|
roomNumberSnapshot: "1.01",
|
||||||
|
roomNameSnapshot: "Besprechung",
|
||||||
|
quantity: 2,
|
||||||
|
powerPerUnit: 0.2,
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
cosPhi: 0.95,
|
||||||
|
remark: "Bestand",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,9 +14,10 @@ import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-
|
|||||||
import { CircuitSectionReorderProjectCommandRepository } from "../src/db/repositories/circuit-section-reorder-project-command.repository.js";
|
import { CircuitSectionReorderProjectCommandRepository } from "../src/db/repositories/circuit-section-reorder-project-command.repository.js";
|
||||||
import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/repositories/circuit-section-renumber-project-command.repository.js";
|
import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/repositories/circuit-section-renumber-project-command.repository.js";
|
||||||
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
|
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
||||||
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
||||||
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
||||||
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
||||||
@@ -29,6 +30,8 @@ import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
|||||||
import { projectDevices } from "../src/db/schema/project-devices.js";
|
import { projectDevices } from "../src/db/schema/project-devices.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
import { projects } from "../src/db/schema/projects.js";
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
|
import { floors } from "../src/db/schema/floors.js";
|
||||||
|
import { rooms } from "../src/db/schema/rooms.js";
|
||||||
import { ProjectHistoryOperationUnavailableError } from "../src/domain/errors/project-history-operation-unavailable.error.js";
|
import { ProjectHistoryOperationUnavailableError } from "../src/domain/errors/project-history-operation-unavailable.error.js";
|
||||||
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||||
import { createCircuitDeviceRowUpdateProjectCommand } from "../src/domain/models/circuit-device-row-project-command.model.js";
|
import { createCircuitDeviceRowUpdateProjectCommand } from "../src/domain/models/circuit-device-row-project-command.model.js";
|
||||||
@@ -46,6 +49,12 @@ import {
|
|||||||
createDistributionBoardInsertProjectCommand,
|
createDistributionBoardInsertProjectCommand,
|
||||||
createDistributionBoardStructureSnapshot,
|
createDistributionBoardStructureSnapshot,
|
||||||
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
||||||
|
import {
|
||||||
|
createProjectFloorInsertProjectCommand,
|
||||||
|
createProjectFloorSnapshot,
|
||||||
|
createProjectRoomInsertProjectCommand,
|
||||||
|
createProjectRoomSnapshot,
|
||||||
|
} from "../src/domain/models/project-location-structure-project-command.model.js";
|
||||||
import { createProjectDeviceRowSyncProjectCommand } from "../src/domain/models/project-device-row-sync-project-command.model.js";
|
import { createProjectDeviceRowSyncProjectCommand } from "../src/domain/models/project-device-row-sync-project-command.model.js";
|
||||||
import { createProjectDeviceUpdateProjectCommand } from "../src/domain/models/project-device-project-command.model.js";
|
import { createProjectDeviceUpdateProjectCommand } from "../src/domain/models/project-device-project-command.model.js";
|
||||||
import { createProjectDeviceInsertProjectCommand } from "../src/domain/models/project-device-structure-project-command.model.js";
|
import { createProjectDeviceInsertProjectCommand } from "../src/domain/models/project-device-structure-project-command.model.js";
|
||||||
@@ -61,7 +70,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
.insert(projects)
|
.insert(projects)
|
||||||
.values({ id: "project-1", name: "Test project" })
|
.values({ id: "project-1", name: "Test project" })
|
||||||
.run();
|
.run();
|
||||||
const board = new DistributionBoardRepository(
|
const board = new DistributionBoardFixtureRepository(
|
||||||
context.db
|
context.db
|
||||||
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||||
const section = context.db
|
const section = context.db
|
||||||
@@ -105,6 +114,7 @@ function createService(context: DatabaseContext) {
|
|||||||
new CircuitDeviceRowMoveProjectCommandRepository(context.db),
|
new CircuitDeviceRowMoveProjectCommandRepository(context.db),
|
||||||
new CircuitStructureProjectCommandRepository(context.db),
|
new CircuitStructureProjectCommandRepository(context.db),
|
||||||
new DistributionBoardStructureProjectCommandRepository(context.db),
|
new DistributionBoardStructureProjectCommandRepository(context.db),
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(context.db),
|
||||||
new CircuitSectionReorderProjectCommandRepository(context.db),
|
new CircuitSectionReorderProjectCommandRepository(context.db),
|
||||||
new CircuitSectionRenumberProjectCommandRepository(context.db),
|
new CircuitSectionRenumberProjectCommandRepository(context.db),
|
||||||
new ProjectDeviceStructureProjectCommandRepository(context.db),
|
new ProjectDeviceStructureProjectCommandRepository(context.db),
|
||||||
@@ -1036,6 +1046,68 @@ describe("project command service", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dispatches floor and room setup with their persisted inverses", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const service = createService(context);
|
||||||
|
const floor = createProjectFloorSnapshot(
|
||||||
|
"project-1",
|
||||||
|
"EG",
|
||||||
|
0
|
||||||
|
);
|
||||||
|
service.executeUser({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
command: createProjectFloorInsertProjectCommand(floor),
|
||||||
|
});
|
||||||
|
const room = createProjectRoomSnapshot("project-1", {
|
||||||
|
floorId: floor.id,
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Technik",
|
||||||
|
});
|
||||||
|
const created = service.executeUser({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
command: createProjectRoomInsertProjectCommand(room),
|
||||||
|
});
|
||||||
|
assert.equal(created.history.currentRevision, 2);
|
||||||
|
assert.ok(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(rooms)
|
||||||
|
.where(eq(rooms.id, room.id))
|
||||||
|
.get()
|
||||||
|
);
|
||||||
|
|
||||||
|
createService(context).undo({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 2,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(rooms)
|
||||||
|
.where(eq(rooms.id, room.id))
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
createService(context).undo({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 3,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(floors)
|
||||||
|
.where(eq(floors.id, floor.id))
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects unsupported and malformed commands before domain writes", () => {
|
it("rejects unsupported and malformed commands before domain writes", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
createDatabaseContext,
|
createDatabaseContext,
|
||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
@@ -37,7 +37,7 @@ function createTestDatabase(): TestFixture {
|
|||||||
{ id: "project-2", name: "Other project" },
|
{ id: "project-2", name: "Other project" },
|
||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
const boards = new DistributionBoardRepository(context.db);
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = boards.createWithCircuitListAndDefaultSections(
|
const board = boards.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
createDatabaseContext,
|
createDatabaseContext,
|
||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
@@ -53,7 +53,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
{ id: "project-2", name: "Other project" },
|
{ id: "project-2", name: "Other project" },
|
||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
const boards = new DistributionBoardRepository(context.db);
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
const board = boards.createWithCircuitListAndDefaultSections(
|
const board = boards.createWithCircuitListAndDefaultSections(
|
||||||
"project-1",
|
"project-1",
|
||||||
"UV-01"
|
"UV-01"
|
||||||
|
|||||||
@@ -59,12 +59,12 @@ function createService() {
|
|||||||
const service = new ProjectDeviceSyncService({
|
const service = new ProjectDeviceSyncService({
|
||||||
projectDeviceRepository: {
|
projectDeviceRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
return projectDevice() as never;
|
return projectDevice();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
deviceRowRepository: {
|
deviceRowRepository: {
|
||||||
async listLinkedByProjectDevice() {
|
async listLinkedByProjectDevice() {
|
||||||
return rows as never;
|
return rows;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { CircuitDeviceRowProjectCommandRepository } from "../src/db/repositories/circuit-device-row-project-command.repository.js";
|
import { CircuitDeviceRowProjectCommandRepository } from "../src/db/repositories/circuit-device-row-project-command.repository.js";
|
||||||
import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-project-command.repository.js";
|
import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
@@ -30,7 +30,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
.insert(projects)
|
.insert(projects)
|
||||||
.values({ id: "project-1", name: "Test project" })
|
.values({ id: "project-1", name: "Test project" })
|
||||||
.run();
|
.run();
|
||||||
const board = new DistributionBoardRepository(
|
const board = new DistributionBoardFixtureRepository(
|
||||||
context.db
|
context.db
|
||||||
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||||
const section = context.db
|
const section = context.db
|
||||||
|
|||||||
@@ -0,0 +1,464 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
|
import {
|
||||||
|
createDatabaseContext,
|
||||||
|
type DatabaseContext,
|
||||||
|
} from "../src/db/database-context.js";
|
||||||
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
|
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
||||||
|
import { consumers } from "../src/db/schema/consumers.js";
|
||||||
|
import { floors } from "../src/db/schema/floors.js";
|
||||||
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
|
import { rooms } from "../src/db/schema/rooms.js";
|
||||||
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||||
|
import {
|
||||||
|
createProjectFloorInsertProjectCommand,
|
||||||
|
createProjectFloorSnapshot,
|
||||||
|
createProjectRoomInsertProjectCommand,
|
||||||
|
createProjectRoomSnapshot,
|
||||||
|
} from "../src/domain/models/project-location-structure-project-command.model.js";
|
||||||
|
import { createFloor, createRoom } from "../src/frontend/utils/api.js";
|
||||||
|
import {
|
||||||
|
createFloorSchema,
|
||||||
|
createRoomSchema,
|
||||||
|
} from "../src/shared/validation/project-structure.schemas.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" },
|
||||||
|
{ id: "project-2", name: "Foreign project" },
|
||||||
|
])
|
||||||
|
.run();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCounts(context: DatabaseContext) {
|
||||||
|
return {
|
||||||
|
floors: context.db.select().from(floors).all().length,
|
||||||
|
rooms: context.db.select().from(rooms).all().length,
|
||||||
|
revisions: context.db.select().from(projectRevisions).all().length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("project-location structure project command", () => {
|
||||||
|
it("normalizes snapshots and sends optimistic frontend revisions", async () => {
|
||||||
|
const floor = createProjectFloorSnapshot("project-1", " EG ", 0);
|
||||||
|
const room = createProjectRoomSnapshot("project-1", {
|
||||||
|
floorId: floor.id,
|
||||||
|
roomNumber: " 001 ",
|
||||||
|
roomName: " Technik ",
|
||||||
|
});
|
||||||
|
assert.equal(floor.name, "EG");
|
||||||
|
assert.equal(room.roomNumber, "001");
|
||||||
|
assert.equal(room.roomName, "Technik");
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createProjectFloorInsertProjectCommand({
|
||||||
|
...floor,
|
||||||
|
name: " EG ",
|
||||||
|
}),
|
||||||
|
/normalized/
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
createFloorSchema.safeParse({ name: "EG" }).success,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
createRoomSchema.safeParse({
|
||||||
|
expectedRevision: 0,
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Technik",
|
||||||
|
unknown: true,
|
||||||
|
}).success,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const requests: Array<{ url: string; body: unknown }> = [];
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = async (input, init) => {
|
||||||
|
requests.push({
|
||||||
|
url: String(input),
|
||||||
|
body: JSON.parse(String(init?.body)),
|
||||||
|
});
|
||||||
|
return new Response(JSON.stringify({}), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await createFloor("project-1", { name: "EG" }, 7);
|
||||||
|
await createRoom(
|
||||||
|
"project-1",
|
||||||
|
{
|
||||||
|
floorId: "floor-1",
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Technik",
|
||||||
|
},
|
||||||
|
8
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
assert.deepEqual(requests, [
|
||||||
|
{
|
||||||
|
url: "/api/projects/project-1/floors",
|
||||||
|
body: { name: "EG", expectedRevision: 7 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/api/projects/project-1/rooms",
|
||||||
|
body: {
|
||||||
|
floorId: "floor-1",
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Technik",
|
||||||
|
expectedRevision: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a stable floor and supports persisted undo and redo", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository =
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(context.db);
|
||||||
|
const history = new ProjectHistoryRepository(context.db);
|
||||||
|
const floor = createProjectFloorSnapshot("project-1", "EG", 0);
|
||||||
|
const command = createProjectFloorInsertProjectCommand(floor);
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getCounts(context), {
|
||||||
|
floors: 1,
|
||||||
|
rooms: 0,
|
||||||
|
revisions: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const undoStep = history.getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undoStep);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undoStep.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getCounts(context), {
|
||||||
|
floors: 0,
|
||||||
|
rooms: 0,
|
||||||
|
revisions: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const redoStep = history.getNextCommand("project-1", "redo");
|
||||||
|
assert.ok(redoStep);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 2,
|
||||||
|
source: "redo",
|
||||||
|
historyTargetChangeSetId: redoStep.changeSetId,
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(floors)
|
||||||
|
.where(eq(floors.id, floor.id))
|
||||||
|
.get(),
|
||||||
|
floor
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a stable room and supports persisted undo and redo", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
context.db
|
||||||
|
.insert(floors)
|
||||||
|
.values({
|
||||||
|
id: "floor-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "EG",
|
||||||
|
sortOrder: 0,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
const repository =
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(context.db);
|
||||||
|
const history = new ProjectHistoryRepository(context.db);
|
||||||
|
const room = createProjectRoomSnapshot("project-1", {
|
||||||
|
floorId: "floor-1",
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Technik",
|
||||||
|
});
|
||||||
|
const command = createProjectRoomInsertProjectCommand(room);
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
const undoStep = history.getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undoStep);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undoStep.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(rooms)
|
||||||
|
.where(eq(rooms.id, room.id))
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const redoStep = history.getNextCommand("project-1", "redo");
|
||||||
|
assert.ok(redoStep);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 2,
|
||||||
|
source: "redo",
|
||||||
|
historyTargetChangeSetId: redoStep.changeSetId,
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(rooms)
|
||||||
|
.where(eq(rooms.id, room.id))
|
||||||
|
.get(),
|
||||||
|
room
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects foreign, stale and changed locations without partial writes", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
context.db
|
||||||
|
.insert(floors)
|
||||||
|
.values({
|
||||||
|
id: "foreign-floor",
|
||||||
|
projectId: "project-2",
|
||||||
|
name: "Fremd",
|
||||||
|
sortOrder: 0,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
const repository =
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(context.db);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createProjectRoomInsertProjectCommand(
|
||||||
|
createProjectRoomSnapshot("project-1", {
|
||||||
|
floorId: "foreign-floor",
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Fremdraum",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/foreign floor/
|
||||||
|
);
|
||||||
|
const floor = createProjectFloorSnapshot("project-1", "EG", 0);
|
||||||
|
const command = createProjectFloorInsertProjectCommand(floor);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command,
|
||||||
|
}),
|
||||||
|
ProjectRevisionConflictError
|
||||||
|
);
|
||||||
|
assert.deepEqual(getCounts(context), {
|
||||||
|
floors: 1,
|
||||||
|
rooms: 0,
|
||||||
|
revisions: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
context.db
|
||||||
|
.update(floors)
|
||||||
|
.set({ name: "Direkt geändert" })
|
||||||
|
.where(eq(floors.id, floor.id))
|
||||||
|
.run();
|
||||||
|
const undoStep = new ProjectHistoryRepository(
|
||||||
|
context.db
|
||||||
|
).getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undoStep);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undoStep.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
}),
|
||||||
|
/changed before deletion/
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
context.db.select().from(projectRevisions).all().length,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses referenced deletes and rolls back late history failures", () => {
|
||||||
|
const floorContext = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository =
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(
|
||||||
|
floorContext.db
|
||||||
|
);
|
||||||
|
const floor = createProjectFloorSnapshot("project-1", "EG", 0);
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createProjectFloorInsertProjectCommand(floor),
|
||||||
|
});
|
||||||
|
floorContext.db
|
||||||
|
.insert(rooms)
|
||||||
|
.values({
|
||||||
|
id: "room-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
floorId: floor.id,
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Technik",
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
const undoStep = new ProjectHistoryRepository(
|
||||||
|
floorContext.db
|
||||||
|
).getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undoStep);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undoStep.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
}),
|
||||||
|
/assigned rooms/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
floorContext.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomContext = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository =
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(
|
||||||
|
roomContext.db
|
||||||
|
);
|
||||||
|
const room = createProjectRoomSnapshot("project-1", {
|
||||||
|
roomNumber: "001",
|
||||||
|
roomName: "Technik",
|
||||||
|
});
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createProjectRoomInsertProjectCommand(room),
|
||||||
|
});
|
||||||
|
roomContext.db
|
||||||
|
.insert(consumers)
|
||||||
|
.values({
|
||||||
|
id: "legacy-consumer-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
roomId: room.id,
|
||||||
|
name: "Bestand",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 1,
|
||||||
|
demandFactor: 1,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
const undoStep = new ProjectHistoryRepository(
|
||||||
|
roomContext.db
|
||||||
|
).getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undoStep);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undoStep.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
}),
|
||||||
|
/referenced project room/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
roomContext.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const rollbackContext = createTestDatabase();
|
||||||
|
try {
|
||||||
|
rollbackContext.sqlite.exec(`
|
||||||
|
CREATE TRIGGER fail_project_location_history
|
||||||
|
BEFORE INSERT ON project_history_stack_entries
|
||||||
|
BEGIN
|
||||||
|
SELECT RAISE(ABORT, 'forced project-location history failure');
|
||||||
|
END;
|
||||||
|
`);
|
||||||
|
const repository =
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(
|
||||||
|
rollbackContext.db
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createProjectFloorInsertProjectCommand(
|
||||||
|
createProjectFloorSnapshot(
|
||||||
|
"project-1",
|
||||||
|
"Rollback",
|
||||||
|
0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/forced project-location history failure/
|
||||||
|
);
|
||||||
|
assert.deepEqual(getCounts(rollbackContext), {
|
||||||
|
floors: 0,
|
||||||
|
rooms: 0,
|
||||||
|
revisions: 0,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rollbackContext.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
+48
-44
@@ -5,12 +5,11 @@ import { eq } from "drizzle-orm";
|
|||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
import {
|
import {
|
||||||
createDatabaseContext,
|
createDatabaseContext,
|
||||||
|
type AppDatabase,
|
||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import {
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||||
ProjectRevisionConflictError,
|
import { appendProjectRevision } from "../src/db/repositories/project-revision.persistence.js";
|
||||||
ProjectRevisionRepository,
|
|
||||||
} from "../src/db/repositories/project-revision.repository.js";
|
|
||||||
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
||||||
import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
@@ -27,40 +26,50 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendTestRevision(
|
function appendTestRevision(
|
||||||
repository: ProjectRevisionRepository,
|
database: AppDatabase,
|
||||||
expectedRevision: number
|
expectedRevision: number
|
||||||
) {
|
) {
|
||||||
return repository.append({
|
return database.transaction((tx) =>
|
||||||
projectId: "project-1",
|
appendProjectRevision(tx, {
|
||||||
expectedRevision,
|
projectId: "project-1",
|
||||||
source: "user",
|
expectedRevision,
|
||||||
description: "Stromkreis bearbeiten",
|
source: "user",
|
||||||
actorId: "test-user",
|
description: "Stromkreis bearbeiten",
|
||||||
forward: {
|
actorId: "test-user",
|
||||||
schemaVersion: 1,
|
forward: {
|
||||||
type: "circuit.update",
|
schemaVersion: 1,
|
||||||
payload: { circuitId: "circuit-1", displayName: "Neu" },
|
type: "circuit.update",
|
||||||
},
|
payload: { circuitId: "circuit-1", displayName: "Neu" },
|
||||||
inverse: {
|
},
|
||||||
schemaVersion: 1,
|
inverse: {
|
||||||
type: "circuit.update",
|
schemaVersion: 1,
|
||||||
payload: { circuitId: "circuit-1", displayName: "Alt" },
|
type: "circuit.update",
|
||||||
},
|
payload: { circuitId: "circuit-1", displayName: "Alt" },
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("project revision repository", () => {
|
function getCurrentRevision(database: AppDatabase, projectId: string) {
|
||||||
|
return (
|
||||||
|
database
|
||||||
|
.select({ currentRevision: projects.currentRevision })
|
||||||
|
.from(projects)
|
||||||
|
.where(eq(projects.id, projectId))
|
||||||
|
.get()?.currentRevision ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("project revision persistence", () => {
|
||||||
it("appends immutable revision metadata and payloads with a monotonic number", () => {
|
it("appends immutable revision metadata and payloads with a monotonic number", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
const repository = new ProjectRevisionRepository(context.db);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 0);
|
||||||
|
const appended = appendTestRevision(context.db, 0);
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 0);
|
|
||||||
const appended = appendTestRevision(repository, 0);
|
|
||||||
|
|
||||||
assert.equal(appended.projectId, "project-1");
|
assert.equal(appended.projectId, "project-1");
|
||||||
assert.equal(appended.revisionNumber, 1);
|
assert.equal(appended.revisionNumber, 1);
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 1);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 1);
|
||||||
|
|
||||||
const revision = context.db
|
const revision = context.db
|
||||||
.select()
|
.select()
|
||||||
@@ -105,9 +114,9 @@ describe("project revision repository", () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const next = appendTestRevision(repository, 1);
|
const next = appendTestRevision(context.db, 1);
|
||||||
assert.equal(next.revisionNumber, 2);
|
assert.equal(next.revisionNumber, 2);
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 2);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 2);
|
||||||
} finally {
|
} finally {
|
||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
@@ -116,18 +125,17 @@ describe("project revision repository", () => {
|
|||||||
it("rejects a stale expected revision without writing history", () => {
|
it("rejects a stale expected revision without writing history", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
const repository = new ProjectRevisionRepository(context.db);
|
appendTestRevision(context.db, 0);
|
||||||
appendTestRevision(repository, 0);
|
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => appendTestRevision(repository, 0),
|
() => appendTestRevision(context.db, 0),
|
||||||
(error) =>
|
(error) =>
|
||||||
error instanceof ProjectRevisionConflictError &&
|
error instanceof ProjectRevisionConflictError &&
|
||||||
error.expectedRevision === 0 &&
|
error.expectedRevision === 0 &&
|
||||||
error.actualRevision === 1
|
error.actualRevision === 1
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 1);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 1);
|
||||||
assert.equal(context.db.select().from(projectRevisions).all().length, 1);
|
assert.equal(context.db.select().from(projectRevisions).all().length, 1);
|
||||||
assert.equal(context.db.select().from(projectChangeSets).all().length, 1);
|
assert.equal(context.db.select().from(projectChangeSets).all().length, 1);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -145,14 +153,12 @@ describe("project revision repository", () => {
|
|||||||
SELECT RAISE(ABORT, 'forced change-set failure');
|
SELECT RAISE(ABORT, 'forced change-set failure');
|
||||||
END;
|
END;
|
||||||
`);
|
`);
|
||||||
const repository = new ProjectRevisionRepository(context.db);
|
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => appendTestRevision(repository, 0),
|
() => appendTestRevision(context.db, 0),
|
||||||
/forced change-set failure/
|
/forced change-set failure/
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 0);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 0);
|
||||||
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
|
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
|
||||||
assert.equal(context.db.select().from(projectChangeSets).all().length, 0);
|
assert.equal(context.db.select().from(projectChangeSets).all().length, 0);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -163,7 +169,6 @@ describe("project revision repository", () => {
|
|||||||
it("captures and retains automatic snapshots without deleting named snapshots", () => {
|
it("captures and retains automatic snapshots without deleting named snapshots", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
const revisions = new ProjectRevisionRepository(context.db);
|
|
||||||
const snapshots = new ProjectSnapshotRepository(context.db);
|
const snapshots = new ProjectSnapshotRepository(context.db);
|
||||||
const named = snapshots.createNamed({
|
const named = snapshots.createNamed({
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
@@ -177,7 +182,7 @@ describe("project revision repository", () => {
|
|||||||
expectedRevision < 325;
|
expectedRevision < 325;
|
||||||
expectedRevision += 1
|
expectedRevision += 1
|
||||||
) {
|
) {
|
||||||
appendTestRevision(revisions, expectedRevision);
|
appendTestRevision(context.db, expectedRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stored = context.db
|
const stored = context.db
|
||||||
@@ -234,13 +239,12 @@ describe("project revision repository", () => {
|
|||||||
it("rolls back revision 25 when automatic snapshot persistence fails", () => {
|
it("rolls back revision 25 when automatic snapshot persistence fails", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
const repository = new ProjectRevisionRepository(context.db);
|
|
||||||
for (
|
for (
|
||||||
let expectedRevision = 0;
|
let expectedRevision = 0;
|
||||||
expectedRevision < 24;
|
expectedRevision < 24;
|
||||||
expectedRevision += 1
|
expectedRevision += 1
|
||||||
) {
|
) {
|
||||||
appendTestRevision(repository, expectedRevision);
|
appendTestRevision(context.db, expectedRevision);
|
||||||
}
|
}
|
||||||
context.sqlite.exec(`
|
context.sqlite.exec(`
|
||||||
CREATE TRIGGER fail_automatic_project_snapshot
|
CREATE TRIGGER fail_automatic_project_snapshot
|
||||||
@@ -252,10 +256,10 @@ describe("project revision repository", () => {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => appendTestRevision(repository, 24),
|
() => appendTestRevision(context.db, 24),
|
||||||
/forced automatic snapshot failure/
|
/forced automatic snapshot failure/
|
||||||
);
|
);
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 24);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 24);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
context.db.select().from(projectRevisions).all().length,
|
context.db.select().from(projectRevisions).all().length,
|
||||||
24
|
24
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
createDatabaseContext,
|
createDatabaseContext,
|
||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
||||||
@@ -37,7 +37,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
const board = new DistributionBoardRepository(
|
const board = new DistributionBoardFixtureRepository(
|
||||||
context.db
|
context.db
|
||||||
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||||
const list = context.db
|
const list = context.db
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-
|
|||||||
import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/repositories/circuit-section-renumber-project-command.repository.js";
|
import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/repositories/circuit-section-renumber-project-command.repository.js";
|
||||||
import { CircuitSectionReorderProjectCommandRepository } from "../src/db/repositories/circuit-section-reorder-project-command.repository.js";
|
import { CircuitSectionReorderProjectCommandRepository } from "../src/db/repositories/circuit-section-reorder-project-command.repository.js";
|
||||||
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
||||||
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/project-device-project-command.repository.js";
|
||||||
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
import { ProjectDeviceRowSyncProjectCommandRepository } from "../src/db/repositories/project-device-row-sync-project-command.repository.js";
|
||||||
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
import { ProjectDeviceStructureProjectCommandRepository } from "../src/db/repositories/project-device-structure-project-command.repository.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
|
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
||||||
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
||||||
import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js";
|
import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js";
|
||||||
import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/project-settings-project-command.repository.js";
|
import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/project-settings-project-command.repository.js";
|
||||||
@@ -49,7 +50,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
.insert(projects)
|
.insert(projects)
|
||||||
.values({ id: "project-1", name: "Ausgangsprojekt" })
|
.values({ id: "project-1", name: "Ausgangsprojekt" })
|
||||||
.run();
|
.run();
|
||||||
const board = new DistributionBoardRepository(
|
const board = new DistributionBoardFixtureRepository(
|
||||||
context.db
|
context.db
|
||||||
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||||
const list = context.db
|
const list = context.db
|
||||||
@@ -134,6 +135,7 @@ function createService(context: DatabaseContext) {
|
|||||||
new CircuitDeviceRowMoveProjectCommandRepository(context.db),
|
new CircuitDeviceRowMoveProjectCommandRepository(context.db),
|
||||||
new CircuitStructureProjectCommandRepository(context.db),
|
new CircuitStructureProjectCommandRepository(context.db),
|
||||||
new DistributionBoardStructureProjectCommandRepository(context.db),
|
new DistributionBoardStructureProjectCommandRepository(context.db),
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(context.db),
|
||||||
new CircuitSectionReorderProjectCommandRepository(context.db),
|
new CircuitSectionReorderProjectCommandRepository(context.db),
|
||||||
new CircuitSectionRenumberProjectCommandRepository(context.db),
|
new CircuitSectionRenumberProjectCommandRepository(context.db),
|
||||||
new ProjectDeviceStructureProjectCommandRepository(context.db),
|
new ProjectDeviceStructureProjectCommandRepository(context.db),
|
||||||
@@ -192,7 +194,7 @@ function changeCompleteProjectState(context: DatabaseContext) {
|
|||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
new DistributionBoardRepository(
|
new DistributionBoardFixtureRepository(
|
||||||
context.db
|
context.db
|
||||||
).createWithCircuitListAndDefaultSections("project-1", "UV-02");
|
).createWithCircuitListAndDefaultSections("project-1", "UV-02");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { AppDatabase } from "../../src/db/database-context.js";
|
||||||
|
import { circuitLists } from "../../src/db/schema/circuit-lists.js";
|
||||||
|
import { circuitSections } from "../../src/db/schema/circuit-sections.js";
|
||||||
|
import { distributionBoards } from "../../src/db/schema/distribution-boards.js";
|
||||||
|
import { createDistributionBoardStructureSnapshot } from "../../src/domain/models/distribution-board-structure-project-command.model.js";
|
||||||
|
|
||||||
|
export class DistributionBoardFixtureRepository {
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
|
createWithCircuitListAndDefaultSections(
|
||||||
|
projectId: string,
|
||||||
|
name: string
|
||||||
|
) {
|
||||||
|
const structure = createDistributionBoardStructureSnapshot(
|
||||||
|
projectId,
|
||||||
|
name
|
||||||
|
);
|
||||||
|
this.database.transaction((tx) => {
|
||||||
|
tx.insert(distributionBoards)
|
||||||
|
.values(structure.distributionBoard)
|
||||||
|
.run();
|
||||||
|
tx.insert(circuitLists).values(structure.circuitList).run();
|
||||||
|
tx.insert(circuitSections).values(structure.sections).run();
|
||||||
|
});
|
||||||
|
return structure.distributionBoard;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["scripts/**/*.ts", "src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user