UNPKG

@finos/legend-extension-dsl-data-quality

Version:
604 lines (576 loc) 19.3 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 TreeData, type TreeNodeData } from '@finos/legend-art'; import { type Hashable, addUniqueEntry, assertErrorThrown, assertType, deleteEntry, hashArray, LogEvent, UnsupportedOperationError, } from '@finos/legend-shared'; import { action, computed, makeObservable, observable } from 'mobx'; import { type AbstractProperty, type Type, type Constraint, type GraphFetchTree, type RawLambda, Class, GRAPH_MANAGER_EVENT, PackageableElementExplicitReference, PropertyExplicitReference, getAllClassConstraints, isStubbed_RawLambda, TYPE_CAST_TOKEN, } from '@finos/legend-graph'; import { type QueryBuilderExplorerTreeNodeData, graphFetchTree_addSubTree, graphFetchTree_removeSubTree, type QueryBuilderExplorerTreeRootNodeData, QueryBuilderExplorerTreePropertyNodeData, QueryBuilderExplorerTreeSubTypeNodeData, } from '@finos/legend-query-builder'; import type { DataQualityState } from '../states/DataQualityState.js'; import { DataQualityPropertyGraphFetchTree, DataQualityRootGraphFetchTree, } from '../../graph/metamodel/pure/packageableElements/data-quality/DataQualityGraphFetchTree.js'; import { ConstraintState } from '../states/ConstraintState.js'; import type { EditorStore } from '@finos/legend-application-studio'; import { DATA_QUALITY_HASH_STRUCTURE } from '../../graph/metamodel/DSL_DataQuality_HashUtils.js'; import { dataQualityGraphFetchTree_removeConstraints, graphFetchTree_removeAllSubTrees, } from '../../graph-manager/DSL_DataQuality_GraphModifierHelper.js'; export class DataQualityGraphFetchTreeNodeData implements TreeNodeData, Hashable { readonly id: string; readonly label: string; readonly tree: | DataQualityPropertyGraphFetchTree | DataQualityRootGraphFetchTree; readonly parentId?: string | undefined; readonly editorStore: EditorStore; isOpen?: boolean | undefined; isReadOnly?: boolean; childrenIds: string[] = []; constraints: ConstraintState[] = []; constructor( editorStore: EditorStore, id: string, label: string, parentId: string | undefined, graphFetchTreeNode: | DataQualityPropertyGraphFetchTree | DataQualityRootGraphFetchTree, ) { makeObservable(this, { hashCode: computed, constraints: observable, isReadOnly: observable, setConstraintsForClass: action, setConstraints: action, setIsReadOnly: action, }); this.id = id; this.label = label; this.parentId = parentId; this.tree = graphFetchTreeNode; // NOTE: in this tree, every node is open this.isOpen = true; this.editorStore = editorStore; } setIsReadOnly(isReadOnly: boolean) { this.isReadOnly = isReadOnly; } setConstraintsForClass(_class: Class, constraintsToSelect: string[]) { const constraints = getAllClassConstraints(_class).map( (constraint) => new ConstraintState(constraint), ); const lambdas = new Map<string, RawLambda>(); const index = new Map<string, ConstraintState>(); constraints.forEach((constraintState) => { if (!isStubbed_RawLambda(constraintState.constraint.functionDefinition)) { lambdas.set( constraintState.lambdaId, constraintState.constraint.functionDefinition, ); index.set(constraintState.lambdaId, constraintState); if ( constraintsToSelect.find( (constraintName) => constraintState.constraint.name === constraintName, ) ) { constraintState.isSelected = true; } } }); if (lambdas.size) { this.editorStore.graphManagerState.graphManager .lambdasToPureCode(lambdas) .then((res: Map<string, string>) => { res.forEach((grammarText: string, key: string) => { const constraintState = index.get(key); constraintState?.setLambdaString( constraintState.extractLambdaString(grammarText), ); }); this.setConstraints(constraints); }) .catch((error) => { assertErrorThrown(error); this.editorStore.applicationStore.logService.error( LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), error, ); }); } } setConstraints(constraints: ConstraintState[]) { this.constraints = constraints; } get type(): Type { if (this.tree instanceof DataQualityPropertyGraphFetchTree) { return this.tree.property.value.genericType.value.rawType; } if (this.tree instanceof DataQualityRootGraphFetchTree) { return this.tree.class.value; } throw new UnsupportedOperationError( `Can't get type of Graph Fetch Tree`, this.tree, ); } get hashCode(): string { return hashArray([ DATA_QUALITY_HASH_STRUCTURE.DATA_QUALITY_GRAPH_FETCH_TREE_NODE_DATA, this.id, this.label, this.tree, this.parentId ?? '', hashArray(this.childrenIds), ]); } } export interface DataQualityGraphFetchTreeData extends TreeData<DataQualityGraphFetchTreeNodeData> { tree: DataQualityRootGraphFetchTree; } export const generateRootGraphFetchTreeNodeID = ( parentNodeId: string | undefined, classValue: string | undefined, ): string => `${parentNodeId ? `${parentNodeId}.` : ''}${ classValue ? `${TYPE_CAST_TOKEN}${classValue}` : '' }`; const getPrunableNodes = ( treeData: DataQualityGraphFetchTreeData, ): DataQualityGraphFetchTreeNodeData[] => Array.from(treeData.nodes.values()).filter( (node) => (node.type instanceof Class && node.childrenIds.length === 0 && node.constraints.length === 0) || // orphan node (node.tree instanceof DataQualityPropertyGraphFetchTree && node.parentId && !treeData.nodes.has(node.parentId)), ); const removeNode = ( treeData: DataQualityGraphFetchTreeData, node: DataQualityGraphFetchTreeNodeData, ): void => { const parentNode = node.parentId ? treeData.nodes.get(node.parentId) : undefined; if (parentNode) { deleteEntry(parentNode.childrenIds, node.id); graphFetchTree_removeSubTree(parentNode.tree, node.tree); } else { if (node.tree instanceof DataQualityRootGraphFetchTree) { deleteEntry(treeData.rootIds, node.id); dataQualityGraphFetchTree_removeConstraints(node.tree); graphFetchTree_removeAllSubTrees(node.tree); } graphFetchTree_removeSubTree(treeData.tree, node.tree); } treeData.nodes.delete(node.id); }; const pruneTreeData = (treeData: DataQualityGraphFetchTreeData): void => { let prunableNodes = getPrunableNodes(treeData); while (prunableNodes.length) { prunableNodes.forEach((node) => { removeNode(treeData, node); }); prunableNodes = getPrunableNodes(treeData); } }; export const removeNodeRecursively = ( treeData: DataQualityGraphFetchTreeData, node: DataQualityGraphFetchTreeNodeData, ): void => { removeNode(treeData, node); pruneTreeData(treeData); }; export const generateGraphFetchTreeNodeID = ( property: AbstractProperty, parentNodeId: string | undefined, subType: Type | undefined, ): string => `${parentNodeId ? `${parentNodeId}.` : ''}${property.name}${ subType ? `${TYPE_CAST_TOKEN}${subType.path}` : '' }`; const buildGraphFetchSubTree = ( editorStore: EditorStore, tree: GraphFetchTree, parentNode: DataQualityGraphFetchTreeNodeData | undefined, nodes: Map<string, DataQualityGraphFetchTreeNodeData>, fetchConstraints: boolean, isReadOnly: boolean, ): DataQualityGraphFetchTreeNodeData => { assertType( tree, DataQualityPropertyGraphFetchTree, 'Graph fetch sub-tree must be a property graph fetch tree', ); const property = tree.property.value; const subType = tree.subType?.value; const parentNodeId = parentNode?.id; const node = new DataQualityGraphFetchTreeNodeData( editorStore, generateGraphFetchTreeNodeID(property, parentNodeId, subType), property.name, parentNodeId, tree, ); node.setIsReadOnly(isReadOnly); if (subType) { node.setConstraintsForClass(subType, tree.constraints); } else if (node.type instanceof Class && fetchConstraints && !isReadOnly) { node.setConstraintsForClass(node.type, tree.constraints); } tree.subTrees.forEach((subTree) => { const subTreeNode = buildGraphFetchSubTree( editorStore, subTree, node, nodes, fetchConstraints, isReadOnly, ); addUniqueEntry(node.childrenIds, subTreeNode.id); nodes.set(subTreeNode.id, subTreeNode); }); return node; }; const buildRootGraphFetchSubTree = ( editorStore: EditorStore, tree: DataQualityRootGraphFetchTree, parentNode: DataQualityGraphFetchTreeNodeData | undefined, nodes: Map<string, DataQualityGraphFetchTreeNodeData>, fetchConstraints: boolean, isReadonly: boolean, ): DataQualityGraphFetchTreeNodeData => { const parentNodeId = parentNode?.id; const node = new DataQualityGraphFetchTreeNodeData( editorStore, generateRootGraphFetchTreeNodeID( parentNodeId, tree.class.valueForSerialization ?? '', ), tree.class.value.name, parentNodeId, tree, ); node.setIsReadOnly(isReadonly); if (node.type instanceof Class && fetchConstraints && !isReadonly) { node.setConstraintsForClass(node.type, tree.constraints); } tree.subTrees.forEach((subTree) => { const subTreeNode = buildGraphFetchSubTree( editorStore, subTree, node, nodes, fetchConstraints, isReadonly, ); addUniqueEntry(node.childrenIds, subTreeNode.id); nodes.set(subTreeNode.id, subTreeNode); }); return node; }; export const buildGraphFetchTreeData = ( editorStore: EditorStore, tree: DataQualityRootGraphFetchTree, displayRoot: boolean, fetchConstraints: boolean, isReadOnly: boolean, ): DataQualityGraphFetchTreeData => { const rootIds: string[] = []; const nodes = new Map<string, DataQualityGraphFetchTreeNodeData>(); if (displayRoot) { const rootNode = buildRootGraphFetchSubTree( editorStore, tree, undefined, nodes, fetchConstraints, isReadOnly, ); addUniqueEntry(rootIds, rootNode.id); nodes.set(rootNode.id, rootNode); } else { tree.subTrees.forEach((subTree) => { const subTreeNode = buildGraphFetchSubTree( editorStore, subTree, undefined, nodes, fetchConstraints, isReadOnly, ); addUniqueEntry(rootIds, subTreeNode.id); nodes.set(subTreeNode.id, subTreeNode); }); } return { rootIds, nodes, tree }; }; export const isGraphFetchTreeDataEmpty = ( treeData: DataQualityGraphFetchTreeData, ): boolean => treeData.tree.subTrees.length === 0; export const isConstraintsClassesTreeEmpty = ( treeData: DataQualityGraphFetchTreeData, ): boolean => treeData.rootIds.length === 0; export const updateNodeConstraints = action( ( treeData: DataQualityGraphFetchTreeData, node: DataQualityGraphFetchTreeNodeData, constraints: Constraint[], addConstraint: boolean, ): void => { //update the node of graph fetch tree present const updatedTreeNode = node.tree; if (addConstraint) { constraints.forEach((constraint) => { updatedTreeNode.constraints.push(constraint.name); }); } else { updatedTreeNode.constraints = updatedTreeNode.constraints.filter( (constraintName) => !constraints.find((constraint) => constraint.name === constraintName), ); } }, ); export const addQueryBuilderPropertyNode = ( treeData: DataQualityGraphFetchTreeData, explorerTreeData: TreeData<QueryBuilderExplorerTreeNodeData>, node: | QueryBuilderExplorerTreePropertyNodeData | QueryBuilderExplorerTreeRootNodeData | QueryBuilderExplorerTreeSubTypeNodeData, dataQualityState: DataQualityState, ): void => { const editorStore = dataQualityState.editorStore; const rootNodeId = generateRootGraphFetchTreeNodeID( undefined, treeData.tree.class.valueForSerialization ?? '', ); //root node and property node handled differently //handling property node if ( node instanceof QueryBuilderExplorerTreePropertyNodeData || node instanceof QueryBuilderExplorerTreeSubTypeNodeData ) { // traverse the property node all the way to the root and resolve the // chain of property that leads to this property node const propertyGraphFetchTrees: DataQualityPropertyGraphFetchTree[] = []; if (node instanceof QueryBuilderExplorerTreePropertyNodeData) { propertyGraphFetchTrees.push( new DataQualityPropertyGraphFetchTree( PropertyExplicitReference.create(node.property), undefined, ), ); } let parentExplorerTreeNode = explorerTreeData.nodes.get(node.parentId); while ( parentExplorerTreeNode instanceof QueryBuilderExplorerTreePropertyNodeData || parentExplorerTreeNode instanceof QueryBuilderExplorerTreeSubTypeNodeData ) { let subType = node instanceof QueryBuilderExplorerTreeSubTypeNodeData ? PackageableElementExplicitReference.create(node.subclass) : undefined; let subtypeAssigned = node instanceof QueryBuilderExplorerTreeSubTypeNodeData; while ( parentExplorerTreeNode instanceof QueryBuilderExplorerTreeSubTypeNodeData ) { if (!subtypeAssigned) { subType = PackageableElementExplicitReference.create( parentExplorerTreeNode.subclass, ); subtypeAssigned = true; } parentExplorerTreeNode = explorerTreeData.nodes.get( parentExplorerTreeNode.parentId, ); } if ( parentExplorerTreeNode instanceof QueryBuilderExplorerTreePropertyNodeData && parentExplorerTreeNode.mappingData.entityMappedProperty?.subType && parentExplorerTreeNode.type instanceof Class ) { subType = PackageableElementExplicitReference.create( parentExplorerTreeNode.type, ); } if ( parentExplorerTreeNode instanceof QueryBuilderExplorerTreePropertyNodeData ) { const propertyGraphFetchTree = new DataQualityPropertyGraphFetchTree( PropertyExplicitReference.create(parentExplorerTreeNode.property), subType, ); if (propertyGraphFetchTrees.length > 0) { propertyGraphFetchTree.subTrees.push( propertyGraphFetchTrees[0] as DataQualityPropertyGraphFetchTree, ); } propertyGraphFetchTrees.unshift(propertyGraphFetchTree); parentExplorerTreeNode = explorerTreeData.nodes.get( parentExplorerTreeNode.parentId, ); } else { dataQualityState.applicationStore.notificationService.notifyError( `Can't cast the root class of graph fetch structure to its subtype`, ); return; } } // traverse the chain of property from the root class to find a node in the // current graph fetch tree that matches any of this property, consider // this the starting point // // If we found a match, use that as the starting point, otherwise, use the root // of the graph fetch tree as the starting point // // NOTE: here we assume that we don't allow specifying duplicated nodes. // perhaps we need to allow that behavior in the future so people can customize the // shape of the object (e.g. same field but under different aliases) //current node id will be pointing root node id let currentNodeId: string | undefined = rootNodeId; let parentNode: DataQualityGraphFetchTreeNodeData | undefined = treeData.nodes.get(rootNodeId); let newSubTree: DataQualityPropertyGraphFetchTree | undefined; for (const propertyGraphFetchTree of propertyGraphFetchTrees) { currentNodeId = generateGraphFetchTreeNodeID( propertyGraphFetchTree.property.value, currentNodeId, propertyGraphFetchTree.subType?.value, ); const existingGraphFetchNode = treeData.nodes.get(currentNodeId); if (existingGraphFetchNode) { parentNode = existingGraphFetchNode; } else { newSubTree = propertyGraphFetchTree; break; } } // construct the query builder graph fetch subtree from the starting point if (newSubTree) { if (!parentNode) { let rootNode = treeData.nodes.get(rootNodeId); if (!rootNode) { rootNode = buildRootGraphFetchSubTree( editorStore, treeData.tree, undefined, treeData.nodes, true, false, ); addUniqueEntry(treeData.rootIds, rootNode.id); treeData.nodes.set(rootNode.id, rootNode); } parentNode = rootNode; } const childNode = buildGraphFetchSubTree( editorStore, newSubTree, parentNode, treeData.nodes, true, false, ); treeData.nodes.set(childNode.id, childNode); addUniqueEntry(parentNode.childrenIds, childNode.id); graphFetchTree_addSubTree( parentNode.tree, childNode.tree, dataQualityState.dataQualityQueryBuilderState.observerContext, ); } } else { //this case arises when root node is dragged let rootNodeFromTree = treeData.nodes.get(rootNodeId); //check if root node already exists in data quality root graph fetch tree if (!rootNodeFromTree) { rootNodeFromTree = buildRootGraphFetchSubTree( editorStore, treeData.tree, undefined, treeData.nodes, true, false, ); addUniqueEntry(treeData.rootIds, rootNodeFromTree.id); } treeData.nodes.set(rootNodeFromTree.id, rootNodeFromTree); } }; export const buildDefaultDataQualityRootGraphFetchTree = action( (selectedClass: Class): DataQualityRootGraphFetchTree => { const dataQualityRootGraphFetchTree = new DataQualityRootGraphFetchTree( PackageableElementExplicitReference.create(selectedClass), ); dataQualityRootGraphFetchTree.constraints = selectedClass.constraints.map( (constraint) => constraint.name, ); dataQualityRootGraphFetchTree.subTrees = selectedClass.properties .filter((property) => property.multiplicity.lowerBound > 0) .map( (property) => new DataQualityPropertyGraphFetchTree( PropertyExplicitReference.create(property), undefined, ), ); return dataQualityRootGraphFetchTree; }, );