import { asc, eq } from "drizzle-orm"; import { circuitGroupDeleteSubtreeCommandType, circuitGroupRestoreSubtreeCommandType, assertCircuitGroupDeleteSubtreeProjectCommand, assertCircuitGroupRestoreSubtreeProjectCommand, createCircuitGroupDeleteSubtreeProjectCommand, createCircuitGroupRestoreSubtreeProjectCommand, type CircuitGroupSubtreeProjectCommand, } from "../../domain/models/circuit-group-subtree-project-command.model.js"; import type { CircuitGroupSubtreeSnapshot, CircuitProtectionDeviceSnapshot, } from "../../domain/models/circuit-group-subtree-snapshot.model.js"; import type { CircuitGroupSubtreeProjectCommandStore, ExecuteCircuitGroupSubtreeCommandInput, } from "../../domain/ports/circuit-group-subtree-project-command.store.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; import { circuitSections } from "../schema/circuit-sections.js"; 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 { 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 { constructor(private readonly database: AppDatabase) {} capture(projectId: string, groupId: string) { return this.captureFrom(this.database, projectId, groupId); } execute(input: ExecuteCircuitGroupSubtreeCommandInput) { if (input.command.type === circuitGroupDeleteSubtreeCommandType) { assertCircuitGroupDeleteSubtreeProjectCommand(input.command); } else { assertCircuitGroupRestoreSubtreeProjectCommand(input.command); } return executeProjectCommandTransactionWithAppliedForward< CircuitGroupSubtreeProjectCommand, CircuitGroupSubtreeProjectCommand, CircuitGroupSubtreeProjectCommand >( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) ); } private applyCommand( database: AppDatabase, projectId: string, command: CircuitGroupSubtreeProjectCommand ): AppliedProjectCommand { if (command.type === circuitGroupDeleteSubtreeCommandType) { const current = this.captureFrom( database, projectId, command.payload.snapshot.group.id ); if (!sameSnapshot(current, command.payload.snapshot)) { throw new Error( "Circuit-group subtree changed before deletion." ); } assertCircuitsAreNotReferencedAsFeeders( database, 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)) .run(); if (deleted.changes !== 1) { throw new Error("Circuit-group subtree could not be deleted."); } 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); 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."); } private captureFrom( database: AppDatabase, projectId: string, groupId: string ): CircuitGroupSubtreeSnapshot { const group = database .select() .from(circuitSections) .where(eq(circuitSections.id, groupId)) .get(); if (!group || group.category === null || group.groupNumber === null) { throw new Error("Circuit group not found."); } this.assertListOwnership(database, projectId, group.circuitListId); const components = database .select() .from(distributionBoardComponents) .where(eq(distributionBoardComponents.sectionId, group.id)) .orderBy( asc(distributionBoardComponents.sortOrder), asc(distributionBoardComponents.id) ) .all() .map((component) => ({ component: { ...component, role: component.role as | "group_upstream_protection" | "group_residual_current_protection", }, protectionDevice: database .select() .from(distributionBoardComponentProtectionDevices) .where( eq( distributionBoardComponentProtectionDevices.componentId, component.id ) ) .get() ?? null, })); const circuitSnapshots = database .select() .from(circuits) .where(eq(circuits.sectionId, group.id)) .orderBy(asc(circuits.sortOrder), asc(circuits.id)) .all() .map((circuit) => { const { isReserve, ...values } = circuit; return { circuit: { ...values, isReserve: Boolean(isReserve), deviceRows: database .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.circuitId, circuit.id)) .orderBy( asc(circuitDeviceRows.sortOrder), asc(circuitDeviceRows.id) ) .all(), }, protectionDevice: (database .select() .from(circuitProtectionDevices) .where(eq(circuitProtectionDevices.circuitId, circuit.id)) .get() as CircuitProtectionDeviceSnapshot | undefined) ?? null, }; }); return { group: { ...group, category: group.category, groupNumber: group.groupNumber, }, components, circuits: circuitSnapshots, }; } private restore( database: AppDatabase, projectId: string, snapshot: CircuitGroupSubtreeSnapshot ) { this.assertListOwnership( database, projectId, snapshot.group.circuitListId ); const existing = database .select({ id: circuitSections.id }) .from(circuitSections) .where(eq(circuitSections.id, snapshot.group.id)) .get(); if (existing) { throw new Error("Circuit group already exists before restoration."); } database.insert(circuitSections).values(snapshot.group).run(); for (const component of snapshot.components) { database .insert(distributionBoardComponents) .values(component.component) .run(); if (component.protectionDevice !== null) { database .insert(distributionBoardComponentProtectionDevices) .values(component.protectionDevice) .run(); } } for (const entry of snapshot.circuits) { const { deviceRows, isReserve, ...circuit } = entry.circuit; database .insert(circuits) .values({ ...circuit, isReserve: isReserve ? 1 : 0 }) .run(); if (entry.protectionDevice !== null) { database .insert(circuitProtectionDevices) .values(entry.protectionDevice) .run(); } if (deviceRows.length > 0) { database .insert(circuitDeviceRows) .values(deviceRows.map(toCircuitDeviceRowInsertValues)) .run(); } } } private assertListOwnership( database: AppDatabase, projectId: string, circuitListId: string ) { const list = database .select({ projectId: circuitLists.projectId }) .from(circuitLists) .where(eq(circuitLists.id, circuitListId)) .get(); if (!list || list.projectId !== projectId) { throw new Error("Circuit-group list does not belong to project."); } } } function sameSnapshot( left: CircuitGroupSubtreeSnapshot, right: CircuitGroupSubtreeSnapshot ) { return canonicalJson(left) === canonicalJson(right); } function canonicalJson(value: unknown): string { if (Array.isArray(value)) { return `[${value.map(canonicalJson).join(",")}]`; } if (value !== null && typeof value === "object") { return `{${Object.entries(value) .sort(([left], [right]) => left.localeCompare(right)) .map( ([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}` ) .join(",")}}`; } return JSON.stringify(value) ?? "null"; }