Persist project locations
This commit is contained in:
@@ -272,6 +272,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:
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ Tabellen. Ein getestetes Repository kann diese Historienmetadaten optimistisch
|
|||||||
und atomar fortschreiben. Vorwärts- und Rückwärtskommandos besitzen einen
|
und atomar fortschreiben. 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.
|
||||||
@@ -198,6 +199,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -451,6 +451,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
|
||||||
|
|||||||
+2
-2
@@ -14,8 +14,8 @@
|
|||||||
"build:api": "tsc -p tsconfig.json",
|
"build:api": "tsc -p tsconfig.json",
|
||||||
"build:web": "next build",
|
"build:web": "next build",
|
||||||
"start": "node dist/server/index.js",
|
"start": "node dist/server/index.js",
|
||||||
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-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.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-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.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.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.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-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.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",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import crypto from "node:crypto";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
|
||||||
import { db } from "../client.js";
|
import { db } from "../client.js";
|
||||||
import { floors } from "../schema/floors.js";
|
import { floors } from "../schema/floors.js";
|
||||||
|
|
||||||
@@ -11,26 +10,4 @@ export class FloorRepository {
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
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 { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
||||||
|
import { appendProjectRevision } from "./project-revision.persistence.js";
|
||||||
|
|
||||||
|
export class ProjectLocationStructureProjectCommandRepository
|
||||||
|
implements ProjectLocationStructureProjectCommandStore
|
||||||
|
{
|
||||||
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
|
execute(input: ExecuteProjectLocationStructureCommandInput) {
|
||||||
|
return this.database.transaction((tx) => {
|
||||||
|
const inverse = 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(
|
||||||
|
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,8 +1,6 @@
|
|||||||
import crypto from "node:crypto";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
|
||||||
import { db } from "../client.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 {
|
||||||
async listByProject(projectId: string) {
|
async listByProject(projectId: string) {
|
||||||
@@ -12,31 +10,4 @@ export class RoomRepository {
|
|||||||
.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,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;
|
||||||
|
}
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +1,12 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
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";
|
||||||
|
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||||
|
|
||||||
const floorRepository = new FloorRepository();
|
const floorRepository = new FloorRepository();
|
||||||
|
|
||||||
@@ -25,6 +31,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,9 +1,13 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
|
||||||
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
||||||
|
import {
|
||||||
|
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";
|
||||||
|
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||||
|
|
||||||
const floorRepository = new FloorRepository();
|
|
||||||
const roomRepository = new RoomRepository();
|
const roomRepository = new RoomRepository();
|
||||||
|
|
||||||
export async function listRoomsByProject(req: Request, res: Response) {
|
export async function listRoomsByProject(req: Request, res: Response) {
|
||||||
@@ -27,13 +31,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>;
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { CircuitStructureProjectCommandRepository } from "../src/db/repositories
|
|||||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.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";
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -20,6 +20,7 @@ import { ProjectDeviceProjectCommandRepository } from "../src/db/repositories/pr
|
|||||||
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";
|
||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user