UNPKG

@itwin/presentation-hierarchies

Version:

A package for creating hierarchies based on data in iTwin.js iModels.

467 lines 23.5 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * 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.NodeSelectClauseColumnNames = void 0; exports.createNodesQueryClauseFactory = createNodesQueryClauseFactory; const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const presentation_shared_1 = require("@itwin/presentation-shared"); /** * Column names of the SELECT clause created by `NodeSelectClauseFactory`. Order of the names matches the order of columns * created by the factory. * * @public */ var NodeSelectClauseColumnNames; (function (NodeSelectClauseColumnNames) { /** Full class name of the instance the node represents. Type: `string` in format `{schema name}:{class name}`. */ NodeSelectClauseColumnNames["FullClassName"] = "FullClassName"; /** ECInstance ID of the instance the node represents. Type: `Id64String`. */ NodeSelectClauseColumnNames["ECInstanceId"] = "ECInstanceId"; /** Display label. Type: `string`. */ NodeSelectClauseColumnNames["DisplayLabel"] = "DisplayLabel"; /** * A flag indicating if the node has children. Type: `boolean`. Values: * - `false` - node never has children. * - `true` - node always has children. * - NULL - unknown, needs to be determined by requesting children for the node. */ NodeSelectClauseColumnNames["HasChildren"] = "HasChildren"; /** A flag indicating that a node should be hidden if it has no children. Type: `boolean`. */ NodeSelectClauseColumnNames["HideIfNoChildren"] = "HideIfNoChildren"; /** A flag indicating that a node should be hidden and its children should be displayed instead. Type: `boolean`. */ NodeSelectClauseColumnNames["HideNodeInHierarchy"] = "HideNodeInHierarchy"; /** A serialized JSON object for providing grouping information. */ NodeSelectClauseColumnNames["Grouping"] = "Grouping"; /** A serialized JSON object for associating a node with additional data. */ NodeSelectClauseColumnNames["ExtendedData"] = "ExtendedData"; /** A flag indicating the node should be auto-expanded when it's loaded. */ NodeSelectClauseColumnNames["AutoExpand"] = "AutoExpand"; /** A flag indicating the node supports child hierarchy level filtering. */ NodeSelectClauseColumnNames["SupportsFiltering"] = "SupportsFiltering"; })(NodeSelectClauseColumnNames || (exports.NodeSelectClauseColumnNames = NodeSelectClauseColumnNames = {})); /** * Creates an instance of `NodeSelectQueryFactory`. * @public */ function createNodesQueryClauseFactory(props) { return new NodeSelectQueryFactory(props); } /** A factory for creating a nodes' select ECSQL query. */ class NodeSelectQueryFactory { _imodelAccess; _instanceLabelSelectClauseFactory; constructor(props) { this._imodelAccess = props.imodelAccess; this._instanceLabelSelectClauseFactory = props.instanceLabelSelectClauseFactory; } /** Create a SELECT clause in a format understood by results reader of the library. */ async createSelectClause(props) { // note: the columns order must match the order in `NodeSelectClauseColumnNames` return ` ec_ClassName(${props.ecClassId.selector}) AS ${NodeSelectClauseColumnNames.FullClassName}, ${props.ecInstanceId.selector} AS ${NodeSelectClauseColumnNames.ECInstanceId}, ${createECSqlValueSelector(props.nodeLabel)} AS ${NodeSelectClauseColumnNames.DisplayLabel}, CAST(${createECSqlValueSelector(props.hasChildren)} AS BOOLEAN) AS ${NodeSelectClauseColumnNames.HasChildren}, CAST(${createECSqlValueSelector(props.hideIfNoChildren)} AS BOOLEAN) AS ${NodeSelectClauseColumnNames.HideIfNoChildren}, CAST(${createECSqlValueSelector(props.hideNodeInHierarchy)} AS BOOLEAN) AS ${NodeSelectClauseColumnNames.HideNodeInHierarchy}, ${props.grouping ? await createGroupingSelector(props.grouping, this._imodelAccess, this._instanceLabelSelectClauseFactory) : "CAST(NULL AS TEXT)"} AS ${NodeSelectClauseColumnNames.Grouping}, ${props.extendedData ? `json_object(${Object.entries(props.extendedData) .map(([key, value]) => `'${key}', ${createECSqlValueSelector(value)}`) .join(", ")})` : "CAST(NULL AS TEXT)"} AS ${NodeSelectClauseColumnNames.ExtendedData}, CAST(${createECSqlValueSelector(props.autoExpand)} AS BOOLEAN) AS ${NodeSelectClauseColumnNames.AutoExpand}, CAST(${createECSqlValueSelector(props.supportsFiltering)} AS BOOLEAN) AS ${NodeSelectClauseColumnNames.SupportsFiltering} `; } /** * Creates the necessary ECSQL snippets to create an instance filter described by the `filter` argument. * - `from` is set to either the `contentClass.fullName` or one of `filter.propertyClassNames`, depending on which is more specific. * - `joins` is set to a number of `JOIN` clauses required to join all relationships described by `filter.relatedInstances`. * - `where` is set to a `WHERE` clause (without the `WHERE` keyword) that filters instances by classes on * `filter.filterClassNames` and by properties as described by `filter.rules`. * * Special cases: * - If `filter` is `undefined`, `joins` and `where` are set to empty strings and `from` is set to `contentClass.fullName`. * - If the provided content class doesn't intersect with the property class in provided filter, a special result * is returned to make sure the resulting query is valid and doesn't return anything. */ async createFilterClauses(props) { const { contentClass, filter } = props; if (!filter) { // undefined filter means we don't want any filtering to be applied return { from: contentClass.fullName, joins: "", where: "" }; } const from = await specializeContentClass({ classHierarchyInspector: this._imodelAccess, contentClassName: contentClass.fullName, filterClassNames: filter.propertyClassNames, }); if (!from) { // filter class doesn't intersect with content class - make sure the query returns nothing by returning a `FALSE` WHERE clause return { from: contentClass.fullName, joins: "", where: "FALSE" }; } /** * TODO: * There may be similar related instance paths, e.g.: * - A -> B -> C, * - A -> B -> D. * At this moment we're handling each path individually which means A and B would get joined twice. We could look into * creating a join tree first, e.g. * C * / * A -> B * \ * D */ const joins = await Promise.all(filter.relatedInstances.map(async (rel, i) => presentation_shared_1.ECSql.createRelationshipPathJoinClause({ schemaProvider: this._imodelAccess, path: assignRelationshipPathAliases(rel.path, i, contentClass.alias, rel.alias), }))); const whereConditions = new Array(); if (filter.filteredClassNames && filter.filteredClassNames.length > 0) { whereConditions.push(`${presentation_shared_1.ECSql.createRawPropertyValueSelector(contentClass.alias, "ECClassId")} IS ( ${filter.filteredClassNames .map(presentation_shared_1.parseFullClassName) .map(({ schemaName, className }) => `[${schemaName}].[${className}]`) .join(", ")} )`); } const classAliasMap = new Map([[contentClass.alias, from]]); filter.relatedInstances.forEach(({ path, alias }) => path.length > 0 && classAliasMap.set(alias, path[path.length - 1].targetClassName)); const propertiesFilter = await createWhereClause(contentClass.alias, async (alias) => { const aliasClassName = classAliasMap.get(alias); return aliasClassName ? (0, presentation_shared_1.getClass)(this._imodelAccess, aliasClassName) : undefined; }, filter.rules); if (propertiesFilter) { whereConditions.push(propertiesFilter); } return { from, joins: joins.join("\n"), where: whereConditions.join(" AND ") }; } } function createECSqlValueSelector(input) { if (input === undefined) { return "NULL"; } if (isSelector(input)) { return input.selector; } return presentation_shared_1.ECSql.createRawPrimitiveValueSelector(input); } function isSelector(x) { return !!x.selector; } async function createGroupingSelector(grouping, imodelAccess, instanceLabelSelectClauseFactory) { const groupingSelectors = new Array(); grouping.byLabel && groupingSelectors.push({ key: "byLabel", selector: typeof grouping.byLabel === "boolean" || isSelector(grouping.byLabel) ? createECSqlValueSelector(grouping.byLabel) : serializeJsonObject(createLabelGroupingBaseParamsSelectors(grouping.byLabel)), }); grouping.byClass && groupingSelectors.push({ key: "byClass", selector: typeof grouping.byClass === "boolean" || isSelector(grouping.byClass) ? createECSqlValueSelector(grouping.byClass) : serializeJsonObject(createBaseGroupingParamSelectors(grouping.byClass)), }); grouping.byBaseClasses && groupingSelectors.push({ key: "byBaseClasses", selector: serializeJsonObject([ { key: "fullClassNames", selector: `json_array(${grouping.byBaseClasses.fullClassNames.map((className) => createECSqlValueSelector(className)).join(", ")})`, }, ...createBaseGroupingParamSelectors(grouping.byBaseClasses), ]), }); // TODO: MISSING_COVERAGE /* v8 ignore else -- @preserve */ if (grouping.byProperties) { const propertyClass = await (0, presentation_shared_1.getClass)(imodelAccess, grouping.byProperties.propertiesClassName); groupingSelectors.push({ key: "byProperties", selector: serializeJsonObject([ { key: "propertiesClassName", selector: `${createECSqlValueSelector(grouping.byProperties.propertiesClassName)}`, }, { key: "propertyGroups", selector: `json_array(${(await Promise.all(grouping.byProperties.propertyGroups.map(async (propertyGroup) => serializeJsonObject(await createPropertyGroupSelectors(propertyGroup, propertyClass, instanceLabelSelectClauseFactory))))).join(", ")})`, }, ...(grouping.byProperties.createGroupForOutOfRangeValues !== undefined ? [ { key: "createGroupForOutOfRangeValues", selector: `CAST(${createECSqlValueSelector(grouping.byProperties.createGroupForOutOfRangeValues)} AS BOOLEAN)`, }, ] : []), ...(grouping.byProperties.createGroupForUnspecifiedValues !== undefined ? [ { key: "createGroupForUnspecifiedValues", selector: `CAST(${createECSqlValueSelector(grouping.byProperties.createGroupForUnspecifiedValues)} AS BOOLEAN)`, }, ] : []), ...createBaseGroupingParamSelectors(grouping.byProperties), ]), }); } return serializeJsonObject(groupingSelectors); } function createLabelGroupingBaseParamsSelectors(byLabel) { const selectors = new Array(); if (byLabel.action !== undefined) { selectors.push({ key: "action", selector: `${createECSqlValueSelector(byLabel.action)}` }); } if (byLabel.groupId !== undefined) { selectors.push({ key: "groupId", selector: createECSqlValueSelector(byLabel.groupId) }); } if (byLabel.action !== "merge") { selectors.push(...createBaseGroupingParamSelectors(byLabel)); } return selectors; } async function createPropertyGroupSelectors(propertyGroup, propertyClass, instanceLabelSelectClauseFactory) { const selectors = new Array(); selectors.push({ key: "propertyName", selector: `${createECSqlValueSelector(propertyGroup.propertyName)}` }); const property = await propertyClass.getProperty(propertyGroup.propertyName); if (!property) { throw new Error(`Property "${propertyGroup.propertyName}" not found in ECClass "${propertyClass.fullName}".`); } if (property.isNavigation()) { const relationshipClass = await property.relationshipClass; const abstractConstraint = property.direction === "Forward" ? await relationshipClass.target.abstractConstraint : await relationshipClass.source.abstractConstraint; // TODO: MISSING_COVERAGE /* v8 ignore else -- @preserve */ if (!abstractConstraint) { throw new Error(`Could not determine class name for navigation property with direction "${property.direction}".`); } const fullName = abstractConstraint.fullName; const targetAlias = "target"; selectors.push({ key: "propertyValue", selector: `( SELECT ${await instanceLabelSelectClauseFactory.createSelectClause({ className: fullName, classAlias: targetAlias })} FROM ${fullName} AS ${targetAlias} WHERE [${targetAlias}].[ECInstanceId] = [${propertyGroup.propertyClassAlias}].[${propertyGroup.propertyName}].[Id] )`, }); } else { selectors.push({ key: "propertyValue", selector: `[${propertyGroup.propertyClassAlias}].[${propertyGroup.propertyName}]`, }); } if (propertyGroup.ranges) { selectors.push(createRangeParamSelectors(propertyGroup.ranges)); } return selectors; } function createRangeParamSelectors(ranges) { return { key: "ranges", selector: `json_array(${ranges .map((range) => serializeJsonObject([ { key: "fromValue", selector: createECSqlValueSelector(range.fromValue) }, { key: "toValue", selector: createECSqlValueSelector(range.toValue) }, ...(range.rangeLabel ? [{ key: "rangeLabel", selector: `${createECSqlValueSelector(range.rangeLabel)}` }] : []), ])) .join(", ")})`, }; } function createBaseGroupingParamSelectors(params) { const selectors = new Array(); if (params.hideIfNoSiblings !== undefined) { selectors.push({ key: "hideIfNoSiblings", selector: createECSqlValueSelector(params.hideIfNoSiblings) }); } if (params.hideIfOneGroupedNode !== undefined) { selectors.push({ key: "hideIfOneGroupedNode", selector: createECSqlValueSelector(params.hideIfOneGroupedNode) }); } if (params.autoExpand !== undefined) { selectors.push({ key: "autoExpand", selector: createECSqlValueSelector(params.autoExpand) }); } return selectors; } function serializeJsonObject(selectors) { return `json_object(${selectors.map(({ key, selector }) => `'${key}', ${selector}`).join(", ")})`; } async function createWhereClause(contentClassAlias, classLoader, rule) { if (core_common_1.GenericInstanceFilter.isFilterRuleGroup(rule)) { const clause = (await Promise.all(rule.rules.map(async (r) => createWhereClause(contentClassAlias, classLoader, r)))) .filter((c) => !!c) .join(` ${getECSqlLogicalOperator(rule.operator)} `); return clause ? (rule.operator === "or" ? `(${clause})` : clause) : undefined; } const sourceAlias = rule.sourceAlias ? rule.sourceAlias : contentClassAlias; const propertyValueSelector = presentation_shared_1.ECSql.createRawPropertyValueSelector(sourceAlias, rule.propertyName); if (isUnaryRuleOperator(rule.operator)) { switch (rule.operator) { case "is-true": return propertyValueSelector; case "is-false": return `NOT ${propertyValueSelector}`; case "is-null": return `${propertyValueSelector} IS NULL`; case "is-not-null": return `${propertyValueSelector} IS NOT NULL`; } } const ecsqlOperator = getECSqlComparisonOperator(rule.operator); if (rule.value === undefined) { throw new Error(`Rule "${rule.propertyName}" ${ecsqlOperator} is missing value.`); } const value = rule.value.rawValue; if (rule.operator === "like" && typeof value === "string") { const escapedValue = value.replace(/[%_\\]/g, "\\$&"); return `${propertyValueSelector} ${ecsqlOperator} '%${escapedValue}%' ESCAPE '\\'`; } const propertyClass = await classLoader(sourceAlias); if (!propertyClass) { throw new Error(`Class with alias "${sourceAlias}" not found.`); } const property = await propertyClass.getProperty(rule.propertyName); if (!property) { throw new Error(`Property "${rule.propertyName}" not found in ECClass "${propertyClass.fullName}".`); } if (property.isNavigation()) { (0, core_bentley_1.assert)(rule.value !== undefined && core_common_1.GenericInstanceFilterRuleValue.isInstanceKey(value)); return `${propertyValueSelector}.[Id] ${ecsqlOperator} ${presentation_shared_1.ECSql.createRawPrimitiveValueSelector(value.id)}`; } if (property.isEnumeration()) { (0, core_bentley_1.assert)(rule.value !== undefined && !core_common_1.GenericInstanceFilterRuleValue.isInstanceKey(value)); return `${propertyValueSelector} ${ecsqlOperator} ${presentation_shared_1.ECSql.createRawPrimitiveValueSelector(value)}`; } if (property.isPrimitive()) { (0, core_bentley_1.assert)(rule.value !== undefined && !core_common_1.GenericInstanceFilterRuleValue.isInstanceKey(value)); switch (property.primitiveType) { case "Point2d": { (0, core_bentley_1.assert)(rule.operator === "is-equal" || rule.operator === "is-not-equal"); (0, core_bentley_1.assert)(presentation_shared_1.PrimitiveValue.isPoint2d(value)); const condition = ` ${createFloatingPointEqualityClause(`${propertyValueSelector}.[x]`, "is-equal", value.x)} AND ${createFloatingPointEqualityClause(`${propertyValueSelector}.[y]`, "is-equal", value.y)} `; return rule.operator === "is-equal" ? condition : `NOT (${condition})`; } case "Point3d": { (0, core_bentley_1.assert)(rule.operator === "is-equal" || rule.operator === "is-not-equal"); (0, core_bentley_1.assert)(presentation_shared_1.PrimitiveValue.isPoint3d(value)); const condition = ` ${createFloatingPointEqualityClause(`${propertyValueSelector}.[x]`, "is-equal", value.x)} AND ${createFloatingPointEqualityClause(`${propertyValueSelector}.[y]`, "is-equal", value.y)} AND ${createFloatingPointEqualityClause(`${propertyValueSelector}.[z]`, "is-equal", value.z)} `; return rule.operator === "is-equal" ? condition : `NOT (${condition})`; } case "Double": { (0, core_bentley_1.assert)(typeof value === "number"); if (rule.operator === "is-equal" || rule.operator === "is-not-equal") { return `${createFloatingPointEqualityClause(propertyValueSelector, rule.operator, value)}`; } return `${propertyValueSelector} ${ecsqlOperator} ${presentation_shared_1.ECSql.createRawPrimitiveValueSelector(value)}`; } default: { return `${propertyValueSelector} ${ecsqlOperator} ${presentation_shared_1.ECSql.createRawPrimitiveValueSelector(value)}`; } } } throw new Error("Struct and array properties are not supported for filtering"); } function createFloatingPointEqualityClause(valueSelector, operator, value) { const [from, to] = getFloatingPointValueRange(value); return `${valueSelector} ${operator === "is-not-equal" ? "NOT " : ""} BETWEEN ${from} AND ${to}`; } function getFloatingPointValueRange(value) { return [value - Number.EPSILON, value + Number.EPSILON]; } function getECSqlLogicalOperator(op) { switch (op) { case "and": return "AND"; case "or": return "OR"; } } function isUnaryRuleOperator(op) { return op === "is-true" || op === "is-false" || op === "is-null" || op === "is-not-null"; } function getECSqlComparisonOperator(op) { switch (op) { case "is-equal": return `=`; case "is-not-equal": return `<>`; case "greater": return `>`; case "greater-or-equal": return `>=`; case "less": return `<`; case "less-or-equal": return `<=`; case "like": return `LIKE`; } } function assignRelationshipPathAliases(path, pathIndex, sourceAlias, targetAlias) { function createAlias(fullClassName, index) { return `rel_${pathIndex}_${fullClassName.replaceAll(/[\.:]/g, "_")}_${index}`; } const result = []; path.forEach((step, i) => { result.push({ targetClassName: step.targetClassName, relationshipName: step.relationshipClassName, sourceClassName: step.sourceClassName, relationshipReverse: !step.isForwardRelationship, sourceAlias: i === 0 ? sourceAlias : result[i - 1].targetAlias, relationshipAlias: createAlias(step.relationshipClassName, i), targetAlias: i === path.length - 1 ? targetAlias : createAlias(step.targetClassName, i), joinType: "inner", }); }); return result; } async function specializeContentClass(props) { const filterClass = await getSpecializedPropertyClass(props.classHierarchyInspector, props.filterClassNames); if (!filterClass) { return props.contentClassName; } if (await props.classHierarchyInspector.classDerivesFrom(filterClass, props.contentClassName)) { return filterClass; } if (await props.classHierarchyInspector.classDerivesFrom(props.contentClassName, filterClass)) { return props.contentClassName; } return undefined; } async function getSpecializedPropertyClass(classHierarchyInspector, classes) { if (classes.length === 0) { return undefined; } const [currClassName, ...restClasses] = classes; let resolvedClassName = currClassName; for (const propClassName of restClasses) { if (await classHierarchyInspector.classDerivesFrom(propClassName, resolvedClassName)) { resolvedClassName = propClassName; } } return resolvedClassName; } //# sourceMappingURL=NodeSelectQueryFactory.js.map