Persist circuit group moves

This commit is contained in:
2026-07-30 20:03:11 +02:00
parent 9ad91887b2
commit 3399fcd88b
15 changed files with 652 additions and 4 deletions
@@ -0,0 +1,104 @@
import { and, eq } from "drizzle-orm";
import {
assertCircuitGroupMoveProjectCommand,
createCircuitGroupMoveProjectCommand,
invertCircuitGroupMovePlan,
} from "../../domain/models/circuit-group-move-project-command.model.js";
import type {
CircuitGroupMoveProjectCommandStore,
ExecuteCircuitGroupMoveCommandInput,
} from "../../domain/ports/circuit-group-move-project-command.store.js";
import { parseGroupedEquipmentIdentifier } from "../../domain/services/circuit-group-numbering.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 { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
export class CircuitGroupMoveProjectCommandRepository
implements CircuitGroupMoveProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteCircuitGroupMoveCommandInput) {
assertCircuitGroupMoveProjectCommand(input.command);
return executeProjectCommandTransaction(
this.database,
input,
(tx) => this.applyCommand(tx, input)
);
}
private applyCommand(
database: AppDatabase,
input: ExecuteCircuitGroupMoveCommandInput
) {
const plan = input.command.payload;
const list = database
.select({ projectId: circuitLists.projectId })
.from(circuitLists)
.where(eq(circuitLists.id, plan.circuitListId))
.get();
if (!list || list.projectId !== input.projectId) {
throw new Error("Circuit list does not belong to project.");
}
const source = database
.select()
.from(circuitSections)
.where(eq(circuitSections.id, plan.expectedSectionId))
.get();
const target = database
.select()
.from(circuitSections)
.where(eq(circuitSections.id, plan.targetSectionId))
.get();
const expectedBmk = parseGroupedEquipmentIdentifier(
plan.expectedEquipmentIdentifier
);
const targetBmk = parseGroupedEquipmentIdentifier(
plan.targetEquipmentIdentifier
);
if (
!source ||
!target ||
source.circuitListId !== plan.circuitListId ||
target.circuitListId !== plan.circuitListId ||
source.category === null ||
source.category !== target.category ||
source.category !== expectedBmk?.category ||
source.groupNumber !== expectedBmk.groupNumber ||
target.category !== targetBmk?.category ||
target.groupNumber !== targetBmk.groupNumber
) {
throw new Error(
"Circuit move requires matching source and target groups."
);
}
const updated = database
.update(circuits)
.set({
sectionId: plan.targetSectionId,
equipmentIdentifier: plan.targetEquipmentIdentifier,
sortOrder: plan.targetSortOrder,
})
.where(
and(
eq(circuits.id, plan.circuitId),
eq(circuits.circuitListId, plan.circuitListId),
eq(circuits.sectionId, plan.expectedSectionId),
eq(
circuits.equipmentIdentifier,
plan.expectedEquipmentIdentifier
),
eq(circuits.sortOrder, plan.expectedSortOrder)
)
)
.run();
if (updated.changes !== 1) {
throw new Error("Circuit changed before group move.");
}
return createCircuitGroupMoveProjectCommand(
invertCircuitGroupMovePlan(plan)
);
}
}
@@ -0,0 +1,92 @@
import { parseGroupedEquipmentIdentifier } from "../services/circuit-group-numbering.js";
import type { CircuitGroupMovePlan } from "../services/circuit-group-move-planning.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const circuitGroupMoveCommandType = "circuit.move-group" as const;
export const circuitGroupMoveCommandSchemaVersion = 1 as const;
export interface CircuitGroupMoveProjectCommand
extends SerializedProjectCommand<CircuitGroupMovePlan> {
schemaVersion: typeof circuitGroupMoveCommandSchemaVersion;
type: typeof circuitGroupMoveCommandType;
}
export function createCircuitGroupMoveProjectCommand(
plan: CircuitGroupMovePlan
): CircuitGroupMoveProjectCommand {
const command: CircuitGroupMoveProjectCommand = {
schemaVersion: circuitGroupMoveCommandSchemaVersion,
type: circuitGroupMoveCommandType,
payload: plan,
};
assertCircuitGroupMoveProjectCommand(command);
return command;
}
export function assertCircuitGroupMoveProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitGroupMoveProjectCommand {
if (
command.schemaVersion !== circuitGroupMoveCommandSchemaVersion ||
command.type !== circuitGroupMoveCommandType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 8
) {
throw new Error("Unsupported circuit group-move command.");
}
for (const field of [
"circuitId",
"circuitListId",
"expectedSectionId",
"targetSectionId",
"expectedEquipmentIdentifier",
"targetEquipmentIdentifier",
] as const) {
if (
typeof command.payload[field] !== "string" ||
!command.payload[field].trim()
) {
throw new Error(`Circuit group-move ${field} is invalid.`);
}
}
if (
command.payload.expectedSectionId === command.payload.targetSectionId ||
!Number.isFinite(command.payload.expectedSortOrder) ||
!Number.isFinite(command.payload.targetSortOrder)
) {
throw new Error("Circuit group-move positions are invalid.");
}
const expected = parseGroupedEquipmentIdentifier(
command.payload.expectedEquipmentIdentifier as string
);
const target = parseGroupedEquipmentIdentifier(
command.payload.targetEquipmentIdentifier as string
);
if (
expected?.kind !== "circuit" ||
target?.kind !== "circuit" ||
expected.category !== target.category ||
expected.groupNumber === target.groupNumber
) {
throw new Error("Circuit group-move BMKs are incompatible.");
}
}
export function invertCircuitGroupMovePlan(
plan: CircuitGroupMovePlan
): CircuitGroupMovePlan {
return {
circuitId: plan.circuitId,
circuitListId: plan.circuitListId,
expectedSectionId: plan.targetSectionId,
targetSectionId: plan.expectedSectionId,
expectedEquipmentIdentifier: plan.targetEquipmentIdentifier,
targetEquipmentIdentifier: plan.expectedEquipmentIdentifier,
expectedSortOrder: plan.targetSortOrder,
targetSortOrder: plan.expectedSortOrder,
};
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
@@ -0,0 +1,22 @@
import type { CircuitGroupMoveProjectCommand } from "../models/circuit-group-move-project-command.model.js";
import type {
AppendedProjectRevision,
ProjectRevisionSource,
} from "./project-revision.store.js";
export interface ExecuteCircuitGroupMoveCommandInput {
projectId: string;
expectedRevision: number;
source: ProjectRevisionSource;
description?: string;
actorId?: string;
historyTargetChangeSetId?: string;
command: CircuitGroupMoveProjectCommand;
}
export interface CircuitGroupMoveProjectCommandStore {
execute(input: ExecuteCircuitGroupMoveCommandInput): {
revision: AppendedProjectRevision;
inverse: CircuitGroupMoveProjectCommand;
};
}
@@ -100,6 +100,7 @@ import type { CircuitSectionRenumberProjectCommandStore } from "../ports/circuit
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
import type { CircuitGroupStructureProjectCommandStore } from "../ports/circuit-group-structure-project-command.store.js";
import type { CircuitGroupRenumberProjectCommandStore } from "../ports/circuit-group-renumber-project-command.store.js";
import type { CircuitGroupMoveProjectCommandStore } from "../ports/circuit-group-move-project-command.store.js";
import type { DistributionBoardStructureProjectCommandStore } from "../ports/distribution-board-structure-project-command.store.js";
import type { DistributionBoardComponentStructureProjectCommandStore } from "../ports/distribution-board-component-structure-project-command.store.js";
import type {
@@ -126,6 +127,10 @@ import {
assertCircuitGroupRenumberProjectCommand,
circuitGroupRenumberCommandType,
} from "../models/circuit-group-renumber-project-command.model.js";
import {
assertCircuitGroupMoveProjectCommand,
circuitGroupMoveCommandType,
} from "../models/circuit-group-move-project-command.model.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 { ProjectDeviceRowSyncProjectCommandStore } from "../ports/project-device-row-sync-project-command.store.js";
@@ -163,6 +168,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
private readonly distributionBoardComponentStructureStore: DistributionBoardComponentStructureProjectCommandStore,
private readonly circuitGroupStructureStore: CircuitGroupStructureProjectCommandStore,
private readonly circuitGroupRenumberStore: CircuitGroupRenumberProjectCommandStore,
private readonly circuitGroupMoveStore: CircuitGroupMoveProjectCommandStore,
private readonly historyStore: ProjectHistoryStore
) {}
@@ -386,6 +392,13 @@ export class ProjectCommandService implements ProjectCommandExecutor {
command: input.command,
}).revision;
}
case circuitGroupMoveCommandType: {
assertCircuitGroupMoveProjectCommand(input.command);
return this.circuitGroupMoveStore.execute({
...input,
command: input.command,
}).revision;
}
case projectFloorInsertCommandType: {
assertProjectFloorInsertProjectCommand(input.command);
return this.projectLocationStructureStore.execute({
@@ -41,6 +41,7 @@ const commandTypeLabels: Record<string, string> = {
"circuit-group.update": "Stromkreisgruppe bearbeitet",
"circuit-group.reorder": "Stromkreisgruppen sortiert",
"circuit-group.renumber": "Stromkreisgruppen neu nummeriert",
"circuit.move-group": "Stromkreis in andere Gruppe verschoben",
"project-floor.insert": "Geschoss angelegt",
"project-floor.delete": "Geschoss entfernt",
"project-room.insert": "Raum angelegt",
@@ -8,6 +8,7 @@ import { CircuitSectionRenumberProjectCommandRepository } from "../../db/reposit
import { CircuitStructureProjectCommandRepository } from "../../db/repositories/circuit-structure-project-command.repository.js";
import { CircuitGroupStructureProjectCommandRepository } from "../../db/repositories/circuit-group-structure-project-command.repository.js";
import { CircuitGroupRenumberProjectCommandRepository } from "../../db/repositories/circuit-group-renumber-project-command.repository.js";
import { CircuitGroupMoveProjectCommandRepository } from "../../db/repositories/circuit-group-move-project-command.repository.js";
import { DistributionBoardStructureProjectCommandRepository } from "../../db/repositories/distribution-board-structure-project-command.repository.js";
import { DistributionBoardComponentStructureProjectCommandRepository } from "../../db/repositories/distribution-board-component-structure-project-command.repository.js";
import { ProjectHistoryRepository } from "../../db/repositories/project-history.repository.js";
@@ -32,6 +33,8 @@ export const circuitGroupStructureProjectCommandStore =
new CircuitGroupStructureProjectCommandRepository(db);
export const circuitGroupRenumberProjectCommandStore =
new CircuitGroupRenumberProjectCommandRepository(db);
export const circuitGroupMoveProjectCommandStore =
new CircuitGroupMoveProjectCommandRepository(db);
export const distributionBoardStructureProjectCommandStore =
new DistributionBoardStructureProjectCommandRepository(db);
export const distributionBoardComponentStructureProjectCommandStore =
@@ -71,5 +74,6 @@ export const projectCommandService = new ProjectCommandService(
distributionBoardComponentStructureProjectCommandStore,
circuitGroupStructureProjectCommandStore,
circuitGroupRenumberProjectCommandStore,
circuitGroupMoveProjectCommandStore,
projectHistoryStore
);