@finos/legend-extension-dsl-data-quality
Version:
Legend extension for Data Quality
322 lines • 16.5 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 {} from '@finos/legend-art';
import { addUniqueEntry, assertErrorThrown, assertType, deleteEntry, hashArray, LogEvent, UnsupportedOperationError, } from '@finos/legend-shared';
import { action, computed, makeObservable, observable } from 'mobx';
import { Class, GRAPH_MANAGER_EVENT, PackageableElementExplicitReference, PropertyExplicitReference, getAllClassConstraints, isStubbed_RawLambda, TYPE_CAST_TOKEN, } from '@finos/legend-graph';
import { graphFetchTree_addSubTree, graphFetchTree_removeSubTree, QueryBuilderExplorerTreePropertyNodeData, QueryBuilderExplorerTreeSubTypeNodeData, } from '@finos/legend-query-builder';
import { DataQualityPropertyGraphFetchTree, DataQualityRootGraphFetchTree, } from '../../graph/metamodel/pure/packageableElements/data-quality/DataQualityGraphFetchTree.js';
import { ConstraintState } from '../states/ConstraintState.js';
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 {
id;
label;
tree;
parentId;
editorStore;
isOpen;
isReadOnly;
childrenIds = [];
constraints = [];
constructor(editorStore, id, label, parentId, graphFetchTreeNode) {
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) {
this.isReadOnly = isReadOnly;
}
setConstraintsForClass(_class, constraintsToSelect) {
const constraints = getAllClassConstraints(_class).map((constraint) => new ConstraintState(constraint));
const lambdas = new Map();
const index = new Map();
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) => {
res.forEach((grammarText, key) => {
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) {
this.constraints = constraints;
}
get 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() {
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 const generateRootGraphFetchTreeNodeID = (parentNodeId, classValue) => `${parentNodeId ? `${parentNodeId}.` : ''}${classValue ? `${TYPE_CAST_TOKEN}${classValue}` : ''}`;
const getPrunableNodes = (treeData) => 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, node) => {
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) => {
let prunableNodes = getPrunableNodes(treeData);
while (prunableNodes.length) {
prunableNodes.forEach((node) => {
removeNode(treeData, node);
});
prunableNodes = getPrunableNodes(treeData);
}
};
export const removeNodeRecursively = (treeData, node) => {
removeNode(treeData, node);
pruneTreeData(treeData);
};
export const generateGraphFetchTreeNodeID = (property, parentNodeId, subType) => `${parentNodeId ? `${parentNodeId}.` : ''}${property.name}${subType ? `${TYPE_CAST_TOKEN}${subType.path}` : ''}`;
const buildGraphFetchSubTree = (editorStore, tree, parentNode, nodes, fetchConstraints, isReadOnly) => {
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, tree, parentNode, nodes, fetchConstraints, isReadonly) => {
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, tree, displayRoot, fetchConstraints, isReadOnly) => {
const rootIds = [];
const nodes = new Map();
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) => treeData.tree.subTrees.length === 0;
export const isConstraintsClassesTreeEmpty = (treeData) => treeData.rootIds.length === 0;
export const updateNodeConstraints = action((treeData, node, constraints, addConstraint) => {
//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, explorerTreeData, node, dataQualityState) => {
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 = [];
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]);
}
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 = rootNodeId;
let parentNode = treeData.nodes.get(rootNodeId);
let newSubTree;
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) => {
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;
});
//# sourceMappingURL=DataQualityGraphFetchTreeUtil.js.map