@finos/legend-studio
Version:
422 lines • 19 kB
JavaScript
/**
* 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