@finos/legend-extension-dsl-data-quality
Version:
Legend extension for Data Quality
604 lines (576 loc) • 19.3 kB
text/typescript
/**
* 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;
},
);