UNPKG

@itwin/presentation-backend

Version:

Backend of iTwin.js Presentation library

269 lines • 12 kB
/*--------------------------------------------------------------------------------------------- * 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 */ import * as path from "path"; import { gt as versionGt, gte as versionGte, lt as versionLt } from "semver"; import { DefinitionModel, DefinitionPartition, KnownLocations, Subject } from "@itwin/core-backend"; import { assert } from "@itwin/core-bentley"; import { BisCodeSpec, Code, CodeScopeSpec, CodeSpec, IModel, QueryBinder, QueryRowFormat, } from "@itwin/core-common"; import { PresentationRules } from "./domain/PresentationRulesDomain.js"; import * as RulesetElements from "./domain/RulesetElements.js"; import { normalizeVersion } from "./Utils.js"; /** * An API for embedding presentation rulesets into iModels. * @public */ export class RulesetEmbedder { _imodel; _parentSubjectId; _schemaPath = path.join(KnownLocations.nativeAssetsDir, "ECSchemas/Domain/PresentationRules.ecschema.xml"); _rulesetModelName = "PresentationRules"; _rulesetSubjectName = "PresentationRules"; /** * Constructs RulesetEmbedder */ constructor(props) { PresentationRules.registerSchema(); this._imodel = props.imodel; this._parentSubjectId = props.parentSubjectId ?? IModel.rootSubjectId; } /** * Inserts a ruleset into iModel. * @param ruleset Ruleset to insert. * @param options Options for inserting a ruleset. * @returns ID of inserted ruleset element or, if insertion was skipped, ID of existing ruleset with the same ID and highest version. */ async insertRuleset(ruleset, options) { const normalizedOptions = normalizeRulesetInsertOptions(options); const rulesetVersion = normalizeVersion(ruleset.version); // ensure imodel has PresentationRules schema and required CodeSpecs await this.handleElementOperationPrerequisites(); // find all rulesets with the same ID const rulesetsWithSameId = []; const query = ` SELECT ECInstanceId, JsonProperties FROM ${RulesetElements.Ruleset.schema.name}.${RulesetElements.Ruleset.className} WHERE json_extract(JsonProperties, '$.jsonProperties.id') = :rulesetId`; const reader = this._imodel.createQueryReader(query, QueryBinder.from({ rulesetId: ruleset.id }), { rowFormat: QueryRowFormat.UseJsPropertyNames }); while (await reader.step()) { const row = reader.current.toRow(); const existingRulesetElementId = row.id; const existingRuleset = JSON.parse(row.jsonProperties).jsonProperties; rulesetsWithSameId.push({ id: existingRulesetElementId, ruleset: existingRuleset, normalizedVersion: normalizeVersion(existingRuleset.version), }); } // check if we need to do anything at all 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) { // we're not inserting anything - return ID of the ruleset element with the highest version 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; } // if requested, delete existing rulesets const rulesetsToRemove = []; const shouldRemove = (_, normalizedVersion) => { 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); // attempt to update ruleset with same ID and version const exactMatch = rulesetsWithSameId.find((curr) => curr.normalizedVersion === rulesetVersion); if (exactMatch !== undefined) { return this.updateRuleset(exactMatch.id, ruleset, normalizedOptions.onEntityUpdate); } // no exact match found - insert a new ruleset element 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); } async updateRuleset(elementId, ruleset, callbacks) { const existingRulesetElement = this._imodel.elements.tryGetElement(elementId); assert(existingRulesetElement !== undefined); existingRulesetElement.jsonProperties.jsonProperties = ruleset; await this.updateElement(existingRulesetElement, callbacks); this._imodel.saveChanges(); return existingRulesetElement.id; } 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. */ async getRulesets() { if (!this._imodel.containsClass(RulesetElements.Ruleset.classFullName)) { return []; } const rulesetList = []; for await (const row of this._imodel.createQueryReader(`SELECT ECInstanceId AS id FROM ${RulesetElements.Ruleset.classFullName}`)) { const rulesetElement = this._imodel.elements.getElement({ id: row.id }); const ruleset = rulesetElement.jsonProperties.jsonProperties; rulesetList.push(ruleset); } return rulesetList; } 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); } queryRulesetModel() { const definitionPartition = this.queryDefinitionPartition(); if (undefined === definitionPartition) { return undefined; } return this._imodel.models.getSubModel(definitionPartition.id); } queryDefinitionPartition() { const subject = this.querySubject(); if (undefined === subject) { return undefined; } return this._imodel.elements.tryGetElement(DefinitionPartition.createCode(this._imodel, subject.id, this._rulesetModelName)); } querySubject() { const parent = this._imodel.elements.getElement(this._parentSubjectId); const codeSpec = this._imodel.codeSpecs.getByName(BisCodeSpec.subject); const code = new Code({ spec: codeSpec.id, scope: parent.id, value: this._rulesetSubjectName, }); return this._imodel.elements.tryGetElement(code); } async insertDefinitionModel(definitionPartition, callbacks) { const modelProps = { modeledElement: definitionPartition, name: this._rulesetModelName, classFullName: DefinitionModel.classFullName, isPrivate: true, }; return this.insertModel(modelProps, callbacks); } 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); } async insertSubject(callbacks) { const parent = this._imodel.elements.getElement(this._parentSubjectId); const codeSpec = this._imodel.codeSpecs.getByName(BisCodeSpec.subject); const subjectCode = new Code({ spec: codeSpec.id, scope: parent.id, value: this._rulesetSubjectName, }); const subjectProps = { classFullName: Subject.classFullName, model: parent.model, parent: { id: parent.id, relClassName: "BisCore:SubjectOwnsSubjects", }, code: subjectCode, }; return this.insertElement(subjectProps, callbacks); } async handleElementOperationPrerequisites() { if (this._imodel.containsClass(RulesetElements.Ruleset.classFullName)) { return; } // import PresentationRules ECSchema await this._imodel.importSchemas([this._schemaPath]); // insert CodeSpec for ruleset elements this._imodel.codeSpecs.insert(CodeSpec.create(this._imodel, PresentationRules.CodeSpec.Ruleset, CodeScopeSpec.Type.Model)); this._imodel.saveChanges(); } async insertElement(props, callbacks) { const element = this._imodel.elements.createElement(props); /* c8 ignore next */ await callbacks?.onBeforeInsert(element); try { return this._imodel.elements.getElement(element.insert()); } finally { /* c8 ignore next */ await callbacks?.onAfterInsert(element); } } async insertModel(props, callbacks) { const model = this._imodel.models.createModel(props); /* c8 ignore next */ await callbacks?.onBeforeInsert(model); try { model.id = model.insert(); return model; } finally { /* c8 ignore next */ await callbacks?.onAfterInsert(model); } } async updateElement(element, callbacks) { /* c8 ignore next */ await callbacks?.onBeforeUpdate(element); try { element.update(); } finally { /* c8 ignore next */ 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, }; } //# sourceMappingURL=RulesetEmbedder.js.map