Complete Revit object editor assignments

This commit is contained in:
Julian Appel 2026-09-30 14:35:14 +02:00
parent 4489cd76f0
commit 77d775fde1
25 changed files with 853 additions and 77 deletions

View file

@ -23,8 +23,15 @@ import {
toCircuitDeviceRowInsertValues,
toCircuitDeviceRowSnapshot,
} from "./circuit-device-row-structure.persistence.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js";
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
import {
applyExternalObjectLinks,
captureExternalObjectDetachTransitions,
invertExternalObjectLinkTransitions,
loadExpectedExternalObjectTransitions,
type ExternalObjectLinkTransition,
} from "./external-object-assignment.persistence.js";
export class CircuitDeviceRowStructureProjectCommandRepository
implements CircuitDeviceRowStructureProjectCommandStore
@ -32,7 +39,11 @@ export class CircuitDeviceRowStructureProjectCommandRepository
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteCircuitDeviceRowStructureCommandInput) {
return executeProjectCommandTransaction(
return executeProjectCommandTransactionWithAppliedForward<
CircuitDeviceRowStructureProjectCommand,
CircuitDeviceRowStructureProjectCommand,
CircuitDeviceRowStructureProjectCommand
>(
this.database,
input,
(tx) =>
@ -50,14 +61,15 @@ export class CircuitDeviceRowStructureProjectCommandRepository
projectId: string,
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
command: CircuitDeviceRowStructureProjectCommand
): CircuitDeviceRowStructureProjectCommand {
): AppliedProjectCommand<CircuitDeviceRowStructureProjectCommand, CircuitDeviceRowStructureProjectCommand> {
if (command.type === circuitDeviceRowInsertCommandType) {
assertCircuitDeviceRowInsertProjectCommand(command);
return this.insert(
database,
projectId,
source,
command.payload.row
command.payload.row,
command.payload.externalObjects ?? []
);
}
if (command.type === circuitDeviceRowDeleteCommandType) {
@ -66,7 +78,8 @@ export class CircuitDeviceRowStructureProjectCommandRepository
database,
projectId,
command.payload.rowId,
command.payload.expectedCircuitId
command.payload.expectedCircuitId,
command.payload.externalObjects
);
}
throw new Error("Unsupported circuit device-row structure command.");
@ -76,7 +89,8 @@ export class CircuitDeviceRowStructureProjectCommandRepository
database: AppDatabase,
projectId: string,
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
row: CircuitDeviceRowSnapshot
row: CircuitDeviceRowSnapshot,
externalObjects: ExternalObjectLinkTransition[]
) {
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
throw new Error("Device-row phase type is invalid.");
@ -107,17 +121,23 @@ export class CircuitDeviceRowStructureProjectCommandRepository
}
updateDerivedCircuitVoltage(database, projectId, row.circuitId);
return createCircuitDeviceRowDeleteProjectCommand(
row.id,
row.circuitId
);
if (externalObjects.length > 0) {
loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert.");
applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert.");
}
return {
forward: createCircuitDeviceRowInsertProjectCommand(row, externalObjects),
inverse: createCircuitDeviceRowDeleteProjectCommand(row.id, row.circuitId, invertExternalObjectLinkTransitions(externalObjects)),
};
}
private delete(
database: AppDatabase,
projectId: string,
rowId: string,
expectedCircuitId: string
expectedCircuitId: string,
suppliedExternalObjects?: ExternalObjectLinkTransition[]
) {
const row = database
.select()
@ -130,6 +150,10 @@ export class CircuitDeviceRowStructureProjectCommandRepository
);
}
this.assertCircuitInProject(database, projectId, row.circuitId);
const externalObjects = suppliedExternalObjects ??
captureExternalObjectDetachTransitions(database, projectId, [rowId]);
loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Löschen geändert.");
applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Löschen geändert.");
const result = database
.delete(circuitDeviceRows)
@ -164,9 +188,10 @@ export class CircuitDeviceRowStructureProjectCommandRepository
expectedCircuitId
);
return createCircuitDeviceRowInsertProjectCommand(
toCircuitDeviceRowSnapshot(row)
);
return {
forward: createCircuitDeviceRowDeleteProjectCommand(rowId, expectedCircuitId, externalObjects),
inverse: createCircuitDeviceRowInsertProjectCommand(toCircuitDeviceRowSnapshot(row), invertExternalObjectLinkTransitions(externalObjects)),
};
}
private assertCircuitInProject(

View file

@ -25,8 +25,14 @@ import { circuits } from "../schema/circuits.js";
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js";
import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
import {
applyExternalObjectLinks,
captureExternalObjectDetachTransitions,
invertExternalObjectLinkTransitions,
loadExpectedExternalObjectTransitions,
} from "./external-object-assignment.persistence.js";
export class CircuitGroupSubtreeProjectCommandRepository
implements CircuitGroupSubtreeProjectCommandStore
@ -43,7 +49,11 @@ export class CircuitGroupSubtreeProjectCommandRepository
} else {
assertCircuitGroupRestoreSubtreeProjectCommand(input.command);
}
return executeProjectCommandTransaction(
return executeProjectCommandTransactionWithAppliedForward<
CircuitGroupSubtreeProjectCommand,
CircuitGroupSubtreeProjectCommand,
CircuitGroupSubtreeProjectCommand
>(
this.database,
input,
(tx) => this.applyCommand(tx, input.projectId, input.command)
@ -54,7 +64,7 @@ export class CircuitGroupSubtreeProjectCommandRepository
database: AppDatabase,
projectId: string,
command: CircuitGroupSubtreeProjectCommand
) {
): AppliedProjectCommand<CircuitGroupSubtreeProjectCommand, CircuitGroupSubtreeProjectCommand> {
if (command.type === circuitGroupDeleteSubtreeCommandType) {
const current = this.captureFrom(
database,
@ -71,6 +81,14 @@ export class CircuitGroupSubtreeProjectCommandRepository
projectId,
current.circuits.map((entry) => entry.circuit.id)
);
const externalObjects = command.payload.externalObjects ??
captureExternalObjectDetachTransitions(
database,
projectId,
current.circuits.flatMap((entry) => entry.circuit.deviceRows.map((row) => row.id))
);
loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Löschen geändert.");
applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Löschen geändert.");
const deleted = database
.delete(circuitSections)
.where(eq(circuitSections.id, current.group.id))
@ -78,15 +96,20 @@ export class CircuitGroupSubtreeProjectCommandRepository
if (deleted.changes !== 1) {
throw new Error("Circuit-group subtree could not be deleted.");
}
return createCircuitGroupRestoreSubtreeProjectCommand(
command.payload.snapshot
);
return {
forward: createCircuitGroupDeleteSubtreeProjectCommand(command.payload.snapshot, externalObjects),
inverse: createCircuitGroupRestoreSubtreeProjectCommand(command.payload.snapshot, invertExternalObjectLinkTransitions(externalObjects)),
};
}
if (command.type === circuitGroupRestoreSubtreeCommandType) {
this.restore(database, projectId, command.payload.snapshot);
return createCircuitGroupDeleteSubtreeProjectCommand(
command.payload.snapshot
);
const externalObjects = command.payload.externalObjects ?? [];
loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert.");
applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert.");
return {
forward: createCircuitGroupRestoreSubtreeProjectCommand(command.payload.snapshot, externalObjects),
inverse: createCircuitGroupDeleteSubtreeProjectCommand(command.payload.snapshot, invertExternalObjectLinkTransitions(externalObjects)),
};
}
throw new Error("Unsupported circuit-group subtree command.");
}

View file

@ -25,10 +25,17 @@ import {
toCircuitDeviceRowInsertValues,
toCircuitDeviceRowSnapshot,
} from "./circuit-device-row-structure.persistence.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js";
import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js";
import {
applyExternalObjectLinks,
captureExternalObjectDetachTransitions,
invertExternalObjectLinkTransitions,
loadExpectedExternalObjectTransitions,
type ExternalObjectLinkTransition,
} from "./external-object-assignment.persistence.js";
export class CircuitStructureProjectCommandRepository
implements CircuitStructureProjectCommandStore
@ -36,11 +43,15 @@ export class CircuitStructureProjectCommandRepository
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteCircuitStructureCommandInput) {
return executeProjectCommandTransaction(
return executeProjectCommandTransactionWithAppliedForward<
CircuitStructureProjectCommand,
CircuitStructureProjectCommand,
CircuitStructureProjectCommand
>(
this.database,
input,
(tx) =>
this.applyInTransaction(
this.applyWithForward(
tx,
input.projectId,
input.source,
@ -59,13 +70,23 @@ export class CircuitStructureProjectCommandRepository
source: ExecuteCircuitStructureCommandInput["source"],
command: CircuitStructureProjectCommand
): CircuitStructureProjectCommand {
return this.applyWithForward(database, projectId, source, command).inverse;
}
private applyWithForward(
database: AppDatabase,
projectId: string,
source: ExecuteCircuitStructureCommandInput["source"],
command: CircuitStructureProjectCommand
): AppliedProjectCommand<CircuitStructureProjectCommand, CircuitStructureProjectCommand> {
if (command.type === circuitInsertCommandType) {
assertCircuitInsertProjectCommand(command);
return this.insert(
database,
projectId,
source,
command.payload.circuit
command.payload.circuit,
command.payload.externalObjects ?? []
);
}
if (command.type === circuitDeleteCommandType) {
@ -74,7 +95,8 @@ export class CircuitStructureProjectCommandRepository
database,
projectId,
command.payload.circuitId,
command.payload.expectedCircuitListId
command.payload.expectedCircuitListId,
command.payload.externalObjects
);
}
throw new Error("Unsupported circuit structure command.");
@ -84,7 +106,8 @@ export class CircuitStructureProjectCommandRepository
database: AppDatabase,
projectId: string,
source: ExecuteCircuitStructureCommandInput["source"],
snapshot: CircuitSnapshot
snapshot: CircuitSnapshot,
externalObjects: ExternalObjectLinkTransition[]
) {
this.assertCircuitLocation(database, projectId, snapshot);
if (source === "user") {
@ -172,17 +195,23 @@ export class CircuitStructureProjectCommandRepository
.run();
}
return createCircuitDeleteProjectCommand(
snapshot.id,
snapshot.circuitListId
);
if (externalObjects.length > 0) {
loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert.");
applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert.");
}
return {
forward: createCircuitInsertProjectCommand(snapshot, externalObjects),
inverse: createCircuitDeleteProjectCommand(snapshot.id, snapshot.circuitListId, invertExternalObjectLinkTransitions(externalObjects)),
};
}
private delete(
database: AppDatabase,
projectId: string,
circuitId: string,
expectedCircuitListId: string
expectedCircuitListId: string,
suppliedExternalObjects?: ExternalObjectLinkTransition[]
) {
const circuit = database
.select()
@ -220,7 +249,12 @@ export class CircuitStructureProjectCommandRepository
.from(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, circuit.id))
.get();
const inverse = createCircuitInsertProjectCommand({
const externalObjects = suppliedExternalObjects ?? captureExternalObjectDetachTransitions(
database, projectId, rows.map((row) => row.id)
);
loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Löschen geändert.");
applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Löschen geändert.");
const circuitSnapshot: CircuitSnapshot = {
id: circuit.id,
circuitListId: circuit.circuitListId,
sectionId: circuit.sectionId,
@ -239,7 +273,7 @@ export class CircuitStructureProjectCommandRepository
remark: circuit.remark,
deviceRows: rows.map(toCircuitDeviceRowSnapshot),
protectionDevice: protectionDevice ?? null,
});
};
assertCircuitsAreNotReferencedAsFeeders(database, projectId, [circuit.id]);
@ -255,7 +289,10 @@ export class CircuitStructureProjectCommandRepository
if (result.changes !== 1) {
throw new Error("Circuit could not be deleted.");
}
return inverse;
return {
forward: createCircuitDeleteProjectCommand(circuitId, expectedCircuitListId, externalObjects),
inverse: createCircuitInsertProjectCommand(circuitSnapshot, invertExternalObjectLinkTransitions(externalObjects)),
};
}
private assertCircuitLocation(

View file

@ -46,6 +46,7 @@ import { floors } from "../schema/floors.js";
import { projects } from "../schema/projects.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
import { assertDistributionBoardHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js";
import { CircuitStructureProjectCommandRepository } from "./circuit-structure-project-command.repository.js";
export class DistributionBoardStructureProjectCommandRepository
@ -290,6 +291,7 @@ export class DistributionBoardStructureProjectCommandRepository
}
assertBoardsAreNotReferencedAsFeeders(database, projectId, [board.id]);
assertDistributionBoardHasNoExternalObjects(database, projectId, board.id);
const deleted = database
.delete(distributionBoards)

View file

@ -33,6 +33,7 @@ import { rooms } from "../schema/rooms.js";
import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
import { assertDistributionBoardHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js";
export class DistributionBoardSubtreeProjectCommandRepository
implements DistributionBoardSubtreeProjectCommandStore
@ -86,6 +87,7 @@ export class DistributionBoardSubtreeProjectCommandRepository
assertBoardsAreNotReferencedAsFeeders(database, projectId, [
current.distributionBoard.id,
]);
assertDistributionBoardHasNoExternalObjects(database, projectId, current.distributionBoard.id);
const deleted = database
.delete(distributionBoards)
.where(

View file

@ -11,6 +11,27 @@ export interface ExternalObjectLinkTransition {
target: ExternalModelObjectSnapshot;
}
export function captureExternalObjectDetachTransitions(
database: AppDatabase,
projectId: string,
rowIds: string[]
): ExternalObjectLinkTransition[] {
if (rowIds.length === 0) return [];
return database.select().from(externalModelObjects)
.where(and(
eq(externalModelObjects.projectId, projectId),
inArray(externalModelObjects.circuitDeviceRowId, rowIds)
)).all()
.map(toExternalObjectSnapshot)
.map((expected) => ({ expected, target: { ...expected, circuitDeviceRowId: null } }));
}
export function invertExternalObjectLinkTransitions(
transitions: ExternalObjectLinkTransition[]
): ExternalObjectLinkTransition[] {
return transitions.map(({ expected, target }) => ({ expected: target, target: expected }));
}
export function loadExpectedExternalObjectTransitions(
database: AppDatabase,
projectId: string,
@ -86,7 +107,13 @@ export function applyExternalObjectLinks(
) {
for (const { expected, target } of transitions) {
const updated = database.update(externalModelObjects)
.set({ circuitDeviceRowId: target.circuitDeviceRowId })
.set({
circuitDeviceRowId: target.circuitDeviceRowId,
distributionBoardId: target.distributionBoardId,
linkedProjectDeviceId: target.linkedProjectDeviceId,
planningValues: target.planningValues,
overriddenFields: target.overriddenFields,
})
.where(and(
eq(externalModelObjects.id, expected.id),
expected.circuitDeviceRowId === null

View file

@ -0,0 +1,37 @@
import { and, eq } from "drizzle-orm";
import type { AppDatabase } from "../database-context.js";
import { externalModelObjects } from "../schema/external-model-objects.js";
import { externalRoomMappings } from "../schema/external-room-mappings.js";
export function assertDistributionBoardHasNoExternalObjects(
database: AppDatabase,
projectId: string,
distributionBoardId: string
) {
const reference = database.select({ id: externalModelObjects.id })
.from(externalModelObjects)
.where(and(
eq(externalModelObjects.projectId, projectId),
eq(externalModelObjects.distributionBoardId, distributionBoardId)
)).limit(1).get();
if (reference) {
throw new Error("Der Verteiler kann nicht gelöscht werden, solange Revit-Objekte auf ihn verweisen.");
}
}
export function assertRoomHasNoExternalObjects(
database: AppDatabase,
projectId: string,
roomId: string
) {
const reference = database.select({ id: externalModelObjects.id })
.from(externalModelObjects)
.innerJoin(externalRoomMappings, eq(externalRoomMappings.id, externalModelObjects.externalRoomMappingId))
.where(and(
eq(externalModelObjects.projectId, projectId),
eq(externalRoomMappings.roomId, roomId)
)).limit(1).get();
if (reference) {
throw new Error("Der Raum kann nicht gelöscht werden, solange Revit-Objekte auf ihn verweisen.");
}
}

View file

@ -46,6 +46,7 @@ import { distributionBoards } from "../schema/distribution-boards.js";
import { projects } from "../schema/projects.js";
import { rooms } from "../schema/rooms.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { assertRoomHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js";
export class ProjectLocationStructureProjectCommandRepository
implements ProjectLocationStructureProjectCommandStore
@ -439,6 +440,7 @@ export class ProjectLocationStructureProjectCommandRepository
"Der Raum kann nicht gelöscht werden, solange er in einem Stromkreis verwendet wird."
);
}
assertRoomHasNoExternalObjects(database, projectId, expected.id);
const deleted = database
.delete(rooms)
.where(