Persist distribution board setup

This commit is contained in:
2026-07-25 23:41:27 +02:00
parent 1a77eedaa8
commit 1cf26a932e
21 changed files with 1027 additions and 22 deletions
+4
View File
@@ -268,6 +268,10 @@ paths do the same; their undo action uses the project-wide history endpoint.
Project voltage settings use the persistent `project.update-settings` command. Project voltage settings use the persistent `project.update-settings` command.
Both voltage defaults change in one revision and Undo/Redo restores them Both voltage defaults change in one revision and Undo/Redo restores them
together; the project PUT route requires `expectedRevision`. together; the project PUT route requires `expectedRevision`.
Distribution-board setup uses `distribution-board.insert` with a complete
stable snapshot of the board, circuit list and four default sections. Its
inverse removes only the same unchanged and still-empty structure; the POST
route requires `expectedRevision` and returns the updated history state.
Required operations: Required operations:
+8 -1
View File
@@ -181,7 +181,14 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
### Distribution Board Setup ### Distribution Board Setup
- `POST /projects/:projectId/distribution-boards` - `POST /projects/:projectId/distribution-boards`
- atomically creates the distribution board, its circuit list and all default circuit sections - body: `{ "name": "UV-02", "expectedRevision": 12 }`
- executes `distribution-board.insert` with stable ids for the distribution
board, its circuit list and all four default circuit sections
- response:
`{ "distributionBoard": { ... }, "revision": { ... }, "history": { ... } }`
- persistent Undo removes only the unchanged and still-empty generated
structure; Redo restores the same ids
- stale revisions return `409 PROJECT_REVISION_CONFLICT`
### Tree Endpoint ### Tree Endpoint
+7
View File
@@ -191,6 +191,13 @@ gespeicherten Projekt ab und schreibt Werte, Revision und Historienstapel
gemeinsam. `PUT /api/projects/:projectId` verlangt deshalb gemeinsam. `PUT /api/projects/:projectId` verlangt deshalb
`expectedRevision`, liefert Projekt plus aktualisierten Historienstand und `expectedRevision`, liefert Projekt plus aktualisierten Historienstand und
besitzt keinen separaten direkten Settings-Schreibweg mehr. besitzt keinen separaten direkten Settings-Schreibweg mehr.
`distribution-board.insert` versioniert die Anlage einer Verteilung als einen
vollständigen Block aus Verteilung, Stromkreisliste und vier Standardbereichen.
Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil.
`distribution-board.delete` ist die persistierte Inverse und entfernt nur den
vollständig unveränderten, weiterhin stromkreislosen Block. Controller und
Projektseite übergeben die erwartete Projektrevision; der frühere direkte
Controller-Schreibweg ist entfernt.
## Projektgeräte ## Projektgeräte
@@ -266,6 +266,10 @@ Completed foundation:
- `project.update-settings` persists both project voltage defaults, its exact - `project.update-settings` persists both project voltage defaults, its exact
inverse, revision and history transition atomically; the project page and inverse, revision and history transition atomically; the project page and
existing PUT route require the current expected revision existing PUT route require the current expected revision
- `distribution-board.insert` persists the complete generated board, circuit
list and default-section structure with stable ids; its delete inverse
refuses changed or populated structures and the project-page POST tracks the
returned revision
Remaining constraints before completing project version history: Remaining constraints before completing project version history:
@@ -448,6 +448,9 @@ Implemented foundation:
- project voltage settings use `project.update-settings`; both values, inverse, - project voltage settings use `project.update-settings`; both values, inverse,
revision and history transition commit atomically and the former direct revision and history transition commit atomically and the former direct
settings write is removed settings write is removed
- distribution-board setup uses `distribution-board.insert` with a complete
stable board/list/default-section snapshot; Undo removes only an unchanged
empty setup and the former direct controller write is 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
View File
@@ -14,8 +14,8 @@
"build:api": "tsc -p tsconfig.json", "build:api": "tsc -p tsconfig.json",
"build:web": "next build", "build:web": "next build",
"start": "node dist/server/index.js", "start": "node dist/server/index.js",
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-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/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-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/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",
"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",
+11 -3
View File
@@ -177,15 +177,23 @@ export default function ProjectDetailPage() {
async function handleCreateBoard(event: FormEvent<HTMLFormElement>) { async function handleCreateBoard(event: FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
if (!projectId || !boardName.trim()) { if (!projectId || !project || !boardName.trim()) {
return; return;
} }
setIsSaving(true); setIsSaving(true);
setError(null); setError(null);
try { try {
const created = await createDistributionBoard(projectId, boardName.trim()); const result = await createDistributionBoard(
setBoards((current) => [...current, created]); projectId,
boardName.trim(),
project.currentRevision
);
setBoards((current) => [
...current,
result.distributionBoard,
]);
setCircuitLists(await listCircuitLists(projectId)); setCircuitLists(await listCircuitLists(projectId));
applyProjectRevision(result.history.currentRevision);
setBoardName(""); setBoardName("");
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden."); setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden.");
@@ -1,15 +1,11 @@
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 { db } from "../client.js"; import { db } from "../client.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) {
return [ return defaultCircuitSectionDefinitions.map((entry) => ({
{ key: "lighting", displayName: "Lighting", prefix: "-1F", sortOrder: 10 },
{ key: "single_phase", displayName: "Single-phase circuits", prefix: "-2F", sortOrder: 20 },
{ key: "three_phase", displayName: "Three-phase circuits", prefix: "-3F", sortOrder: 30 },
{ key: "unassigned", displayName: "Unassigned", prefix: "-UF", sortOrder: 90 },
].map((entry) => ({
id: crypto.randomUUID(), id: crypto.randomUUID(),
circuitListId, circuitListId,
...entry, ...entry,
@@ -0,0 +1,238 @@
import { and, asc, eq, inArray } from "drizzle-orm";
import {
assertDistributionBoardDeleteProjectCommand,
assertDistributionBoardInsertProjectCommand,
createDistributionBoardDeleteProjectCommand,
createDistributionBoardInsertProjectCommand,
distributionBoardDeleteCommandType,
distributionBoardInsertCommandType,
type DistributionBoardStructureProjectCommand,
type DistributionBoardStructureSnapshot,
} from "../../domain/models/distribution-board-structure-project-command.model.js";
import type {
DistributionBoardStructureProjectCommandStore,
ExecuteDistributionBoardStructureCommandInput,
} from "../../domain/ports/distribution-board-structure-project-command.store.js";
import type { AppDatabase } from "../database-context.js";
import { circuitLists } from "../schema/circuit-lists.js";
import { circuitSections } from "../schema/circuit-sections.js";
import { circuits } from "../schema/circuits.js";
import { distributionBoards } from "../schema/distribution-boards.js";
import { projects } from "../schema/projects.js";
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
import { appendProjectRevision } from "./project-revision.persistence.js";
export class DistributionBoardStructureProjectCommandRepository
implements DistributionBoardStructureProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteDistributionBoardStructureCommandInput) {
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: DistributionBoardStructureProjectCommand
): DistributionBoardStructureProjectCommand {
if (command.type === distributionBoardInsertCommandType) {
assertDistributionBoardInsertProjectCommand(command);
return this.insert(
database,
projectId,
command.payload.structure
);
}
if (command.type === distributionBoardDeleteCommandType) {
assertDistributionBoardDeleteProjectCommand(command);
return this.delete(
database,
projectId,
command.payload.structure
);
}
throw new Error("Unsupported distribution-board structure command.");
}
private insert(
database: AppDatabase,
projectId: string,
structure: DistributionBoardStructureSnapshot
) {
if (
structure.distributionBoard.projectId !== projectId ||
structure.circuitList.projectId !== projectId
) {
throw new Error(
"Distribution-board structure does not belong to project."
);
}
const project = database
.select({ id: projects.id })
.from(projects)
.where(eq(projects.id, projectId))
.get();
if (!project) {
throw new Error("Project not found.");
}
const existingBoard = database
.select({ id: distributionBoards.id })
.from(distributionBoards)
.where(eq(distributionBoards.id, structure.distributionBoard.id))
.get();
const existingList = database
.select({ id: circuitLists.id })
.from(circuitLists)
.where(eq(circuitLists.id, structure.circuitList.id))
.get();
const existingSection = database
.select({ id: circuitSections.id })
.from(circuitSections)
.where(
inArray(
circuitSections.id,
structure.sections.map((section) => section.id)
)
)
.limit(1)
.get();
if (existingBoard || existingList || existingSection) {
throw new Error("Distribution-board structure id already exists.");
}
database
.insert(distributionBoards)
.values(structure.distributionBoard)
.run();
database.insert(circuitLists).values(structure.circuitList).run();
database.insert(circuitSections).values(structure.sections).run();
return createDistributionBoardDeleteProjectCommand(structure);
}
private delete(
database: AppDatabase,
projectId: string,
structure: DistributionBoardStructureSnapshot
) {
const board = database
.select()
.from(distributionBoards)
.where(eq(distributionBoards.id, structure.distributionBoard.id))
.get();
const list = database
.select()
.from(circuitLists)
.where(eq(circuitLists.id, structure.circuitList.id))
.get();
const sections = database
.select()
.from(circuitSections)
.where(
eq(circuitSections.circuitListId, structure.circuitList.id)
)
.orderBy(
asc(circuitSections.sortOrder),
asc(circuitSections.id)
)
.all();
if (
!board ||
!list ||
board.projectId !== projectId ||
!structureMatches(structure, { board, list, sections })
) {
throw new Error(
"Distribution-board structure changed before deletion."
);
}
const populatedCircuit = database
.select({ id: circuits.id })
.from(circuits)
.where(eq(circuits.circuitListId, structure.circuitList.id))
.limit(1)
.get();
if (populatedCircuit) {
throw new Error(
"A populated distribution board cannot be removed by history."
);
}
const deleted = database
.delete(distributionBoards)
.where(
and(
eq(
distributionBoards.id,
structure.distributionBoard.id
),
eq(distributionBoards.projectId, projectId)
)
)
.run();
if (deleted.changes !== 1) {
throw new Error("Distribution board could not be deleted.");
}
return createDistributionBoardInsertProjectCommand(structure);
}
}
function structureMatches(
expected: DistributionBoardStructureSnapshot,
actual: {
board: typeof distributionBoards.$inferSelect;
list: typeof circuitLists.$inferSelect;
sections: Array<typeof circuitSections.$inferSelect>;
}
) {
if (
!sameRecord(expected.distributionBoard, actual.board) ||
!sameRecord(expected.circuitList, actual.list) ||
expected.sections.length !== actual.sections.length
) {
return false;
}
const expectedSections = [...expected.sections].sort(
(left, right) =>
left.sortOrder - right.sortOrder ||
left.id.localeCompare(right.id)
);
return expectedSections.every((section, index) =>
sameRecord(section, actual.sections[index])
);
}
function sameRecord(
expected: Record<string, unknown>,
actual: Record<string, unknown>
) {
const expectedEntries = Object.entries(expected);
return (
expectedEntries.length === Object.keys(actual).length &&
expectedEntries.every(
([key, value]) => actual[key] === value
)
);
}
@@ -0,0 +1,243 @@
import crypto from "node:crypto";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const distributionBoardInsertCommandType =
"distribution-board.insert" as const;
export const distributionBoardDeleteCommandType =
"distribution-board.delete" as const;
export const distributionBoardStructureCommandSchemaVersion = 1 as const;
export const defaultCircuitSectionDefinitions = [
{
key: "lighting",
displayName: "Lighting",
prefix: "-1F",
sortOrder: 10,
},
{
key: "single_phase",
displayName: "Single-phase circuits",
prefix: "-2F",
sortOrder: 20,
},
{
key: "three_phase",
displayName: "Three-phase circuits",
prefix: "-3F",
sortOrder: 30,
},
{
key: "unassigned",
displayName: "Unassigned",
prefix: "-UF",
sortOrder: 90,
},
] as const;
export interface DistributionBoardStructureSnapshot {
distributionBoard: {
id: string;
projectId: string;
name: string;
};
circuitList: {
id: string;
projectId: string;
distributionBoardId: string;
name: string;
};
sections: Array<{
id: string;
circuitListId: string;
key: string;
displayName: string;
prefix: string;
sortOrder: number;
}>;
}
interface DistributionBoardStructureCommandPayload {
structure: DistributionBoardStructureSnapshot;
}
export interface DistributionBoardInsertProjectCommand
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
type: typeof distributionBoardInsertCommandType;
}
export interface DistributionBoardDeleteProjectCommand
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
type: typeof distributionBoardDeleteCommandType;
}
export type DistributionBoardStructureProjectCommand =
| DistributionBoardInsertProjectCommand
| DistributionBoardDeleteProjectCommand;
export function createDistributionBoardStructureSnapshot(
projectId: string,
name: string
): DistributionBoardStructureSnapshot {
const normalizedName = name.trim();
assertNonEmptyString(projectId, "projectId");
assertNonEmptyString(normalizedName, "name");
const structureId = crypto.randomUUID();
const structure: DistributionBoardStructureSnapshot = {
distributionBoard: {
id: structureId,
projectId,
name: normalizedName,
},
circuitList: {
id: structureId,
projectId,
distributionBoardId: structureId,
name: `${normalizedName} Stromkreisliste`,
},
sections: defaultCircuitSectionDefinitions.map((section) => ({
id: crypto.randomUUID(),
circuitListId: structureId,
...section,
})),
};
assertDistributionBoardStructureSnapshot(structure);
return structure;
}
export function createDistributionBoardInsertProjectCommand(
structure: DistributionBoardStructureSnapshot
): DistributionBoardInsertProjectCommand {
const command: DistributionBoardInsertProjectCommand = {
schemaVersion: distributionBoardStructureCommandSchemaVersion,
type: distributionBoardInsertCommandType,
payload: { structure },
};
assertDistributionBoardInsertProjectCommand(command);
return command;
}
export function createDistributionBoardDeleteProjectCommand(
structure: DistributionBoardStructureSnapshot
): DistributionBoardDeleteProjectCommand {
const command: DistributionBoardDeleteProjectCommand = {
schemaVersion: distributionBoardStructureCommandSchemaVersion,
type: distributionBoardDeleteCommandType,
payload: { structure },
};
assertDistributionBoardDeleteProjectCommand(command);
return command;
}
export function assertDistributionBoardInsertProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is DistributionBoardInsertProjectCommand {
assertDistributionBoardStructureProjectCommand(
command,
distributionBoardInsertCommandType
);
}
export function assertDistributionBoardDeleteProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is DistributionBoardDeleteProjectCommand {
assertDistributionBoardStructureProjectCommand(
command,
distributionBoardDeleteCommandType
);
}
export function assertDistributionBoardStructureSnapshot(
structure: unknown
): asserts structure is DistributionBoardStructureSnapshot {
if (!isPlainObject(structure) || Object.keys(structure).length !== 3) {
throw new Error("Distribution-board structure is invalid.");
}
const { distributionBoard, circuitList, sections } = structure;
if (
!isPlainObject(distributionBoard) ||
Object.keys(distributionBoard).length !== 3 ||
!isPlainObject(circuitList) ||
Object.keys(circuitList).length !== 4 ||
!Array.isArray(sections) ||
sections.length !== defaultCircuitSectionDefinitions.length
) {
throw new Error("Distribution-board structure is incomplete.");
}
for (const [field, value] of Object.entries(distributionBoard)) {
assertNonEmptyString(value, `distributionBoard.${field}`);
}
for (const [field, value] of Object.entries(circuitList)) {
assertNonEmptyString(value, `circuitList.${field}`);
}
if (
circuitList.projectId !== distributionBoard.projectId ||
circuitList.distributionBoardId !== distributionBoard.id ||
circuitList.name !==
`${distributionBoard.name} Stromkreisliste`
) {
throw new Error(
"Distribution board and circuit list are inconsistent."
);
}
const sectionIds = new Set<string>();
for (let index = 0; index < sections.length; index += 1) {
const section = sections[index];
const expected = defaultCircuitSectionDefinitions[index];
if (
!isPlainObject(section) ||
Object.keys(section).length !== 6
) {
throw new Error("Default circuit section is invalid.");
}
assertNonEmptyString(section.id, "section.id");
if (sectionIds.has(section.id)) {
throw new Error("Default circuit-section ids must be unique.");
}
sectionIds.add(section.id);
if (
section.circuitListId !== circuitList.id ||
section.key !== expected.key ||
section.displayName !== expected.displayName ||
section.prefix !== expected.prefix ||
section.sortOrder !== expected.sortOrder
) {
throw new Error(
"Distribution-board structure has invalid default sections."
);
}
}
}
function assertDistributionBoardStructureProjectCommand(
command: SerializedProjectCommand<unknown>,
expectedType:
| typeof distributionBoardInsertCommandType
| typeof distributionBoardDeleteCommandType
) {
if (
command.schemaVersion !==
distributionBoardStructureCommandSchemaVersion ||
command.type !== expectedType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 1
) {
throw new Error("Unsupported distribution-board structure command.");
}
assertDistributionBoardStructureSnapshot(command.payload.structure);
}
function assertNonEmptyString(
value: unknown,
field: string
): asserts value is string {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${field} must be a 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 { DistributionBoardStructureProjectCommand } from "../models/distribution-board-structure-project-command.model.js";
import type {
AppendedProjectRevision,
ProjectRevisionSource,
} from "./project-revision.store.js";
export interface ExecuteDistributionBoardStructureCommandInput {
projectId: string;
expectedRevision: number;
source: ProjectRevisionSource;
description?: string;
actorId?: string;
historyTargetChangeSetId?: string;
command: DistributionBoardStructureProjectCommand;
}
export interface ExecutedDistributionBoardStructureCommand {
revision: AppendedProjectRevision;
inverse: DistributionBoardStructureProjectCommand;
}
export interface DistributionBoardStructureProjectCommandStore {
execute(
input: ExecuteDistributionBoardStructureCommandInput
): ExecutedDistributionBoardStructureCommand;
}
@@ -37,6 +37,12 @@ import {
circuitDeleteCommandType, circuitDeleteCommandType,
circuitInsertCommandType, circuitInsertCommandType,
} from "../models/circuit-structure-project-command.model.js"; } from "../models/circuit-structure-project-command.model.js";
import {
assertDistributionBoardDeleteProjectCommand,
assertDistributionBoardInsertProjectCommand,
distributionBoardDeleteCommandType,
distributionBoardInsertCommandType,
} from "../models/distribution-board-structure-project-command.model.js";
import { import {
assertSerializedProjectCommand, assertSerializedProjectCommand,
type SerializedProjectCommand, type SerializedProjectCommand,
@@ -70,6 +76,7 @@ import type { CircuitProjectCommandStore } from "../ports/circuit-project-comman
import type { CircuitSectionReorderProjectCommandStore } from "../ports/circuit-section-reorder-project-command.store.js"; import type { CircuitSectionReorderProjectCommandStore } from "../ports/circuit-section-reorder-project-command.store.js";
import type { CircuitSectionRenumberProjectCommandStore } from "../ports/circuit-section-renumber-project-command.store.js"; import type { CircuitSectionRenumberProjectCommandStore } from "../ports/circuit-section-renumber-project-command.store.js";
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js"; import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
import type { DistributionBoardStructureProjectCommandStore } from "../ports/distribution-board-structure-project-command.store.js";
import type { import type {
ExecuteProjectHistoryCommandInput, ExecuteProjectHistoryCommandInput,
ExecutedProjectCommand, ExecutedProjectCommand,
@@ -104,6 +111,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
private readonly deviceRowStructureStore: CircuitDeviceRowStructureProjectCommandStore, private readonly deviceRowStructureStore: CircuitDeviceRowStructureProjectCommandStore,
private readonly deviceRowMoveStore: CircuitDeviceRowMoveProjectCommandStore, private readonly deviceRowMoveStore: CircuitDeviceRowMoveProjectCommandStore,
private readonly circuitStructureStore: CircuitStructureProjectCommandStore, private readonly circuitStructureStore: CircuitStructureProjectCommandStore,
private readonly distributionBoardStructureStore: DistributionBoardStructureProjectCommandStore,
private readonly circuitSectionReorderStore: CircuitSectionReorderProjectCommandStore, private readonly circuitSectionReorderStore: CircuitSectionReorderProjectCommandStore,
private readonly circuitSectionRenumberStore: CircuitSectionRenumberProjectCommandStore, private readonly circuitSectionRenumberStore: CircuitSectionRenumberProjectCommandStore,
private readonly projectDeviceStructureStore: ProjectDeviceStructureProjectCommandStore, private readonly projectDeviceStructureStore: ProjectDeviceStructureProjectCommandStore,
@@ -251,6 +259,20 @@ export class ProjectCommandService implements ProjectCommandExecutor {
command: input.command, command: input.command,
}).revision; }).revision;
} }
case distributionBoardInsertCommandType: {
assertDistributionBoardInsertProjectCommand(input.command);
return this.distributionBoardStructureStore.execute({
...input,
command: input.command,
}).revision;
}
case distributionBoardDeleteCommandType: {
assertDistributionBoardDeleteProjectCommand(input.command);
return this.distributionBoardStructureStore.execute({
...input,
command: input.command,
}).revision;
}
case circuitSectionReorderCommandType: { case circuitSectionReorderCommandType: {
assertCircuitSectionReorderProjectCommand(input.command); assertCircuitSectionReorderProjectCommand(input.command);
return this.circuitSectionReorderStore.execute({ return this.circuitSectionReorderStore.execute({
+5
View File
@@ -87,6 +87,11 @@ export interface DistributionBoardDto {
name: string; name: string;
} }
export interface DistributionBoardCommandResultDto
extends ProjectCommandResultDto {
distributionBoard: DistributionBoardDto;
}
export interface CircuitListDto { export interface CircuitListDto {
id: string; id: string;
projectId: string; projectId: string;
+12 -4
View File
@@ -5,6 +5,7 @@ import type {
CreateRoomInput, CreateRoomInput,
CreateGlobalDeviceInput, CreateGlobalDeviceInput,
DistributionBoardDto, DistributionBoardDto,
DistributionBoardCommandResultDto,
FloorDto, FloorDto,
GlobalDeviceDto, GlobalDeviceDto,
ProjectCommandDto, ProjectCommandDto,
@@ -222,11 +223,18 @@ export function listDistributionBoards(projectId: string) {
return request<DistributionBoardDto[]>(`/api/projects/${projectId}/distribution-boards`); return request<DistributionBoardDto[]>(`/api/projects/${projectId}/distribution-boards`);
} }
export function createDistributionBoard(projectId: string, name: string) { export function createDistributionBoard(
return request<DistributionBoardDto>(`/api/projects/${projectId}/distribution-boards`, { projectId: string,
name: string,
expectedRevision: number
) {
return request<DistributionBoardCommandResultDto>(
`/api/projects/${projectId}/distribution-boards`,
{
method: "POST", method: "POST",
body: JSON.stringify({ name }), body: JSON.stringify({ name, expectedRevision }),
}); }
);
} }
export function listCircuitLists(projectId: string) { export function listCircuitLists(projectId: string) {
@@ -30,6 +30,8 @@ const commandTypeLabels: Record<string, string> = {
"project-device.delete": "Projektgerät gelöscht", "project-device.delete": "Projektgerät gelöscht",
"project-device.sync-rows": "Projektgerät-Verknüpfungen geändert", "project-device.sync-rows": "Projektgerät-Verknüpfungen geändert",
"project.update-settings": "Projekteinstellungen bearbeitet", "project.update-settings": "Projekteinstellungen bearbeitet",
"distribution-board.insert": "Verteilung angelegt",
"distribution-board.delete": "Verteilung entfernt",
"project.restore-state": "Projektstand wiederhergestellt", "project.restore-state": "Projektstand wiederhergestellt",
}; };
@@ -6,6 +6,7 @@ import { CircuitProjectCommandRepository } from "../../db/repositories/circuit-p
import { CircuitSectionReorderProjectCommandRepository } from "../../db/repositories/circuit-section-reorder-project-command.repository.js"; import { CircuitSectionReorderProjectCommandRepository } from "../../db/repositories/circuit-section-reorder-project-command.repository.js";
import { CircuitSectionRenumberProjectCommandRepository } from "../../db/repositories/circuit-section-renumber-project-command.repository.js"; import { CircuitSectionRenumberProjectCommandRepository } from "../../db/repositories/circuit-section-renumber-project-command.repository.js";
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 { ProjectHistoryRepository } from "../../db/repositories/project-history.repository.js"; import { ProjectHistoryRepository } from "../../db/repositories/project-history.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";
@@ -23,6 +24,8 @@ export const circuitDeviceRowMoveProjectCommandStore =
new CircuitDeviceRowMoveProjectCommandRepository(db); new CircuitDeviceRowMoveProjectCommandRepository(db);
export const circuitStructureProjectCommandStore = export const circuitStructureProjectCommandStore =
new CircuitStructureProjectCommandRepository(db); new CircuitStructureProjectCommandRepository(db);
export const distributionBoardStructureProjectCommandStore =
new DistributionBoardStructureProjectCommandRepository(db);
export const circuitSectionReorderProjectCommandStore = export const circuitSectionReorderProjectCommandStore =
new CircuitSectionReorderProjectCommandRepository(db); new CircuitSectionReorderProjectCommandRepository(db);
export const circuitSectionRenumberProjectCommandStore = export const circuitSectionRenumberProjectCommandStore =
@@ -44,6 +47,7 @@ export const projectCommandService = new ProjectCommandService(
circuitDeviceRowStructureProjectCommandStore, circuitDeviceRowStructureProjectCommandStore,
circuitDeviceRowMoveProjectCommandStore, circuitDeviceRowMoveProjectCommandStore,
circuitStructureProjectCommandStore, circuitStructureProjectCommandStore,
distributionBoardStructureProjectCommandStore,
circuitSectionReorderProjectCommandStore, circuitSectionReorderProjectCommandStore,
circuitSectionRenumberProjectCommandStore, circuitSectionRenumberProjectCommandStore,
projectDeviceStructureProjectCommandStore, projectDeviceStructureProjectCommandStore,
@@ -1,7 +1,13 @@
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { db } from "../../db/client.js"; import { db } from "../../db/client.js";
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js"; import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
import {
createDistributionBoardInsertProjectCommand,
createDistributionBoardStructureSnapshot,
} 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 { respondWithProjectCommandError } from "./project-command.controller.js";
const distributionBoardRepository = new DistributionBoardRepository(db); const distributionBoardRepository = new DistributionBoardRepository(db);
@@ -26,9 +32,23 @@ export async function createDistributionBoard(req: Request, res: Response) {
return res.status(400).json({ error: parsed.error.flatten() }); return res.status(400).json({ error: parsed.error.flatten() });
} }
const board = distributionBoardRepository.createWithCircuitListAndDefaultSections( const structure = createDistributionBoardStructureSnapshot(
projectId, projectId,
parsed.data.name parsed.data.name
); );
return res.status(201).json(board); try {
const result = projectCommandService.executeUser({
projectId,
expectedRevision: parsed.data.expectedRevision,
description: "Verteilung anlegen",
command:
createDistributionBoardInsertProjectCommand(structure),
});
return res.status(201).json({
...result,
distributionBoard: structure.distributionBoard,
});
} catch (error) {
return respondWithProjectCommandError(error, res);
}
} }
@@ -15,9 +15,12 @@ export const updateProjectSettingsSchema = z
}) })
.strict(); .strict();
export const createDistributionBoardSchema = z.object({ export const createDistributionBoardSchema = z
name: z.string().min(1), .object({
}); expectedRevision: expectedProjectRevisionSchema,
name: z.string().trim().min(1),
})
.strict();
export const createFloorSchema = z.object({ export const createFloorSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
@@ -0,0 +1,346 @@
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 { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
import { circuitLists } from "../src/db/schema/circuit-lists.js";
import { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
import { projectRevisions } from "../src/db/schema/project-revisions.js";
import { projects } from "../src/db/schema/projects.js";
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
import {
createDistributionBoardInsertProjectCommand,
createDistributionBoardStructureSnapshot,
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
import { createDistributionBoard } from "../src/frontend/utils/api.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 getStructureCounts(context: DatabaseContext) {
return {
boards: context.db.select().from(distributionBoards).all().length,
lists: context.db.select().from(circuitLists).all().length,
sections: context.db.select().from(circuitSections).all().length,
revisions: context.db.select().from(projectRevisions).all().length,
};
}
describe("distribution-board structure project command", () => {
it("builds the fixed setup and sends the expected frontend revision", async () => {
const structure = createDistributionBoardStructureSnapshot(
"project-1",
" UV-02 "
);
assert.equal(structure.distributionBoard.name, "UV-02");
assert.equal(
structure.circuitList.name,
"UV-02 Stromkreisliste"
);
assert.deepEqual(
structure.sections.map((section) => [
section.key,
section.prefix,
]),
[
["lighting", "-1F"],
["single_phase", "-2F"],
["three_phase", "-3F"],
["unassigned", "-UF"],
]
);
assert.throws(
() =>
createDistributionBoardInsertProjectCommand({
...structure,
sections: structure.sections.slice(0, 3),
}),
/incomplete/
);
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 createDistributionBoard("project-1", "UV-02", 7);
} finally {
globalThis.fetch = originalFetch;
}
assert.deepEqual(requests, [
{
url: "/api/projects/project-1/distribution-boards",
body: { name: "UV-02", expectedRevision: 7 },
},
]);
});
it("creates stable structure and supports persisted undo and redo", () => {
const context = createTestDatabase();
try {
const repository =
new DistributionBoardStructureProjectCommandRepository(
context.db
);
const history = new ProjectHistoryRepository(context.db);
const structure = createDistributionBoardStructureSnapshot(
"project-1",
"UV-02"
);
const command =
createDistributionBoardInsertProjectCommand(structure);
const forward = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
assert.deepEqual(getStructureCounts(context), {
boards: 1,
lists: 1,
sections: 4,
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: forward.inverse,
});
assert.deepEqual(getStructureCounts(context), {
boards: 0,
lists: 0,
sections: 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(getStructureCounts(context), {
boards: 1,
lists: 1,
sections: 4,
revisions: 3,
});
assert.ok(
context.db
.select()
.from(distributionBoards)
.where(
eq(
distributionBoards.id,
structure.distributionBoard.id
)
)
.get()
);
} finally {
context.close();
}
});
it("rejects foreign, stale and changed structures without partial writes", () => {
const context = createTestDatabase();
try {
const repository =
new DistributionBoardStructureProjectCommandRepository(
context.db
);
const foreign = createDistributionBoardInsertProjectCommand(
createDistributionBoardStructureSnapshot(
"project-2",
"UV fremd"
)
);
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: foreign,
}),
/does not belong/
);
const structure = createDistributionBoardStructureSnapshot(
"project-1",
"UV-02"
);
const command =
createDistributionBoardInsertProjectCommand(structure);
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command,
}),
ProjectRevisionConflictError
);
assert.deepEqual(getStructureCounts(context), {
boards: 0,
lists: 0,
sections: 0,
revisions: 0,
});
const inserted = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
context.db
.update(distributionBoards)
.set({ name: "Direkt geändert" })
.where(eq(distributionBoards.id, structure.distributionBoard.id))
.run();
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId:
new ProjectHistoryRepository(
context.db
).getNextCommand("project-1", "undo")?.changeSetId,
command: inserted.inverse,
}),
/changed before deletion/
);
assert.equal(
context.db.select().from(projectRevisions).all().length,
1
);
} finally {
context.close();
}
});
it("refuses populated deletion and rolls back late history failures", () => {
const populatedContext = createTestDatabase();
try {
const repository =
new DistributionBoardStructureProjectCommandRepository(
populatedContext.db
);
const structure = createDistributionBoardStructureSnapshot(
"project-1",
"UV-02"
);
const inserted = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardInsertProjectCommand(structure),
});
populatedContext.db
.insert(circuits)
.values({
id: "circuit-1",
circuitListId: structure.circuitList.id,
sectionId: structure.sections[0].id,
equipmentIdentifier: "-1F1",
sortOrder: 10,
})
.run();
const undoStep = new ProjectHistoryRepository(
populatedContext.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,
}),
/populated/
);
} finally {
populatedContext.close();
}
const rollbackContext = createTestDatabase();
try {
rollbackContext.sqlite.exec(`
CREATE TRIGGER fail_distribution_board_history
BEFORE INSERT ON project_history_stack_entries
BEGIN
SELECT RAISE(ABORT, 'forced distribution-board history failure');
END;
`);
const repository =
new DistributionBoardStructureProjectCommandRepository(
rollbackContext.db
);
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createDistributionBoardInsertProjectCommand(
createDistributionBoardStructureSnapshot(
"project-1",
"UV rollback"
)
),
}),
/forced distribution-board history failure/
);
assert.deepEqual(getStructureCounts(rollbackContext), {
boards: 0,
lists: 0,
sections: 0,
revisions: 0,
});
} finally {
rollbackContext.close();
}
});
});
+57
View File
@@ -15,6 +15,7 @@ import { CircuitSectionReorderProjectCommandRepository } from "../src/db/reposit
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 { DistributionBoardRepository } from "../src/db/repositories/distribution-board.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 { 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";
@@ -41,6 +42,10 @@ import { createCircuitSectionReorderProjectCommand } from "../src/domain/models/
import { createCircuitSectionsReorderProjectCommand } from "../src/domain/models/circuit-sections-reorder-project-command.model.js"; import { createCircuitSectionsReorderProjectCommand } from "../src/domain/models/circuit-sections-reorder-project-command.model.js";
import { createCircuitSectionRenumberProjectCommand } from "../src/domain/models/circuit-section-renumber-project-command.model.js"; import { createCircuitSectionRenumberProjectCommand } from "../src/domain/models/circuit-section-renumber-project-command.model.js";
import { createCircuitInsertProjectCommand } from "../src/domain/models/circuit-structure-project-command.model.js"; import { createCircuitInsertProjectCommand } from "../src/domain/models/circuit-structure-project-command.model.js";
import {
createDistributionBoardInsertProjectCommand,
createDistributionBoardStructureSnapshot,
} from "../src/domain/models/distribution-board-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";
@@ -99,6 +104,7 @@ function createService(context: DatabaseContext) {
new CircuitDeviceRowStructureProjectCommandRepository(context.db), new CircuitDeviceRowStructureProjectCommandRepository(context.db),
new CircuitDeviceRowMoveProjectCommandRepository(context.db), new CircuitDeviceRowMoveProjectCommandRepository(context.db),
new CircuitStructureProjectCommandRepository(context.db), new CircuitStructureProjectCommandRepository(context.db),
new DistributionBoardStructureProjectCommandRepository(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),
@@ -979,6 +985,57 @@ describe("project command service", () => {
} }
}); });
it("dispatches distribution-board setup and its persisted inverse", () => {
const context = createTestDatabase();
try {
const service = createService(context);
const structure = createDistributionBoardStructureSnapshot(
"project-1",
"UV-02"
);
const created = service.executeUser({
projectId: "project-1",
expectedRevision: 0,
command:
createDistributionBoardInsertProjectCommand(structure),
});
assert.equal(created.history.currentRevision, 1);
assert.ok(
context.db
.select()
.from(circuitSections)
.where(
eq(
circuitSections.circuitListId,
structure.circuitList.id
)
)
.get()
);
const undone = createService(context).undo({
projectId: "project-1",
expectedRevision: 1,
});
assert.equal(undone.history.currentRevision, 2);
assert.equal(
context.db
.select()
.from(circuitSections)
.where(
eq(
circuitSections.circuitListId,
structure.circuitList.id
)
)
.all().length,
0
);
} 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 {
@@ -15,6 +15,7 @@ import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/reposi
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 { DistributionBoardRepository } from "../src/db/repositories/distribution-board.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";
@@ -132,6 +133,7 @@ function createService(context: DatabaseContext) {
new CircuitDeviceRowStructureProjectCommandRepository(context.db), new CircuitDeviceRowStructureProjectCommandRepository(context.db),
new CircuitDeviceRowMoveProjectCommandRepository(context.db), new CircuitDeviceRowMoveProjectCommandRepository(context.db),
new CircuitStructureProjectCommandRepository(context.db), new CircuitStructureProjectCommandRepository(context.db),
new DistributionBoardStructureProjectCommandRepository(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),