packageDocumentation module import * as path from "path" ; import { gt as versionGt , gte as versionGte , lt as versionLt } from "semver" ; import { DefinitionElement , DefinitionModel , DefinitionPartition , ECSqlStatement , Element , Entity , IModelDb , KnownLocations , Model , Subject , } from "@itwin/core-backend" ; import { assert , DbResult , Id64String } from "@itwin/core-bentley" ; import { BisCodeSpec , Code , CodeScopeSpec , CodeSpec , DefinitionElementProps , ElementProps , InformationPartitionElementProps , ModelProps , QueryBinder , QueryRowFormat , SubjectProps , } from "@itwin/core-common" ; import { Ruleset } from "@itwin/presentation-common" ; import { PresentationRules } from "./domain/PresentationRulesDomain" ; import * as RulesetElements from "./domain/RulesetElements" ; import { normalizeVersion } from "./Utils" ; beta interface UpdateCallbacks { onBeforeUpdate : ( props : Entity ) => Promise < void > ; onAfterUpdate : ( props : Entity ) => Promise < void > ; } beta interface InsertCallbacks { onBeforeInsert : ( props : Entity ) => Promise < void > ; onAfterInsert : ( props : Entity ) => Promise < void > ; } beta export interface RulesetInsertOptions { /**
     * When should insertion be skipped:
     * - `same-id` - if iModel already contains a ruleset with the same id and **any** version
     * - `same-id-and-version-eq` - if iModel already contains a ruleset with same id and version
     * - `same-id-and-version-gte` - if iModel already contains a ruleset with same id and
     * version greater or equal to version of the inserted ruleset.
     *
     * Defaults to `same-id-and-version-eq`.
     */ skip ? : "never" | "same-id" | "same-id-and-version-eq" | "same-id-and-version-gte" ; /**
     * Which existing versions of rulesets with same id should be replaced when we insert a new one:
     * - `all` - replace all rulesets with same id.
     * - `all-lower` - replace rulesets with same id and version lower than the version of inserted ruleset.
     * - `exact` - replace only the ruleset whose id and version matches the inserted ruleset.
     *
     * Defaults to `exact`.
     */ replaceVersions ? : "all" | "all-lower" | "exact" ; beta onEntityUpdate ? : UpdateCallbacks ; beta onEntityInsert ? : InsertCallbacks ; } public export interface RulesetEmbedderProps { /** iModel to embed rulesets to */ imodel : IModelDb ; } beta export class RulesetEmbedder { private _imodel : IModelDb ; private readonly _schemaPath = path . join ( KnownLocations . nativeAssetsDir , "ECSchemas/Domain/PresentationRules.ecschema.xml" ) ; private readonly _rulesetModelName = "PresentationRules" ; private readonly _rulesetSubjectName = "PresentationRules" ; /**
     * Constructs RulesetEmbedder
     */ public constructor ( props ) { PresentationRules . registerSchema ( ) ; this . _imodel = props . imodel ; } param ruleset param options returns public async insertRuleset ( ruleset , options ? ) { const normalizedOptions = normalizeRulesetInsertOptions ( options ) ; const rulesetVersion = normalizeVersion ( ruleset . version ) ; await this . handleElementOperationPrerequisites ( ) ; const rulesetsWithSameId = [ ] ; const query = `
      SELECT ECInstanceId, JsonProperties
      FROM ${ RulesetElements . Ruleset . schema . name }.${ RulesetElements . Ruleset . className }
      WHERE json_extract(JsonProperties, '$.jsonProperties.id') = :rulesetId` ; for await ( const row of this . _imodel . query ( query , QueryBinder . from ( { rulesetId : ruleset . id } ) , { rowFormat : QueryRowFormat . UseJsPropertyNames } ) ) { const existingRulesetElementId = row . id ; const existingRuleset = JSON . parse ( row . jsonProperties ) . jsonProperties ; rulesetsWithSameId . push ( { id : existingRulesetElementId , ruleset : existingRuleset , normalizedVersion : normalizeVersion ( existingRuleset . version ) } ) ; } const shouldSkip = normalizedOptions . skip === "same-id" && rulesetsWithSameId . length > 0 || normalizedOptions . skip === "same-id-and-version-eq" && rulesetsWithSameId . some ( ( entry ) => entry . normalizedVersion === rulesetVersion ) || normalizedOptions . skip === "same-id-and-version-gte" && rulesetsWithSameId . some ( ( entry ) => versionGte ( entry . normalizedVersion , rulesetVersion ) ) ; if ( shouldSkip ) { const rulesetEntryWithHighestVersion = rulesetsWithSameId . reduce ( ( highest , curr ) => { if ( ! highest . ruleset . version || curr . ruleset . version && versionGt ( curr . ruleset . version , highest . ruleset . version ) ) return curr ; return highest ; } , rulesetsWithSameId [ 0 ] ) ; return rulesetEntryWithHighestVersion . id ; } const rulesetsToRemove = [ ] ; const shouldRemove = ( _ , normalizedVersion ) : boolean => { switch ( normalizedOptions . replaceVersions ) { case "all" : return normalizedVersion !== rulesetVersion ; case "all-lower" : return normalizedVersion !== rulesetVersion && versionLt ( normalizedVersion , rulesetVersion ) ; } return false ; } ; rulesetsWithSameId . forEach ( ( entry ) => { if ( shouldRemove ( entry . ruleset , entry . normalizedVersion ) ) rulesetsToRemove . push ( entry . id ) ; } ) ; this . _imodel . elements . deleteElement ( rulesetsToRemove ) ; const exactMatch = rulesetsWithSameId . find ( ( curr ) => curr . normalizedVersion === rulesetVersion ) ; if ( exactMatch !== undefined ) { return this . updateRuleset ( exactMatch . id , ruleset , normalizedOptions . onEntityUpdate ) ; } const model = await this . getOrCreateRulesetModel ( normalizedOptions . onEntityInsert ) ; const rulesetCode = RulesetElements . Ruleset . createRulesetCode ( this . _imodel , model . id , ruleset ) ; return this . insertNewRuleset ( ruleset , model , rulesetCode , normalizedOptions . onEntityInsert ) ; } private async updateRuleset ( elementId , ruleset , callbacks ? ) { const existingRulesetElement = this . _imodel . elements . tryGetElement < DefinitionElement > ( elementId ) ; assert ( existingRulesetElement !== undefined ) ; existingRulesetElement . jsonProperties . jsonProperties = ruleset ; await this . updateElement ( existingRulesetElement , callbacks ) ; this . _imodel . saveChanges ( ) ; return existingRulesetElement . id ; } private async insertNewRuleset ( ruleset , model , rulesetCode , callbacks ? ) { const props = { model : model . id , code : rulesetCode , classFullName : RulesetElements . Ruleset . classFullName , jsonProperties : { jsonProperties : ruleset } } ; const element = await this . insertElement ( props , callbacks ) ; this . _imodel . saveChanges ( ) ; return element . id ; } /**
     * Get all rulesets embedded in the iModel.
     */ public async getRulesets ( ) { if ( ! this . _imodel . containsClass ( RulesetElements . Ruleset . classFullName ) ) return [ ] ; const rulesetList = [ ] ; this . _imodel . withPreparedStatement ( `SELECT ECInstanceId AS id FROM ${ RulesetElements . Ruleset . classFullName }` , ( statement ) => { while ( DbResult . BE_SQLITE_ROW === statement . step ( ) ) { const row = statement . getRow ( ) ; const rulesetElement = this . _imodel . elements . getElement ( { id : row . id } ) ; const ruleset = rulesetElement . jsonProperties . jsonProperties ; rulesetList . push ( ruleset ) ; } } ) ; return rulesetList ; } private async getOrCreateRulesetModel ( callbacks ? ) { const rulesetModel = this . queryRulesetModel ( ) ; if ( undefined !== rulesetModel ) return rulesetModel ; const rulesetSubject = await this . insertSubject ( callbacks ) ; const definitionPartition = await this . insertDefinitionPartition ( rulesetSubject , callbacks ) ; return this . insertDefinitionModel ( definitionPartition , callbacks ) ; } private queryRulesetModel ( ) { const definitionPartition = this . queryDefinitionPartition ( ) ; if ( undefined === definitionPartition ) return undefined ; return this . _imodel . models . getSubModel ( definitionPartition . id ) ; } private queryDefinitionPartition ( ) { const subject = this . querySubject ( ) ; if ( undefined === subject ) return undefined ; return this . _imodel . elements . tryGetElement < DefinitionPartition > ( DefinitionPartition . createCode ( this . _imodel , subject . id , this . _rulesetModelName ) ) ; } private querySubject ( ) { const root = this . _imodel . elements . getRootSubject ( ) ; const codeSpec = this . _imodel . codeSpecs . getByName ( BisCodeSpec . subject ) ; const code = new Code ( { spec : codeSpec . id , scope : root . id , value : this . _rulesetSubjectName } ) ; return this . _imodel . elements . tryGetElement < DefinitionPartition > ( code ) ; } private async insertDefinitionModel ( definitionPartition , callbacks ? ) { const modelProps = { modeledElement : definitionPartition , name : this . _rulesetModelName , classFullName : DefinitionModel . classFullName , isPrivate : true } ; return this . insertModel ( modelProps , callbacks ) ; } private async insertDefinitionPartition ( rulesetSubject , callbacks ? ) { const partitionCode = DefinitionPartition . createCode ( this . _imodel , rulesetSubject . id , this . _rulesetModelName ) ; const definitionPartitionProps = { parent : { id : rulesetSubject . id , relClassName : "BisCore:SubjectOwnsPartitionElements" } , model : rulesetSubject . model , code : partitionCode , classFullName : DefinitionPartition . classFullName } ; return this . insertElement ( definitionPartitionProps , callbacks ) ; } private async insertSubject ( callbacks ? ) { const root = this . _imodel . elements . getRootSubject ( ) ; const codeSpec = this . _imodel . codeSpecs . getByName ( BisCodeSpec . subject ) ; const subjectCode = new Code ( { spec : codeSpec . id , scope : root . id , value : this . _rulesetSubjectName } ) ; const subjectProps = { classFullName : Subject . classFullName , model : root . model , parent : { id : root . id , relClassName : "BisCore:SubjectOwnsSubjects" } , code : subjectCode } ; return this . insertElement ( subjectProps , callbacks ) ; } private async handleElementOperationPrerequisites ( ) { if ( this . _imodel . containsClass ( RulesetElements . Ruleset . classFullName ) ) return ; await this . _imodel . importSchemas ( [ this . _schemaPath ] ) ; this . _imodel . codeSpecs . insert ( CodeSpec . create ( this . _imodel , PresentationRules . CodeSpec . Ruleset , CodeScopeSpec . Type . Model ) ) ; this . _imodel . saveChanges ( ) ; } private async insertElement < TProps extends ElementProps > ( props , callbacks ? ) { const element = this . _imodel . elements . createElement ( props ) ; await callbacks ?. onBeforeInsert ( element ) ; try { return this . _imodel . elements . getElement ( element . insert ( ) ) ; } finally { await callbacks ?. onAfterInsert ( element ) ; } } private async insertModel ( props , callbacks ? ) { const model = this . _imodel . models . createModel ( props ) ; await callbacks ?. onBeforeInsert ( model ) ; try { model . id = model . insert ( ) ; return model ; } finally { await callbacks ?. onAfterInsert ( model ) ; } } private async updateElement ( element , callbacks ? ) { await callbacks ?. onBeforeUpdate ( element ) ; try { element . update ( ) ; } finally { await callbacks ?. onAfterUpdate ( element ) ; } } } function normalizeRulesetInsertOptions ( options ? ) { if ( options === undefined ) return { skip : "same-id-and-version-eq" , replaceVersions : "exact" } ; return { skip : options . skip ?? "same-id-and-version-eq" , replaceVersions : options . replaceVersions ?? "exact" , onEntityUpdate : options . onEntityUpdate , onEntityInsert : options . onEntityInsert } ; }