UNPKG

@finos/legend-studio

Version:
422 lines 19 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 { assertErrorThrown, LogEvent, addUniqueEntry, assertNonEmptyString, assertTrue, guaranteeNonNullable, isNonNullable, filterByType, ActionState, } from '@finos/legend-shared'; import { observable, action, makeObservable, flow, flowResult } from 'mobx'; import { LEGEND_STUDIO_APP_EVENT } from '../../../LegendStudioAppEvent.js'; import { DatabaseBuilderInput, DatabasePattern, TargetDatabase, PackageableElementExplicitReference, Column, Database, isValidFullPath, resolvePackagePathAndElementName, isStubbed_PackageableElement, getSchema, getNullableSchema, getNullableTable, } from '@finos/legend-graph'; import { connection_setStore } from '../../../graphModifier/DSLMapping_GraphModifierHelper.js'; export class DatabaseBuilderTreeNodeData { isOpen; id; label; parentId; childrenIds; isChecked = false; constructor(id, label, parentId) { this.id = id; this.label = label; this.parentId = parentId; } } export class SchemaDatabaseBuilderTreeNodeData extends DatabaseBuilderTreeNodeData { schema; constructor(id, parentId, schema) { super(id, schema.name, parentId); this.schema = schema; } } export class TableDatabaseBuilderTreeNodeData extends DatabaseBuilderTreeNodeData { table; constructor(id, parentId, table) { super(id, table.name, parentId); this.table = table; } } export class ColumnDatabaseBuilderTreeNodeData extends DatabaseBuilderTreeNodeData { column; constructor(id, parentId, column) { super(id, column.name, parentId); this.column = column; } } const WILDCARD = '%'; export class DatabaseBuilderState { editorStore; connection; showModal = false; databaseGrammarCode = ''; isBuildingDatabase = false; isSavingDatabase = false; targetDatabasePath; treeData; constructor(editorStore, connection) { makeObservable(this, { showModal: observable, targetDatabasePath: observable, isBuildingDatabase: observable, databaseGrammarCode: observable, isSavingDatabase: observable, setTargetDatabasePath: action, setShowModal: action, setDatabaseGrammarCode: action, setTreeData: action, treeData: observable, onNodeSelect: flow, buildDatabaseGrammar: flow, buildDatabaseFromInput: flow, buildDatabaseWithTreeData: flow, createOrUpdateDatabase: flow, fetchSchemaDefinitions: flow, fetchSchemaMetadata: flow, fetchTableMetadata: flow, }); this.connection = connection; this.editorStore = editorStore; this.targetDatabasePath = this.currentDatabase?.path ?? 'store::MyDatabase'; } setShowModal(val) { this.showModal = val; } setTreeData(builderTreeData) { this.treeData = builderTreeData; } setDatabaseGrammarCode(val) { this.databaseGrammarCode = val; } setTargetDatabasePath(val) { this.targetDatabasePath = val; } *onNodeSelect(node, treeData) { if (node instanceof SchemaDatabaseBuilderTreeNodeData && !node.childrenIds) { yield flowResult(this.fetchSchemaMetadata(node, treeData)); } else if (node instanceof TableDatabaseBuilderTreeNodeData && !node.childrenIds) { yield flowResult(this.fetchTableMetadata(node, treeData)); } node.isOpen = !node.isOpen; this.setTreeData({ ...treeData }); } getChildNodes(node, treeData) { return node.childrenIds ?.map((n) => treeData.nodes.get(n)) .filter(isNonNullable); } toggleCheckedNode(node, treeData) { node.isChecked = !node.isChecked; if (node instanceof SchemaDatabaseBuilderTreeNodeData) { this.getChildNodes(node, treeData)?.forEach((n) => { n.isChecked = node.isChecked; }); } else if (node instanceof TableDatabaseBuilderTreeNodeData) { if (node.parentId) { const parent = treeData.nodes.get(node.parentId); if (parent && this.getChildNodes(parent, treeData)?.every((e) => e.isChecked === node.isChecked)) { parent.isChecked = node.isChecked; } } } // TODO: handle ColumnDatabaseBuilderTreeNodeData this.setTreeData({ ...treeData }); } buildNonEnrichedDbBuilderInput(schema) { const databaseBuilderInput = new DatabaseBuilderInput(this.connection); const [packagePath, databaseName] = this.getDatabasePackageAndName(); databaseBuilderInput.targetDatabase = new TargetDatabase(packagePath, databaseName); databaseBuilderInput.config.maxTables = undefined; databaseBuilderInput.config.enrichTables = Boolean(schema); databaseBuilderInput.config.patterns = [ new DatabasePattern(schema ?? WILDCARD, WILDCARD), ]; return databaseBuilderInput; } *fetchSchemaDefinitions() { try { this.isBuildingDatabase = true; const databaseBuilderInput = this.buildNonEnrichedDbBuilderInput(); const database = (yield flowResult(this.buildDatabaseFromInput(databaseBuilderInput))); const rootIds = []; const nodes = new Map(); database.schemas .slice() .sort((schemaA, schemaB) => schemaA.name.localeCompare(schemaB.name)) .forEach((dbSchema) => { const schemaId = dbSchema.name; rootIds.push(schemaId); const schemaNode = new SchemaDatabaseBuilderTreeNodeData(schemaId, undefined, dbSchema); schemaNode.isChecked = Boolean(this.currentDatabase?.schemas.find((cSchema) => cSchema.name === dbSchema.name)); nodes.set(schemaId, schemaNode); }); const treeData = { rootIds, nodes, database }; this.setTreeData(treeData); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error); this.editorStore.applicationStore.notifyError(error); } finally { this.isBuildingDatabase = false; } } *fetchSchemaMetadata(schemaNode, treeData) { try { this.isBuildingDatabase = true; const schema = schemaNode.schema; const databaseBuilderInput = this.buildNonEnrichedDbBuilderInput(schema.name); const database = (yield flowResult(this.buildDatabaseFromInput(databaseBuilderInput))); const tables = getSchema(database, schema.name).tables; const childrenIds = schemaNode.childrenIds ?? []; schema.tables = tables; tables .slice() .sort((tableA, tableB) => tableA.name.localeCompare(tableB.name)) .forEach((table) => { table.schema = schema; const tableId = `${schema.name}.${table.name}`; const tableNode = new TableDatabaseBuilderTreeNodeData(tableId, schemaNode.id, table); if (this.currentDatabase) { const matchingSchema = getNullableSchema(this.currentDatabase, schema.name); tableNode.isChecked = Boolean(matchingSchema ? getNullableTable(matchingSchema, table.name) : undefined); } else { tableNode.isChecked = false; } treeData.nodes.set(tableId, tableNode); addUniqueEntry(childrenIds, tableId); }); schemaNode.childrenIds = childrenIds; this.setTreeData({ ...treeData }); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error); this.editorStore.applicationStore.notifyError(error); } finally { this.isBuildingDatabase = false; } } *fetchTableMetadata(tableNode, treeData) { try { const databaseBuilderInput = new DatabaseBuilderInput(this.connection); const [packagePath, databaseName] = resolvePackagePathAndElementName(this.targetDatabasePath); databaseBuilderInput.targetDatabase = new TargetDatabase(packagePath, databaseName); const config = databaseBuilderInput.config; config.maxTables = undefined; config.enrichTables = true; config.enrichColumns = true; config.enrichPrimaryKeys = true; const table = tableNode.table; config.patterns = [new DatabasePattern(table.schema.name, table.name)]; const database = (yield flowResult(this.buildDatabaseFromInput(databaseBuilderInput))); const enrichedTable = database.schemas .find((s) => table.schema.name === s.name) ?.tables.find((t) => t.name === table.name); if (enrichedTable) { this.addColumnsNodeToTableNode(tableNode, enrichedTable, treeData); } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error); this.editorStore.applicationStore.notifyError(error); } finally { this.isBuildingDatabase = false; } } addColumnsNodeToTableNode(tableNode, enrichedTable, treeData) { const columns = enrichedTable.columns.filter(filterByType(Column)); tableNode.table.columns = columns; this.removeChildren(tableNode, treeData); const childrenIds = []; const tableId = tableNode.id; columns .slice() .sort((colA, colB) => colA.name.localeCompare(colB.name)) .forEach((c) => { const columnId = `${tableId}.${c.name}`; const columnNode = new ColumnDatabaseBuilderTreeNodeData(columnId, tableId, c); c.owner = tableNode.table; treeData.nodes.set(columnId, columnNode); addUniqueEntry(childrenIds, columnId); }); tableNode.childrenIds = childrenIds; } removeChildren(node, treeData) { const currentChildren = node.childrenIds; if (currentChildren) { currentChildren.forEach((c) => treeData.nodes.delete(c)); node.childrenIds = undefined; } } getDatabasePackageAndName() { if (this.currentDatabase) { return [ guaranteeNonNullable(this.currentDatabase.package).path, this.currentDatabase.name, ]; } assertNonEmptyString(this.targetDatabasePath, 'Must specify database path'); assertTrue(isValidFullPath(this.targetDatabasePath), 'Invalid database path'); return resolvePackagePathAndElementName(this.targetDatabasePath, this.targetDatabasePath); } *buildDatabaseWithTreeData() { try { if (this.treeData) { const dbTreeData = this.treeData; this.isBuildingDatabase = true; const databaseBuilderInput = new DatabaseBuilderInput(this.connection); const [packagePath, databaseName] = this.getDatabasePackageAndName(); databaseBuilderInput.targetDatabase = new TargetDatabase(packagePath, databaseName); const config = databaseBuilderInput.config; config.maxTables = undefined; config.enrichTables = true; config.enrichColumns = true; config.enrichPrimaryKeys = true; dbTreeData.rootIds .map((e) => dbTreeData.nodes.get(e)) .filter(isNonNullable) .forEach((schemaNode) => { if (schemaNode instanceof SchemaDatabaseBuilderTreeNodeData) { const tableNodes = this.getChildNodes(schemaNode, dbTreeData); const allChecked = tableNodes?.every((t) => t.isChecked === true); if (allChecked || (schemaNode.isChecked && !schemaNode.childrenIds)) { config.patterns.push(new DatabasePattern(schemaNode.schema.name, WILDCARD)); } else { tableNodes?.forEach((t) => { if (t instanceof TableDatabaseBuilderTreeNodeData && t.isChecked) { config.patterns.push(new DatabasePattern(schemaNode.schema.name, t.table.name)); } }); } } }); const entities = (yield this.editorStore.graphManagerState.graphManager.buildDatabase(databaseBuilderInput)); const dbGrammar = (yield this.editorStore.graphManagerState.graphManager.entitiesToPureCode(entities)); this.setDatabaseGrammarCode(dbGrammar); } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error); this.editorStore.applicationStore.notifyError(error); } finally { this.isBuildingDatabase = false; } } getSchemasFromTreeNode(tree) { return Array.from(tree.nodes.values()) .map((e) => { if (e instanceof SchemaDatabaseBuilderTreeNodeData) { return e.schema; } return undefined; }) .filter(isNonNullable); } *buildDatabaseGrammar(grammar) { const entities = (yield this.editorStore.graphManagerState.graphManager.pureCodeToEntities(grammar)); const dbGraph = this.editorStore.graphManagerState.createEmptyGraph(); (yield this.editorStore.graphManagerState.graphManager.buildGraph(dbGraph, entities, ActionState.create())); assertTrue(dbGraph.ownDatabases.length === 1, 'Expected one database to be generated from grammar'); return dbGraph.ownDatabases[0]; } *buildDatabaseFromInput(databaseBuilderInput) { const entities = (yield this.editorStore.graphManagerState.graphManager.buildDatabase(databaseBuilderInput)); const dbGraph = this.editorStore.graphManagerState.createEmptyGraph(); (yield this.editorStore.graphManagerState.graphManager.buildGraph(dbGraph, entities, ActionState.create())); assertTrue(dbGraph.ownDatabases.length === 1, 'Expected one database to be generated from input'); return dbGraph.ownDatabases[0]; } *createOrUpdateDatabase() { try { this.isSavingDatabase = true; assertNonEmptyString(this.databaseGrammarCode, 'Database grammar is empty'); const database = (yield flowResult(this.buildDatabaseGrammar(this.databaseGrammarCode))); let currentDatabase; const isUpdating = Boolean(this.currentDatabase); if (!this.currentDatabase) { const newDatabase = new Database(database.name); connection_setStore(this.connection, PackageableElementExplicitReference.create(newDatabase)); const packagePath = guaranteeNonNullable(database.package?.name, 'Database package is missing'); yield flowResult(this.editorStore.addElement(newDatabase, packagePath, false)); currentDatabase = newDatabase; } else { currentDatabase = this.currentDatabase; } if (this.treeData) { const schemas = this.getSchemasFromTreeNode(this.treeData); this.updateDatabase(currentDatabase, database, schemas); this.editorStore.applicationStore.notifySuccess(`Database successfully '${isUpdating ? 'updated' : 'created'}. ${!isUpdating ? 'Recompiling...' : ''}`); this.fetchSchemaDefinitions(); if (isUpdating) { yield flowResult(this.editorStore.graphState.globalCompileInFormMode({ message: `Can't compile graph after editing database. Redirecting you to text mode`, })); } } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE), error); this.editorStore.applicationStore.notifyError(error); } finally { this.isSavingDatabase = false; } } updateDatabase(current, generatedDb, allSchemas) { // remove shemas not defined current.schemas = current.schemas.filter((schema) => { if (allSchemas.find((c) => c.name === schema.name) && !generatedDb.schemas.find((c) => c.name === schema.name)) { return false; } return true; }); // update existing schemas generatedDb.schemas.forEach((schema) => { schema._OWNER = current; const currentSchemaIndex = current.schemas.findIndex((c) => c.name === schema.name); if (currentSchemaIndex !== -1) { current.schemas[currentSchemaIndex] = schema; } else { current.schemas.push(schema); } }); } get currentDatabase() { const store = this.connection.store.value; if (store instanceof Database && !isStubbed_PackageableElement(store)) { return store; } return undefined; } } //# sourceMappingURL=DatabaseBuilderState.js.map