packageDocumentation module import { Guid , Id64 , Id64String } from "@itwin/core-bentley" ; import { Field , PropertiesField } from "./content/Fields" ; import { Item } from "./content/Item" ; import { PrimitiveTypeDescription , PropertyValueFormat } from "./content/TypeDescription" ; import { DisplayValue , Value } from "./content/Value" ; import { ClassInfo , InstanceKey , RelationshipPath } from "./EC" ; import { MultiSchemaClassesSpecification , SingleSchemaClassSpecification } from "./rules/ClassSpecifications" ; import { ContentSpecificationTypes } from "./rules/content/ContentSpecification" ; import { RelatedInstanceSpecification } from "./rules/RelatedInstanceSpecification" ; import { RelationshipDirection } from "./rules/RelationshipDirection" ; import { RuleTypes } from "./rules/Rule" ; import { Ruleset } from "./rules/Ruleset" ; public export class RulesetsFactory { private createSimilarInstancesRulesetInfo ( field , record ) { if ( ! field . isPropertiesField ( ) ) throw new Error ( "Can only create 'similar instances' ruleset for properties-based records" ) ; if ( field . type . valueFormat !== 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 }/${ Guid . createValue ( ) }` , rules : [ ] } ; ruleset . rules . push ( { ruleType : RuleTypes . Content , specifications : [ { specType : ContentSpecificationTypes . ContentInstancesOfSpecificClasses , classes : createMultiClassSpecification ( record . classInfo ) , handleInstancesPolymorphically : true , relatedInstances : relatedInstanceInfo ? [ relatedInstanceInfo . spec ] : [ ] , instanceFilter : createInstanceFilter ( relatedInstanceInfo ?. spec , field . type , propertyName , propertyValue . raw ) } ] } ) ; return { ruleset , relatedClass , propertyName , propertyValue } ; } param field param record param computeDisplayValue public 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 } ; } } public export type ComputeDisplayValueCallback = ( type : string , value : PrimitivePropertyValue , displayValue : string ) => Promise < string > ; interface PrimitiveValueDef { raw : PrimitivePropertyValue ; display : string ; } public export type PrimitivePropertyValue = string | number | boolean | Point | InstanceKey | undefined ; type Point = { x : number ; y : number ; z ? : number ; } ; const toString = ( displayValue ) : string => { if ( ! displayValue ) return "NULL" ; return displayValue . toString ( ) ; } ; const createDescription = ( record , relatedClass , field , value ) : string => { const classInfo = relatedClass ?? record . classInfo ! ; return `[${ classInfo . label }].[${ field . label }] = ${ value }` ; } ; const createDescriptionAsync = async ( record , relatedClass , field , value , computeDisplayValue ? ) : Promise < string > => { 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 ) : value is PrimitivePropertyValue => { if ( Value . isPrimitive ( value ) ) { return true ; } if ( 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 ) : PrimitiveValueDef => { 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 . 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 ) ) throw new Error ( "Can only create 'similar instances' ruleset for primitive values" ) ; return { raw : value , display : toString ( displayValue ) } ; } ; const createInstanceFilter = ( relatedInstanceSpec , propertyType , propertyName , propertyValue ) : string => { const alias = relatedInstanceSpec ? relatedInstanceSpec . alias : "this" ; return createComparison ( propertyType , `${ alias }.${ propertyName }` , "=" , propertyValue ) ; } ; type Operator = "=" | "!=" | ">" | ">=" | "<" | "<=" ; const createComparison = ( type , name , operator , value ) : string => { 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" && Id64 . isId64 ( ( value as InstanceKey ) . id ) ) { compareValue = hexToDec ( ( value as InstanceKey ) . 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 : PropertyValueFormat . Primitive , typeName : "double" } ; const pointValue = value as Point ; 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 ) : MultiSchemaClassesSpecification => { const [ schemaName , className ] = classInfo . name . split ( ":" ) ; return { schemaName , classNames : [ className ] } ; } ; const createSingleClassSpecification = ( classInfo ) : SingleSchemaClassSpecification => { const [ schemaName , className ] = classInfo . name . split ( ":" ) ; return { schemaName , className } ; } ; const createRelatedInstanceSpec = ( pathFromSelectToPropertyClass , index ) : { spec : RelatedInstanceSpecification ; class : ClassInfo ; } => ( { spec : { relationshipPath : pathFromSelectToPropertyClass . map ( ( step ) => ( { relationship : createSingleClassSpecification ( step . relationshipInfo ) , direction : step . isForwardRelationship ? RelationshipDirection . Forward : RelationshipDirection . Backward , targetClass : createSingleClassSpecification ( step . targetClassInfo ) } ) ) , isRequired : true , alias : `related_${ index }` } , class : pathFromSelectToPropertyClass [ pathFromSelectToPropertyClass . length - 1 ] . targetClassInfo } ) ; const createPathFromSelectToPropertyClass = ( field ) : RelationshipPath => { let currField = field ; const pathFromPropertyToSelectClass = [ ] ; while ( currField . parent ) { pathFromPropertyToSelectClass . push ( ... currField . parent . pathToPrimaryClass ) ; currField = currField . parent ; } return RelationshipPath . reverse ( pathFromPropertyToSelectClass ) ; } ; const createRelatedInstanceSpecInfo = ( field ) : { spec : RelatedInstanceSpecification ; class : ClassInfo ; } | undefined => { 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 ( "" ) ; } ;