UNPKG

@finos/legend-application-studio

Version:
923 lines (871 loc) 29 kB
/** * 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 type { Entity } from '@finos/legend-storage'; import type { TreeData, TreeNodeData } from '@finos/legend-art'; import { type GeneratorFn, type Writable, assertErrorThrown, LogEvent, addUniqueEntry, guaranteeNonNullable, isNonNullable, filterByType, ActionState, at, guaranteeType, assertNonEmptyString, assertTrue, UnsupportedOperationError, } from '@finos/legend-shared'; import { observable, action, makeObservable, flow, flowResult, computed, } from 'mobx'; import { LEGEND_STUDIO_APP_EVENT } from '../../../../../__lib__/LegendStudioEvent.js'; import type { EditorStore } from '../../../EditorStore.js'; import { type RawLambda, type PureModel, type Runtime, type ExecutionResultWithMetadata, TDSExecutionResult, getColumn, PrimitiveType, PRIMITIVE_TYPE, TDSRow, type Schema, Table, RelationalDatabaseConnection, DatabaseBuilderInput, DatabasePattern, TargetDatabase, Column, Database, resolvePackagePathAndElementName, getSchema, getNullableSchema, getNullableTable, isStubbed_PackageableElement, isValidFullPath, PackageableElementExplicitReference, getTable, Mapping, EngineRuntime, StoreConnections, IdentifiedConnection, getOrCreateGraphPackage, extractElementNameFromPath, extractPackagePathFromPath, } from '@finos/legend-graph'; import { GraphEditFormModeState } from '../../../GraphEditFormModeState.js'; import { connection_setStore } from '../../../../graph-modifier/DSL_Mapping_GraphModifierHelper.js'; import { getTDSColumnDerivedProperyFromType } from '@finos/legend-query-builder'; import { getPrimitiveTypeFromRelationalType } from '../../../utils/MockDataUtils.js'; const GENERATED_PACKAGE = 'generated'; const TDS_LIMIT = 1000; const buildTableToTDSQueryGrammar = (table: Table): string => { const tableName = table.name; const schemaName = table.schema.name; const db = table.schema._OWNER.path; return `|${db}->tableReference( '${schemaName}', '${tableName}' )->tableToTDS()->take(${TDS_LIMIT})`; }; const buildTableToTDSQueryNonNumericWithColumnGrammar = ( column: Column, ): string => { const table = guaranteeType(column.owner, Table); const tableName = table.name; const colName = column.name; const schemaName = table.schema.name; const db = table.schema._OWNER.path; const PREVIEW_COLUMN_NAME = 'Count Value'; const columnGetter = getTDSColumnDerivedProperyFromType( getPrimitiveTypeFromRelationalType(column.type) ?? PrimitiveType.STRING, ); return `|${db}->tableReference( '${schemaName}', '${tableName}' )->tableToTDS()->restrict( ['${colName}'] )->groupBy( ['${colName}'], '${PREVIEW_COLUMN_NAME}'->agg( row|$row.${columnGetter}('${colName}'), y|$y->count() ) )->sort( [ desc('${colName}'), asc('${PREVIEW_COLUMN_NAME}') ] )->take(${TDS_LIMIT})`; }; const buildTableToTDSQueryNumericWithColumnGrammar = ( column: Column, ): string => { const table = guaranteeType(column.owner, Table); const tableName = table.name; const colName = column.name; const schemaName = table.schema.name; const db = table.schema._OWNER.path; const columnGetter = getTDSColumnDerivedProperyFromType( getPrimitiveTypeFromRelationalType(column.type) ?? PrimitiveType.STRING, ); return `|${db}->tableReference( '${schemaName}', '${tableName}' )->tableToTDS()->restrict( ['${colName}'] )->groupBy( [], [ 'Count'->agg( row|$row.${columnGetter}('${colName}'), x|$x->count() ), 'Distinct Count'->agg( row|$row.${columnGetter}('${colName}'), x|$x->distinct()->count() ), 'Sum'->agg( row|$row.${columnGetter}('${colName}'), x|$x->sum() ), 'Min'->agg( row|$row.${columnGetter}('${colName}'), x|$x->min() ), 'Max'->agg( row|$row.${columnGetter}('${colName}'), x|$x->max() ), 'Average'->agg( row|$row.${columnGetter}('${colName}'), x|$x->average() ), 'Std Dev (Population)'->agg( row|$row.${columnGetter}('${colName}'), x|$x->stdDevPopulation() ), 'Std Dev (Sample)'->agg( row|$row.${columnGetter}('${colName}'), x|$x->stdDevSample() ) ] )`; }; const buildTableToTDSQueryColumnQuery = (column: Column): [string, boolean] => { const type = getPrimitiveTypeFromRelationalType(column.type) ?? PrimitiveType.STRING; const numerics = [ PRIMITIVE_TYPE.NUMBER, PRIMITIVE_TYPE.INTEGER, PRIMITIVE_TYPE.DECIMAL, PRIMITIVE_TYPE.FLOAT, ]; if (numerics.includes(type.path as PRIMITIVE_TYPE)) { return [buildTableToTDSQueryNumericWithColumnGrammar(column), true]; } return [buildTableToTDSQueryNonNumericWithColumnGrammar(column), false]; }; // 1. mapping // 2. connection // 3. runtime const buildTDSModel = ( graph: PureModel, connection: RelationalDatabaseConnection, db: Database, ): { mapping: Mapping; runtime: Runtime; } => { // mapping const mappingName = 'EmptyMapping'; const _mapping = new Mapping(mappingName); graph.addElement(_mapping, GENERATED_PACKAGE); const engineRuntime = new EngineRuntime(); engineRuntime.mappings = [ PackageableElementExplicitReference.create(_mapping), ]; const _storeConnection = new StoreConnections( PackageableElementExplicitReference.create(db), ); // copy over new connection const newconnection = new RelationalDatabaseConnection( PackageableElementExplicitReference.create(db), connection.type, connection.datasourceSpecification, connection.authenticationStrategy, ); newconnection.localMode = connection.localMode; newconnection.timeZone = connection.timeZone; _storeConnection.storeConnections = [ new IdentifiedConnection('connection1', newconnection), ]; engineRuntime.connections = [_storeConnection]; return { runtime: engineRuntime, mapping: _mapping, }; }; export abstract class DatabaseSchemaExplorerTreeNodeData implements TreeNodeData { isOpen?: boolean | undefined; id: string; label: string; parentId?: string | undefined; childrenIds?: string[] | undefined; isChecked = false; constructor(id: string, label: string, parentId: string | undefined) { makeObservable(this, { isChecked: observable, setChecked: action, }); this.id = id; this.label = label; this.parentId = parentId; } setChecked(val: boolean): void { this.isChecked = val; } } export class DatabaseSchemaExplorerTreeSchemaNodeData extends DatabaseSchemaExplorerTreeNodeData { schema: Schema; constructor(id: string, schema: Schema) { super(id, schema.name, undefined); this.schema = schema; } } export class DatabaseSchemaExplorerTreeTableNodeData extends DatabaseSchemaExplorerTreeNodeData { override parentId: string; owner: Schema; table: Table; constructor(id: string, parentId: string, owner: Schema, table: Table) { super(id, table.name, parentId); this.parentId = parentId; this.owner = owner; this.table = table; } } export class DatabaseSchemaExplorerTreeColumnNodeData extends DatabaseSchemaExplorerTreeNodeData { override parentId: string; owner: Table; column: Column; constructor(id: string, parentId: string, owner: Table, column: Column) { super(id, column.name, parentId); this.parentId = parentId; this.owner = owner; this.column = column; } } export interface DatabaseExplorerTreeData extends TreeData<DatabaseSchemaExplorerTreeNodeData> { database: Database; } export const DEFAULT_DATABASE_PATH = 'store::MyDatabase'; export class DatabaseSchemaExplorerState { readonly editorStore: EditorStore; readonly connection: RelationalDatabaseConnection; database: Database; targetDatabasePath: string; makeTargetDatabasePathEditable?: boolean; isGeneratingDatabase = false; isUpdatingDatabase = false; treeData?: DatabaseExplorerTreeData | undefined; previewer: TDSExecutionResult | undefined; previewDataState = ActionState.create(); constructor( editorStore: EditorStore, connection: RelationalDatabaseConnection, ) { makeObservable(this, { isGeneratingDatabase: observable, isUpdatingDatabase: observable, database: observable, treeData: observable, targetDatabasePath: observable, previewer: observable, previewDataState: observable, makeTargetDatabasePathEditable: observable, isCreatingNewDatabase: computed, resolveDatabasePackageAndName: computed, setTreeData: action, setTargetDatabasePath: action, setMakeTargetDatabasePathEditable: action, onNodeSelect: flow, fetchDatabaseMetadata: flow, fetchSchemaMetadata: flow, fetchTableMetadata: flow, generateDatabase: flow, updateDatabase: flow, updateDatabaseAndGraph: flow, previewData: flow, }); this.connection = connection; this.database = guaranteeType(connection.store.value, Database); this.editorStore = editorStore; this.targetDatabasePath = DEFAULT_DATABASE_PATH; } get isCreatingNewDatabase(): boolean { return isStubbed_PackageableElement(this.connection.store.value); } setMakeTargetDatabasePathEditable(val: boolean): void { this.makeTargetDatabasePathEditable = val; } get resolveDatabasePackageAndName(): [string, string] { if (!this.isCreatingNewDatabase && !this.makeTargetDatabasePathEditable) { return [ guaranteeNonNullable(this.database.package).path, this.database.name, ]; } assertNonEmptyString(this.targetDatabasePath, 'Must specify database path'); assertTrue( isValidFullPath(this.targetDatabasePath), 'Invalid database path', ); return resolvePackagePathAndElementName( this.targetDatabasePath, this.targetDatabasePath, ); } setTargetDatabasePath(val: string): void { this.targetDatabasePath = val; } setTreeData(builderTreeData?: DatabaseExplorerTreeData): void { this.treeData = builderTreeData; } *onNodeSelect( node: DatabaseSchemaExplorerTreeNodeData, treeData: DatabaseExplorerTreeData, ): GeneratorFn<void> { if ( node instanceof DatabaseSchemaExplorerTreeSchemaNodeData && !node.childrenIds ) { yield flowResult(this.fetchSchemaMetadata(node, treeData)); } else if ( node instanceof DatabaseSchemaExplorerTreeTableNodeData && !node.childrenIds ) { yield flowResult(this.fetchTableMetadata(node, treeData)); } node.isOpen = !node.isOpen; this.setTreeData({ ...treeData }); } getChildNodes( node: DatabaseSchemaExplorerTreeNodeData, treeData: DatabaseExplorerTreeData, ): DatabaseSchemaExplorerTreeNodeData[] | undefined { return node.childrenIds ?.map((childNode) => treeData.nodes.get(childNode)) .filter(isNonNullable); } toggleCheckedNode( node: DatabaseSchemaExplorerTreeNodeData, treeData: DatabaseExplorerTreeData, ): void { node.setChecked(!node.isChecked); if (node instanceof DatabaseSchemaExplorerTreeSchemaNodeData) { this.getChildNodes(node, treeData)?.forEach((childNode) => { childNode.setChecked(node.isChecked); }); } else if (node instanceof DatabaseSchemaExplorerTreeTableNodeData) { if (node.parentId) { const parent = treeData.nodes.get(node.parentId); if ( parent && this.getChildNodes(parent, treeData)?.every( (e) => e.isChecked === node.isChecked, ) ) { parent.setChecked(node.isChecked); } } } // TODO: support toggling check for columns this.setTreeData({ ...treeData }); } *fetchDatabaseMetadata(): GeneratorFn<void> { try { this.isGeneratingDatabase = true; const databaseBuilderInput = new DatabaseBuilderInput(this.connection); const [packagePath, name] = this.resolveDatabasePackageAndName; databaseBuilderInput.targetDatabase = new TargetDatabase( packagePath, name, ); databaseBuilderInput.config.maxTables = undefined; databaseBuilderInput.config.enrichTables = false; databaseBuilderInput.config.patterns = [ new DatabasePattern(undefined, undefined), ]; const database = (yield this.buildIntermediateDatabase( databaseBuilderInput, )) as Database; const rootIds: string[] = []; const nodes = new Map<string, DatabaseSchemaExplorerTreeNodeData>(); database.schemas .toSorted((schemaA, schemaB) => schemaA.name.localeCompare(schemaB.name), ) .forEach((schema) => { const schemaId = schema.name; rootIds.push(schemaId); const schemaNode = new DatabaseSchemaExplorerTreeSchemaNodeData( schemaId, schema, ); nodes.set(schemaId, schemaNode); schemaNode.setChecked( Boolean( this.database.schemas.find( (cSchema) => cSchema.name === schema.name, ), ), ); }); const treeData = { rootIds, nodes, database }; this.setTreeData(treeData); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error( LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error, ); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isGeneratingDatabase = false; } } *fetchSchemaMetadata( schemaNode: DatabaseSchemaExplorerTreeSchemaNodeData, treeData: DatabaseExplorerTreeData, ): GeneratorFn<void> { try { this.isGeneratingDatabase = true; const schema = schemaNode.schema; const databaseBuilderInput = new DatabaseBuilderInput(this.connection); const [packagePath, name] = this.resolveDatabasePackageAndName; databaseBuilderInput.targetDatabase = new TargetDatabase( packagePath, name, ); databaseBuilderInput.config.maxTables = undefined; databaseBuilderInput.config.enrichTables = true; databaseBuilderInput.config.patterns = [ new DatabasePattern(schema.name, undefined), ]; const database = (yield this.buildIntermediateDatabase( databaseBuilderInput, )) as Database; const tables = getSchema(database, schema.name).tables; const childrenIds = schemaNode.childrenIds ?? []; schema.tables = tables; tables .toSorted((tableA, tableB) => tableA.name.localeCompare(tableB.name)) .forEach((table) => { table.schema = schema; const tableId = `${schema.name}.${table.name}`; const tableNode = new DatabaseSchemaExplorerTreeTableNodeData( tableId, schemaNode.id, schema, table, ); treeData.nodes.set(tableId, tableNode); addUniqueEntry(childrenIds, tableId); const matchingSchema = getNullableSchema(this.database, schema.name); tableNode.setChecked( Boolean( matchingSchema ? getNullableTable(matchingSchema, table.name) : undefined, ), ); }); schemaNode.childrenIds = childrenIds; this.setTreeData({ ...treeData }); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error( LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error, ); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isGeneratingDatabase = false; } } *fetchTableMetadata( tableNode: DatabaseSchemaExplorerTreeTableNodeData, treeData: DatabaseExplorerTreeData, ): GeneratorFn<void> { try { this.isGeneratingDatabase = true; const databaseBuilderInput = new DatabaseBuilderInput(this.connection); const [packagePath, name] = this.resolveDatabasePackageAndName; databaseBuilderInput.targetDatabase = new TargetDatabase( packagePath, name, ); const table = tableNode.table; const config = databaseBuilderInput.config; config.maxTables = undefined; config.enrichTables = true; config.enrichColumns = true; config.enrichPrimaryKeys = true; config.patterns = [new DatabasePattern(table.schema.name, table.name)]; const database = (yield this.buildIntermediateDatabase( databaseBuilderInput, )) as Database; const enrichedTable = database.schemas .find((s) => table.schema.name === s.name) ?.tables.find((t) => t.name === table.name); if (enrichedTable) { table.primaryKey = enrichedTable.primaryKey; const columns = enrichedTable.columns.filter(filterByType(Column)); tableNode.table.columns = columns; tableNode.childrenIds?.forEach((childId) => treeData.nodes.delete(childId), ); tableNode.childrenIds = undefined; const childrenIds: string[] = []; const tableId = tableNode.id; columns .toSorted((colA, colB) => colA.name.localeCompare(colB.name)) .forEach((column) => { const columnId = `${tableId}.${column.name}`; const columnNode = new DatabaseSchemaExplorerTreeColumnNodeData( columnId, tableId, table, column, ); column.owner = tableNode.table; treeData.nodes.set(columnId, columnNode); addUniqueEntry(childrenIds, columnId); }); tableNode.childrenIds = childrenIds; } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error( LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error, ); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isGeneratingDatabase = false; } } private async buildIntermediateDatabase( databaseBuilderInput: DatabaseBuilderInput, ): Promise<Database> { const entities = await this.editorStore.graphManagerState.graphManager.buildDatabase( databaseBuilderInput, ); const graph = this.editorStore.graphManagerState.createNewGraph(); await this.editorStore.graphManagerState.graphManager.buildGraph( graph, entities, ActionState.create(), ); return at( graph.ownDatabases, 0, 'Expected one database to be generated from input', ); } *previewData(node: DatabaseSchemaExplorerTreeNodeData): GeneratorFn<void> { try { this.previewer = undefined; this.previewDataState.inProgress(); let column: Column | undefined; let table: Table | undefined; if (node instanceof DatabaseSchemaExplorerTreeTableNodeData) { table = node.table; } else if (node instanceof DatabaseSchemaExplorerTreeColumnNodeData) { table = guaranteeType(node.column.owner, Table); column = node.column; } else { throw new UnsupportedOperationError( 'Preview data only supported for column and table', ); } const schemaName = table.schema.name; const tableName = table.name; const dummyPackage = 'generation'; const dummyName = 'myDB'; const dummyDbPath = `${dummyPackage}::${dummyName}`; const databaseBuilderInput = new DatabaseBuilderInput(this.connection); databaseBuilderInput.targetDatabase = new TargetDatabase( dummyPackage, dummyName, ); const config = databaseBuilderInput.config; config.maxTables = undefined; config.enrichTables = true; config.enrichColumns = true; config.enrichPrimaryKeys = true; config.patterns.push(new DatabasePattern(table.schema.name, table.name)); const entities = (yield this.editorStore.graphManagerState.graphManager.buildDatabase( databaseBuilderInput, )) as Entity[]; assertTrue(entities.length === 1); const dbEntity = guaranteeNonNullable(entities[0]); const emptyGraph = this.editorStore.graphManagerState.createNewGraph(); (yield this.editorStore.graphManagerState.graphManager.buildGraph( emptyGraph, [dbEntity], ActionState.create(), )) as Entity[]; const generatedDb = emptyGraph.getDatabase(dummyDbPath); const resolvedTable = getTable( getSchema(generatedDb, schemaName), tableName, ); let queryGrammar: string; let resolveResult = false; if (column) { const resolvedColumn = getColumn(resolvedTable, column.name); const grammarResult = buildTableToTDSQueryColumnQuery(resolvedColumn); queryGrammar = grammarResult[0]; resolveResult = grammarResult[1]; } else { queryGrammar = buildTableToTDSQueryGrammar(resolvedTable); } const rawLambda = (yield this.editorStore.graphManagerState.graphManager.pureCodeToLambda( queryGrammar, 'QUERY', )) as RawLambda; const { mapping, runtime } = buildTDSModel( emptyGraph, this.connection, generatedDb, ); const execPlan = ( (yield this.editorStore.graphManagerState.graphManager.runQuery( rawLambda, mapping, runtime, emptyGraph, )) as ExecutionResultWithMetadata ).executionResult; let tdsResult = guaranteeType( execPlan, TDSExecutionResult, 'Execution from `tabletoTDS` expected to be TDS', ); if (resolveResult) { const newResult = new TDSExecutionResult(); newResult.result.columns = ['Aggregation', 'Value']; newResult.result.rows = tdsResult.result.columns.map((col, idx) => { const _row = new TDSRow(); _row.values = [ col, guaranteeNonNullable( guaranteeNonNullable(tdsResult.result.rows[0]).values[idx], ), ]; return _row; }); tdsResult = newResult; } this.previewer = tdsResult; } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.notificationService.notifyError( `Unable to preview data: ${error.message}`, ); } finally { this.previewDataState.complete(); } } *generateDatabase(): GeneratorFn<Entity> { try { this.isGeneratingDatabase = true; const treeData = guaranteeNonNullable(this.treeData); const databaseBuilderInput = new DatabaseBuilderInput(this.connection); const [packagePath, name] = this.resolveDatabasePackageAndName; databaseBuilderInput.targetDatabase = new TargetDatabase( packagePath, name, ); const config = databaseBuilderInput.config; config.maxTables = undefined; config.enrichTables = true; config.enrichColumns = true; config.enrichPrimaryKeys = true; treeData.rootIds .map((e) => treeData.nodes.get(e)) .filter(isNonNullable) .forEach((schemaNode) => { if (schemaNode instanceof DatabaseSchemaExplorerTreeSchemaNodeData) { const tableNodes = this.getChildNodes(schemaNode, treeData); const allChecked = tableNodes?.every((t) => t.isChecked === true); if ( allChecked || (schemaNode.isChecked && !schemaNode.childrenIds) ) { config.patterns.push( new DatabasePattern(schemaNode.schema.name, undefined), ); } else { tableNodes?.forEach((t) => { if ( t instanceof DatabaseSchemaExplorerTreeTableNodeData && t.isChecked ) { config.patterns.push( new DatabasePattern(schemaNode.schema.name, t.table.name), ); } }); } } }); const entities = (yield this.editorStore.graphManagerState.graphManager.buildDatabase( databaseBuilderInput, )) as Entity[]; return at(entities, 0, 'Expected a database to be generated'); } finally { this.isGeneratingDatabase = false; } } // this method just updates database *updateDatabase(forceRename?: boolean): GeneratorFn<Database> { this.isUpdatingDatabase = true; const graph = this.editorStore.graphManagerState.createNewGraph(); (yield this.editorStore.graphManagerState.graphManager.buildGraph( graph, [(yield flowResult(this.generateDatabase())) as Entity], ActionState.create(), )) as Entity[]; const database = at( graph.ownDatabases, 0, 'Expected one database to be generated from input', ); // remove undefined schemas const schemas = Array.from( guaranteeNonNullable(this.treeData).nodes.values(), ) .map((schemaNode) => { if (schemaNode instanceof DatabaseSchemaExplorerTreeSchemaNodeData) { return schemaNode.schema; } return undefined; }) .filter(isNonNullable); // update this.database packge and name if (forceRename || this.database.name === '' || !this.database.package) { this.database.package = getOrCreateGraphPackage( graph, extractPackagePathFromPath(this.targetDatabasePath), undefined, ); this.database.name = extractElementNameFromPath(this.targetDatabasePath); } // update schemas this.database.schemas = this.database.schemas.filter((schema) => { if ( schemas.find((item) => item.name === schema.name) && !database.schemas.find((s) => s.name === schema.name) ) { return false; } return true; }); // update existing schemas database.schemas.forEach((schema) => { (schema as Writable<Schema>)._OWNER = this.database; const currentSchemaIndex = this.database.schemas.findIndex( (item) => item.name === schema.name, ); if (currentSchemaIndex !== -1) { this.database.schemas[currentSchemaIndex] = schema; } else { this.database.schemas.push(schema); } }); this.isUpdatingDatabase = false; return database; } // this method updates database and add database to the graph *updateDatabaseAndGraph(): GeneratorFn<void> { if (!this.treeData) { return; } try { const createDatabase = this.isCreatingNewDatabase && !this.editorStore.graphManagerState.graph.databases.includes( this.database, ); this.isUpdatingDatabase = true; const database = (yield flowResult(this.updateDatabase())) as Database; if (createDatabase) { connection_setStore( this.connection, PackageableElementExplicitReference.create(database), ); const packagePath = guaranteeNonNullable( database.package?.name, 'Database package is missing', ); yield flowResult( this.editorStore.graphEditorMode.addElement( database, packagePath, false, ), ); } this.editorStore.applicationStore.notificationService.notifySuccess( `Database successfully updated`, ); yield flowResult( this.editorStore .getGraphEditorMode(GraphEditFormModeState) .globalCompile({ message: `Can't compile graph after editing database. Redirecting you to text mode`, }), ); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error( LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error, ); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isUpdatingDatabase = false; } } }