@itwin/presentation-hierarchies
Version:
A package for creating hierarchies based on data in iTwin.js iModels.
274 lines • 14.3 kB
JavaScript
;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ECSQL_COLUMN_NAME_FilterClassName = exports.ECSQL_COLUMN_NAME_FilterECInstanceId = exports.FilteringHierarchyDefinition = void 0;
exports.applyECInstanceIdsFilter = applyECInstanceIdsFilter;
exports.applyECInstanceIdsSelector = applyECInstanceIdsSelector;
const rxjs_1 = require("rxjs");
const HierarchyFiltering_js_1 = require("../HierarchyFiltering.js");
const HierarchyNodeIdentifier_js_1 = require("../HierarchyNodeIdentifier.js");
const IModelHierarchyDefinition_js_1 = require("../imodel/IModelHierarchyDefinition.js");
const IModelHierarchyNode_js_1 = require("../imodel/IModelHierarchyNode.js");
const NodeSelectQueryFactory_js_1 = require("../imodel/NodeSelectQueryFactory.js");
const TreeNodesReader_js_1 = require("../imodel/TreeNodesReader.js");
const Partition_js_1 = require("../internal/operators/Partition.js");
/** @internal */
class FilteringHierarchyDefinition {
_imodelAccess;
_source;
_nodeIdentifierPaths;
_nodesParser;
constructor(props) {
this._imodelAccess = props.imodelAccess;
this._source = props.source;
this._nodeIdentifierPaths = props.nodeIdentifierPaths;
this._nodesParser = props.nodesParser ?? this._source.parseNode ?? ((row) => (0, rxjs_1.of)((0, TreeNodesReader_js_1.defaultNodesParser)(row)));
}
get preProcessNode() {
return (node) => {
return (this._source.preProcessNode ? this._source.preProcessNode(node) : (0, rxjs_1.of)(node)).pipe((0, rxjs_1.filter)((processedNode) => {
if (processedNode.processingParams?.hideInHierarchy && processedNode.filtering?.isFilterTarget && !processedNode.filtering.hasFilterTargetAncestor) {
// we want to hide target nodes if they have `hideInHierarchy` param, but only if they're not under another filter target
return false;
}
return true;
}));
};
}
shouldExpandGroupingNode(node) {
for (const child of node.children) {
/* c8 ignore next 3 */
if (!child.filtering) {
continue;
}
if (child.filtering.isFilterTarget) {
const childAutoExpand = child.filtering.filterTargetOptions?.autoExpand;
// If child is a filter target and is has no `autoExpand` flag, then it's always to be expanded.
if (childAutoExpand === true) {
return true;
}
// If it's not an object and not `true`, then it's falsy - continue looking
if (typeof childAutoExpand !== "object") {
continue;
}
// If grouping node's child has `autoExpandUntil` flag,
// auto-expand the grouping node only if it's depth is lower than that of the grouping node in associated with the target.
const nodeDepth = node.parentKeys.length;
const filterTargetDepth = childAutoExpand.depth;
if (nodeDepth < filterTargetDepth) {
return true;
}
}
if (!child.filtering.filteredChildrenIdentifierPaths) {
/* c8 ignore next */
continue;
}
for (const path of child.filtering.filteredChildrenIdentifierPaths) {
if ("path" in path && path.options?.autoExpand) {
return true;
}
}
}
return false;
}
get postProcessNode() {
return (node) => {
return (this._source.postProcessNode ? this._source.postProcessNode(node) : (0, rxjs_1.of)(node)).pipe((0, rxjs_1.map)((processedNode) => {
// instance nodes get the auto-expand flag in `parseNode`, but grouping ones need to be handled during post-processing
if (IModelHierarchyNode_js_1.ProcessedHierarchyNode.isGroupingNode(node) && this.shouldExpandGroupingNode(node)) {
Object.assign(processedNode, { autoExpand: true });
}
return processedNode;
}));
};
}
get parseNode() {
return (row, parentNode) => {
return this._nodesParser(row).pipe((0, rxjs_1.mergeMap)((parsedNode) => {
if (!row[exports.ECSQL_COLUMN_NAME_FilterECInstanceId] || !row[exports.ECSQL_COLUMN_NAME_FilterClassName]) {
return (0, rxjs_1.of)(parsedNode);
}
const rowInstanceKey = { className: row[exports.ECSQL_COLUMN_NAME_FilterClassName], id: row[exports.ECSQL_COLUMN_NAME_FilterECInstanceId] };
const filteringHelper = (0, HierarchyFiltering_js_1.createHierarchyFilteringHelper)(this._nodeIdentifierPaths, parentNode);
const nodeExtraPropsPossiblyPromise = filteringHelper.createChildNodePropsAsync({
pathMatcher: (identifier) => {
if (identifier.id !== rowInstanceKey.id) {
return false;
}
if (!HierarchyNodeIdentifier_js_1.HierarchyNodeIdentifier.isInstanceNodeIdentifier(identifier)) {
return false;
}
if (identifier.imodelKey && identifier.imodelKey !== this._imodelAccess.imodelKey) {
return false;
}
if (identifier.className === rowInstanceKey.className) {
return true;
}
return Promise.all([
this._imodelAccess.classDerivesFrom(identifier.className, rowInstanceKey.className),
this._imodelAccess.classDerivesFrom(rowInstanceKey.className, identifier.className),
]).then(([isDerivedFrom, isDerivedTo]) => isDerivedFrom || isDerivedTo);
},
});
return (nodeExtraPropsPossiblyPromise instanceof Promise ? (0, rxjs_1.from)(nodeExtraPropsPossiblyPromise) : (0, rxjs_1.of)(nodeExtraPropsPossiblyPromise)).pipe((0, rxjs_1.map)((nodeExtraProps) => {
if (nodeExtraProps?.autoExpand) {
parsedNode.autoExpand = true;
}
if (nodeExtraProps?.filtering) {
parsedNode.filtering = nodeExtraProps.filtering;
}
return parsedNode;
}));
}));
};
}
defineHierarchyLevel(props) {
const sourceDefinitions = this._source.defineHierarchyLevel(props);
const filteringHelper = (0, HierarchyFiltering_js_1.createHierarchyFilteringHelper)(this._nodeIdentifierPaths, props.parentNode);
const childNodeFilteringIdentifiers = filteringHelper.getChildNodeFilteringIdentifiers();
if (!childNodeFilteringIdentifiers) {
return sourceDefinitions;
}
const [genericNodeDefinitions, instanceNodeDefinitions] = (0, Partition_js_1.partition)(sourceDefinitions.pipe((0, rxjs_1.mergeAll)()), IModelHierarchyDefinition_js_1.HierarchyNodesDefinition.isGenericNode);
return (0, rxjs_1.merge)(genericNodeDefinitions.pipe((0, rxjs_1.map)((definition) => {
if (filteringHelper.hasFilterTargetAncestor ||
childNodeFilteringIdentifiers.some((identifier) => HierarchyNodeIdentifier_js_1.HierarchyNodeIdentifier.isGenericNodeIdentifier(identifier) &&
(!identifier.source || identifier.source === this._imodelAccess.imodelKey) &&
identifier.id === definition.node.key)) {
return {
...definition,
node: {
...definition.node,
...filteringHelper.createChildNodeProps({ pathMatcher: ({ id }) => id === definition.node.key }),
},
};
}
return undefined;
}), (0, rxjs_1.filter)((def) => !!def)), instanceNodeDefinitions.pipe((0, rxjs_1.mergeMap)((definition) => {
if (filteringHelper.hasFilterTargetAncestor) {
// if we have a filter target ancestor, we don't need to filter the definitions - we use all of them
return (0, rxjs_1.of)(applyECInstanceIdsSelector(definition));
}
return (0, rxjs_1.defer)(async () => {
const imodelAccess = this._imodelAccess;
const matches = new Map();
async function getEntries(x) {
let entries = matches.get(x.id);
if (!entries) {
entries = [];
matches.set(x.id, entries);
}
// make sure that we don't have path identifiers that only differ by class name, where
// class names are in the same class hierarchy, e.g.:
// [{ className: "bis.Element", id: "0x1" }, { className: "bis.Subject", id: "0x1" }]
// are actually the same instance, so we only need to keep one of them, or otherwise the query will
// duplicate it
for (const entry of entries) {
if (entry.className === x.className ||
(await imodelAccess.classDerivesFrom(entry.className, x.className)) ||
(await imodelAccess.classDerivesFrom(x.className, entry.className))) {
return undefined;
}
}
return entries;
}
for (const id of childNodeFilteringIdentifiers) {
// take only identifiers that match the definition by type, source and class
if (!HierarchyNodeIdentifier_js_1.HierarchyNodeIdentifier.isInstanceNodeIdentifier(id)) {
continue;
}
if (id.imodelKey && id.imodelKey !== imodelAccess.imodelKey) {
continue;
}
if (id.className !== definition.fullClassName &&
!(await Promise.all([
imodelAccess.classDerivesFrom(id.className, definition.fullClassName),
imodelAccess.classDerivesFrom(definition.fullClassName, id.className),
]).then(([isDerivedFrom, isDerivedTo]) => isDerivedFrom || isDerivedTo))) {
continue;
}
const entriesTargetForPathIdentifier = await getEntries(id);
if (entriesTargetForPathIdentifier) {
entriesTargetForPathIdentifier.push(id);
}
}
return Array.from(matches.values()).flat();
}).pipe(
// only take definitions that have matching path identifiers
(0, rxjs_1.filter)((pathIdentifiers) => pathIdentifiers.length > 0),
// for each definition that we're going to use, apply query-level filter
(0, rxjs_1.map)((pathIdentifiers) => applyECInstanceIdsFilter(definition, pathIdentifiers)));
}))).pipe((0, rxjs_1.toArray)());
}
}
exports.FilteringHierarchyDefinition = FilteringHierarchyDefinition;
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
exports.ECSQL_COLUMN_NAME_FilterECInstanceId = "FilterECInstanceId";
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
exports.ECSQL_COLUMN_NAME_FilterClassName = "FilterClassName";
function getClassECInstanceIds(filteredInstanceKeys) {
const classNameECInstanceIds = new Map();
for (const key of filteredInstanceKeys) {
const entry = classNameECInstanceIds.get(key.className);
if (entry === undefined) {
classNameECInstanceIds.set(key.className, [key.id]);
continue;
}
entry.push(key.id);
}
return classNameECInstanceIds;
}
/** @internal */
function applyECInstanceIdsFilter(def, filteredInstanceKeys) {
const instanceIdsByClass = getClassECInstanceIds(filteredInstanceKeys);
return {
...def,
query: {
...def.query,
ctes: [
...(def.query.ctes ?? []),
`FilteringInfo(ECInstanceId, FilterClassName) AS (
${Array.from(instanceIdsByClass)
.map(([className, ecInstanceIds]) => `
SELECT ECInstanceId, '${className}' AS FilterClassName
FROM ${className}
WHERE ECInstanceId IN (${ecInstanceIds.join(", ")})
`)
.join(" UNION ALL ")}
)`,
],
ecsql: `
SELECT
[q].*,
IdToHex([f].[ECInstanceId]) AS [${exports.ECSQL_COLUMN_NAME_FilterECInstanceId}],
[f].[FilterClassName] AS [${exports.ECSQL_COLUMN_NAME_FilterClassName}]
FROM (
${def.query.ecsql}
) [q]
JOIN FilteringInfo [f] ON [f].[ECInstanceId] = [q].[ECInstanceId]
`,
},
};
}
/** @internal */
function applyECInstanceIdsSelector(def) {
return {
...def,
query: {
...def.query,
ecsql: `
SELECT
[q].*,
IdToHex([q].[ECInstanceId]) AS [${exports.ECSQL_COLUMN_NAME_FilterECInstanceId}],
[q].[${NodeSelectQueryFactory_js_1.NodeSelectClauseColumnNames.FullClassName}] AS [${exports.ECSQL_COLUMN_NAME_FilterClassName}]
FROM (${def.query.ecsql}) [q]
`,
},
};
}
//# sourceMappingURL=FilteringHierarchyDefinition.js.map