diff --git a/.changeset/sixty-carpets-cheat.md b/.changeset/sixty-carpets-cheat.md new file mode 100644 index 0000000000..a3ee5c9225 --- /dev/null +++ b/.changeset/sixty-carpets-cheat.md @@ -0,0 +1,5 @@ +--- +'@finos/legend-application-studio': minor +'@finos/legend-graph': minor +--- +Support generating models from relational database. diff --git a/packages/legend-application-studio/src/components/editor/side-bar/Explorer.tsx b/packages/legend-application-studio/src/components/editor/side-bar/Explorer.tsx index 6036c8d624..54e2c636d6 100644 --- a/packages/legend-application-studio/src/components/editor/side-bar/Explorer.tsx +++ b/packages/legend-application-studio/src/components/editor/side-bar/Explorer.tsx @@ -111,8 +111,13 @@ import { guaranteeRelationalDatabaseConnection, extractDependencyGACoordinateFromRootPackageName, type FunctionActivatorConfiguration, + Database, } from '@finos/legend-graph'; -import { useApplicationStore } from '@finos/legend-application'; +import { + ActionAlertActionType, + ActionAlertType, + useApplicationStore, +} from '@finos/legend-application'; import { getPackageableElementOptionFormatter, type PackageableElementOption, @@ -452,6 +457,9 @@ const isRelationalDatabaseConnection = ( val instanceof PackageableConnection && val.connectionValue instanceof RelationalDatabaseConnection; +const isRelationalDatabase = (val: PackageableElement | undefined): boolean => + val instanceof Database; + const ExplorerContextMenu = observer( forwardRef< HTMLDivElement, @@ -523,6 +531,46 @@ const ExplorerContextMenu = observer( } }, ); + const generateModelsFromDatabaseSpecification = + editorStore.applicationStore.guardUnhandledError(async () => { + if (isRelationalDatabase(node?.packageableElement)) { + const databasePath = guaranteeNonEmptyString( + node?.packageableElement.path, + ); + const graph = editorStore.graphManagerState.graph; + if (graph.getDatabase(databasePath).joins.length === 0) { + applicationStore.alertService.setActionAlertInfo({ + message: + 'You are attempting to generate models but have defined no joins. Are you sure you wish to proceed?', + type: ActionAlertType.CAUTION, + actions: [ + { + label: 'Proceed', + type: ActionAlertActionType.PROCEED_WITH_CAUTION, + handler: () => { + flowResult( + editorStore.explorerTreeState.generateModelsFromDatabaseSpecification( + databasePath, + graph, + ), + ).catch(applicationStore.alertUnhandledError); + }, + }, + { + label: 'Abort', + type: ActionAlertActionType.PROCEED, + default: true, + }, + ], + }); + } else { + editorStore.explorerTreeState.generateModelsFromDatabaseSpecification( + databasePath, + graph, + ); + } + } + }); const openSQLPlayground = (): void => { if (isRelationalDatabaseConnection(node?.packageableElement)) { editorStore.panelGroupDisplayState.open(); @@ -812,6 +860,14 @@ const ExplorerContextMenu = observer( )} + {isRelationalDatabase(node.packageableElement) && ( + <> + + Generate Models + + + + )} {extraExplorerContextMenuItems} {Boolean(extraExplorerContextMenuItems.length) && ( diff --git a/packages/legend-application-studio/src/stores/editor/ExplorerTreeState.ts b/packages/legend-application-studio/src/stores/editor/ExplorerTreeState.ts index 4e1340114a..810f8e7fb4 100644 --- a/packages/legend-application-studio/src/stores/editor/ExplorerTreeState.ts +++ b/packages/legend-application-studio/src/stores/editor/ExplorerTreeState.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { action, observable, makeObservable } from 'mobx'; +import { action, observable, makeObservable, flow, flowResult } from 'mobx'; import type { EditorStore } from './EditorStore.js'; import { LogEvent, @@ -23,6 +23,8 @@ import { UnsupportedOperationError, guaranteeNonNullable, ActionState, + type GeneratorFn, + assertErrorThrown, } from '@finos/legend-shared'; import { getDependenciesPackableElementTreeData, @@ -49,9 +51,12 @@ import { isDependencyElement, type Class, type RelationalDatabaseConnection, + type PureModel, } from '@finos/legend-graph'; import { APPLICATION_EVENT } from '@finos/legend-application'; import { DatabaseBuilderWizardState } from './editor-state/element-editor-state/connection/DatabaseBuilderWizardState.js'; +import type { Entity } from '@finos/legend-storage'; +import { EntityChangeType, type EntityChange } from '@finos/legend-server-sdlc'; export enum ExplorerTreeRootPackageLabel { FILE_GENERATION = 'generated-files', @@ -107,6 +112,7 @@ export class ExplorerTreeState { setDatabaseBuilderState: action, onTreeNodeSelect: action, openNode: action, + generateModelsFromDatabaseSpecification: flow, }); this.editorStore = editorStore; @@ -189,6 +195,50 @@ export class ExplorerTreeState { this.setDatabaseBuilderState(dbBuilderState); } + *generateModelsFromDatabaseSpecification( + databasePath: string, + graph: PureModel, + ): GeneratorFn { + try { + const entities = + (yield this.editorStore.graphManagerState.graphManager.generateModelsFromDatabaseSpecification( + databasePath, + graph, + )) as Entity[]; + const newEntities: EntityChange[] = []; + for (const entity of entities) { + let entityChangeType: EntityChangeType; + if (graph.getNullableElement(entity.path) === undefined) { + entityChangeType = EntityChangeType.CREATE; + } else { + entityChangeType = EntityChangeType.MODIFY; + } + newEntities.push({ + type: entityChangeType, + entityPath: entity.path, + content: entity.content, + }); + } + yield flowResult( + this.editorStore.graphState.loadEntityChangesToGraph( + newEntities, + undefined, + ), + ); + this.editorStore.applicationStore.notificationService.notifySuccess( + 'Generated models successfully!', + ); + } catch (error) { + assertErrorThrown(error); + this.editorStore.applicationStore.logService.error( + LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), + error, + ); + this.editorStore.applicationStore.notificationService.notifyError(error); + throw error; + } + } + setSelectedNode(node: PackageTreeNodeData | undefined): void { if (this.selectedNode) { this.selectedNode.isSelected = false; diff --git a/packages/legend-graph/src/graph-manager/AbstractPureGraphManager.ts b/packages/legend-graph/src/graph-manager/AbstractPureGraphManager.ts index aaeeeaf58e..890f220bd0 100644 --- a/packages/legend-graph/src/graph-manager/AbstractPureGraphManager.ts +++ b/packages/legend-graph/src/graph-manager/AbstractPureGraphManager.ts @@ -570,6 +570,13 @@ export abstract class AbstractPureGraphManager { graphData: GraphData, ): Promise; + // --------------------------------------------- Relational --------------------------------------------- + + abstract generateModelsFromDatabaseSpecification( + databasePath: string, + graph: PureModel, + ): Promise; + // ------------------------------------------- Service ------------------------------------------- /** * @modularize diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts index 5e1b8241e2..67571e2040 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts @@ -291,6 +291,7 @@ import { FunctionActivatorConfiguration } from '../../../action/functionActivato import { V1_FunctionActivatorInput } from './engine/functionActivator/V1_FunctionActivatorInput.js'; import { V1_FunctionActivator } from './model/packageableElements/function/V1_FunctionActivator.js'; import { V1_INTERNAL__UnknownFunctionActivator } from './model/packageableElements/function/V1_INTERNAL__UnknownFunctionActivator.js'; +import { V1_DatabaseToModelGenerationInput } from './engine/relational/V1_DatabaseToModelGenerationInput.js'; import type { RelationalDatabaseConnection } from '../../../../STO_Relational_Exports.js'; import { V1_RawSQLExecuteInput } from './engine/execution/V1_RawSQLExecuteInput.js'; import type { SubtypeInfo } from '../../../action/protocol/ProtocolInfo.js'; @@ -2996,6 +2997,21 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { await this.engine.publishFunctionActivatorToSandbox(input); } + // --------------------------------------------- Relational --------------------------------------------- + + async generateModelsFromDatabaseSpecification( + databasePath: string, + graph: PureModel, + ): Promise { + const graphData = this.graphToPureModelContextData(graph); + const input = new V1_DatabaseToModelGenerationInput(); + input.databasePath = databasePath; + input.modelData = graphData; + const generatedModel = + await this.engine.generateModelsFromDatabaseSpecification(input); + return this.pureModelContextDataToEntities(generatedModel); + } + // --------------------------------------------- Service --------------------------------------------- async registerService( diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_Engine.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_Engine.ts index 804a07cea9..9a4d9b00c4 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_Engine.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_Engine.ts @@ -135,6 +135,7 @@ import { V1_ArtifactGenerationExtensionOutput, V1_ArtifactGenerationExtensionInput, } from './generation/V1_ArtifactGenerationExtensionApi.js'; +import { V1_DatabaseToModelGenerationInput } from './relational/V1_DatabaseToModelGenerationInput.js'; class V1_EngineConfig extends TEMPORARY__AbstractEngineConfig { private engine: V1_Engine; @@ -1013,4 +1014,31 @@ export class V1_Engine { ); } } + + // ------------------------------------------- Relational ------------------------------------------- + + async generateModelsFromDatabaseSpecification( + input: V1_DatabaseToModelGenerationInput, + ): Promise { + try { + const json = + await this.engineServerClient.generateModelsFromDatabaseSpecification( + V1_DatabaseToModelGenerationInput.serialization.toJson(input), + ); + return V1_deserializePureModelContextData(json); + } catch (error) { + assertErrorThrown(error); + if ( + error instanceof NetworkClientError && + error.response.status === HttpStatus.BAD_REQUEST + ) { + throw V1_buildParserError( + V1_ParserError.serialization.fromJson( + error.payload as PlainObject, + ), + ); + } + throw error; + } + } } diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_EngineServerClient.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_EngineServerClient.ts index 706685ee83..b52523df69 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_EngineServerClient.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_EngineServerClient.ts @@ -77,11 +77,14 @@ import type { V1_ArtifactGenerationExtensionInput, V1_ArtifactGenerationExtensionOutput, } from './generation/V1_ArtifactGenerationExtensionApi.js'; +import type { V1_DatabaseToModelGenerationInput } from './relational/V1_DatabaseToModelGenerationInput.js'; enum CORE_ENGINE_ACTIVITY_TRACE { GRAMMAR_TO_JSON = 'transform Pure code to protocol', JSON_TO_GRAMMAR = 'transform protocol to Pure code', + DATABASE_TO_MODELS = 'generate models from database', + EXTERNAL_FORMAT_TO_PROTOCOL = 'transform external format code to protocol', GENERATE_FILE = 'generate file', @@ -774,6 +777,20 @@ export class V1_EngineServerClient extends AbstractServerClient { ); } + // ------------------------------------------- Relational --------------------------------------- + + _relationalElement = (): string => `${this._pure()}/relational`; + + generateModelsFromDatabaseSpecification( + input: PlainObject, + ): Promise> { + return this.postWithTracing( + this.getTraceData(CORE_ENGINE_ACTIVITY_TRACE.DATABASE_TO_MODELS), + `${this._relationalElement()}/generateModelsFromDatabaseSpecification`, + this.debugPayload(input, CORE_ENGINE_ACTIVITY_TRACE.DATABASE_TO_MODELS), + ); + } + // ------------------------------------------- Service ------------------------------------------- _service = (serviceServerUrl?: string): string => diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/relational/V1_DatabaseToModelGenerationInput.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/relational/V1_DatabaseToModelGenerationInput.ts new file mode 100644 index 0000000000..32fe8abbfb --- /dev/null +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/relational/V1_DatabaseToModelGenerationInput.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2020-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { primitive, createModelSchema } from 'serializr'; +import { SerializationFactory } from '@finos/legend-shared'; +import type { V1_PureModelContextData } from '../../model/context/V1_PureModelContextData.js'; +import { V1_pureModelContextDataPropSchema } from '../../transformation/pureProtocol/V1_PureProtocolSerialization.js'; + +export class V1_DatabaseToModelGenerationInput { + databasePath!: string; + modelData!: V1_PureModelContextData; + + static readonly serialization = new SerializationFactory( + createModelSchema(V1_DatabaseToModelGenerationInput, { + databasePath: primitive(), + modelData: V1_pureModelContextDataPropSchema, + }), + ); +}