@itwin/presentation-common
Version:
Common pieces for iModel.js presentation packages
239 lines • 10.8 kB
JavaScript
;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Core
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RulesetsFactory = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const TypeDescription_js_1 = require("./content/TypeDescription.js");
const Value_js_1 = require("./content/Value.js");
const EC_js_1 = require("./EC.js");
const ContentSpecification_js_1 = require("./rules/content/ContentSpecification.js");
const RelationshipDirection_js_1 = require("./rules/RelationshipDirection.js");
const Rule_js_1 = require("./rules/Rule.js");
/**
* A factory class that can be used to create presentation rulesets targeted towards
* specific use cases.
*
* @public
*/
class RulesetsFactory {
createSimilarInstancesRulesetInfo(field, record) {
if (!field.isPropertiesField()) {
throw new Error("Can only create 'similar instances' ruleset for properties-based records");
}
if (field.type.valueFormat !== TypeDescription_js_1.PropertyValueFormat.Primitive) {
throw new Error("Can only create 'similar instances' ruleset for primitive properties");
}
if (field.properties.length === 0) {
throw new Error("Invalid properties' field with no properties");
}
if (record.isFieldMerged(field.name)) {
throw new Error("Can't create 'similar instances' ruleset for merged values");
}
if (!record.classInfo) {
throw new Error("Can't create 'similar instances' for records based on multiple different ECClass instances");
}
const propertyName = getPropertyName(field);
const propertyValue = getPropertyValue(record, field);
const relatedInstanceInfo = createRelatedInstanceSpecInfo(field);
const relatedClass = relatedInstanceInfo?.class;
const ruleset = {
id: `SimilarInstances/${propertyName}/${core_bentley_1.Guid.createValue()}`,
rules: [],
};
ruleset.rules.push({
ruleType: Rule_js_1.RuleTypes.Content,
specifications: [
{
specType: ContentSpecification_js_1.ContentSpecificationTypes.ContentInstancesOfSpecificClasses,
classes: createMultiClassSpecification(record.classInfo),
relatedInstances: relatedInstanceInfo ? [relatedInstanceInfo.spec] : [],
instanceFilter: createInstanceFilter(relatedInstanceInfo?.spec, field.type, propertyName, propertyValue.raw),
},
],
});
return { ruleset, relatedClass, propertyName, propertyValue };
}
/**
* Create a ruleset with content rules for getting instances are of the
* same ECClass and have the same property value as the provided `record`.
* @param field A field identifying which property of the record we should use
* @param record A record whose similar instances should be found
* @param computeDisplayValue Optional callback function to calculate display value that's
* used in ruleset's description. If not provided, display value from record is used instead.
*/
async createSimilarInstancesRuleset(field, record, computeDisplayValue) {
const info = this.createSimilarInstancesRulesetInfo(field, record);
const description = await createDescriptionAsync(record, info.relatedClass, field, info.propertyValue, computeDisplayValue);
return { ruleset: info.ruleset, description };
}
}
exports.RulesetsFactory = RulesetsFactory;
const toString = (displayValue) => {
if (!displayValue) {
return "NULL";
}
return displayValue.toString();
};
const createDescription = (record, relatedClass, field, value) => {
const classInfo = relatedClass ?? record.classInfo;
return `[${classInfo.label}].[${field.label}] = ${value}`;
};
const createDescriptionAsync = async (record, relatedClass, field, value, computeDisplayValue) => {
const displayValue = computeDisplayValue ? await computeDisplayValue(field.type.typeName, value.raw, value.display) : toString(value.display);
return createDescription(record, relatedClass, field, displayValue);
};
const getPropertyName = (field) => {
let name = field.properties[0].property.name;
if (field.type.typeName === "navigation") {
name += ".Id";
}
return name;
};
const isPrimitivePropertyValue = (value) => {
if (Value_js_1.Value.isPrimitive(value)) {
return true;
}
if (Value_js_1.Value.isMap(value)) {
if (typeof value.x === "number" && typeof value.y === "number" && (value.z === undefined || typeof value.z === "number")) {
return true;
}
if (typeof value.className === "string" && typeof value.id === "string") {
return true;
}
}
return false;
};
const getPropertyValue = (record, field) => {
const fieldNamesStack = [];
let currField = field;
while (currField) {
fieldNamesStack.push(currField.name);
currField = currField.parent;
}
let currFieldName = fieldNamesStack.pop();
let displayValue = record.displayValues[currFieldName];
let value = record.values[currFieldName];
currFieldName = fieldNamesStack.pop();
while (currFieldName) {
if (!Value_js_1.Value.isNestedContent(value) || value.length === 0) {
throw new Error("Invalid record value");
}
if (value.length > 1) {
throw new Error("Can't create 'similar instances' for records related through many part of *-to-many relationship");
}
if (value[0].mergedFieldNames.indexOf(currFieldName) !== -1) {
throw new Error("Can't create 'similar instances' ruleset for merged values");
}
displayValue = value[0].displayValues[currFieldName];
value = value[0].values[currFieldName];
currFieldName = fieldNamesStack.pop();
}
if (!isPrimitivePropertyValue(value) || !(typeof displayValue === "undefined" || typeof displayValue === "string")) {
throw new Error("Can only create 'similar instances' ruleset for primitive values");
}
return { raw: value, display: toString(displayValue) };
};
const createInstanceFilter = (relatedInstanceSpec, propertyType, propertyName, propertyValue) => {
const alias = relatedInstanceSpec ? relatedInstanceSpec.alias : "this";
return createComparison(propertyType, `${alias}.${propertyName}`, "=", propertyValue);
};
const createComparison = (type, name, operator, value) => {
let compareValue = "";
switch (typeof value) {
case "undefined":
compareValue = "NULL";
break;
case "string":
compareValue = `"${value.replace(/"/g, `""`)}"`;
break;
case "boolean":
compareValue = value ? "TRUE" : "FALSE";
break;
case "number":
compareValue = value.toString();
break;
}
if (type.typeName === "navigation" && typeof value === "object" && core_bentley_1.Id64.isId64(value.id)) {
// note: this is temporary until we support hex ids in instance filters
compareValue = hexToDec(value.id);
}
if (type.typeName === "point2d" || type.typeName === "point3d") {
if (typeof value !== "object") {
throw new Error("Expecting point values to be supplied as objects");
}
const dimensionType = {
valueFormat: TypeDescription_js_1.PropertyValueFormat.Primitive,
typeName: "double",
};
const pointValue = value;
let comparison = `${createComparison(dimensionType, `${name}.x`, operator, pointValue.x)}`;
comparison += ` AND ${createComparison(dimensionType, `${name}.y`, operator, pointValue.y)}`;
if (type.typeName === "point3d" && pointValue.z !== undefined) {
comparison += ` AND ${createComparison(dimensionType, `${name}.z`, operator, pointValue.z)}`;
}
return comparison;
}
if (type.typeName === "double") {
return `CompareDoubles(${name}, ${compareValue}) ${operator} 0`;
}
if (type.typeName === "dateTime") {
return `CompareDateTimes(${name}, ${compareValue}) ${operator} 0`;
}
return `${name} ${operator} ${compareValue}`;
};
const createMultiClassSpecification = (classInfo) => {
const [schemaName, className] = classInfo.name.split(":");
return { schemaName, classNames: [className], arePolymorphic: true };
};
const createSingleClassSpecification = (classInfo) => {
const [schemaName, className] = classInfo.name.split(":");
return { schemaName, className };
};
const createRelatedInstanceSpec = (pathFromSelectToPropertyClass, index) => ({
spec: {
relationshipPath: pathFromSelectToPropertyClass.map((step) => ({
relationship: createSingleClassSpecification(step.relationshipInfo),
direction: step.isForwardRelationship ? RelationshipDirection_js_1.RelationshipDirection.Forward : RelationshipDirection_js_1.RelationshipDirection.Backward,
targetClass: createSingleClassSpecification(step.targetClassInfo),
})),
isRequired: true,
alias: `related_${index}`,
},
class: pathFromSelectToPropertyClass[pathFromSelectToPropertyClass.length - 1].targetClassInfo,
});
const createPathFromSelectToPropertyClass = (field) => {
let currField = field;
const pathFromPropertyToSelectClass = [];
while (currField.parent) {
pathFromPropertyToSelectClass.push(...currField.parent.pathToPrimaryClass);
currField = currField.parent;
}
return EC_js_1.RelationshipPath.reverse(pathFromPropertyToSelectClass);
};
const createRelatedInstanceSpecInfo = (field) => {
const path = createPathFromSelectToPropertyClass(field);
return path.length ? createRelatedInstanceSpec(path, 0) : undefined;
};
const hexToDec = (id) => {
const digits = [0];
for (let i = 2; i < id.length; ++i) {
let carry = parseInt(id.charAt(i), 16);
for (let j = 0; j < digits.length; ++j) {
digits[j] = digits[j] * 16 + carry;
carry = (digits[j] / 10) | 0;
digits[j] %= 10;
}
while (carry > 0) {
digits.push(carry % 10);
carry = (carry / 10) | 0;
}
}
return digits.reverse().join("");
};
//# sourceMappingURL=RulesetsFactory.js.map