UNPKG

@itwin/presentation-backend

Version:

Backend of iTwin.js Presentation library

302 lines • 13.7 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. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Core */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RulesetEmbedder = void 0; const path = __importStar(require("path")); const semver_1 = require("semver"); const core_backend_1 = require("@itwin/core-backend"); const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const PresentationRulesDomain_js_1 = require("./domain/PresentationRulesDomain.js"); const RulesetElements = __importStar(require("./domain/RulesetElements.js")); const Utils_js_1 = require("./Utils.js"); /** * An API for embedding presentation rulesets into iModels. * @public */ class RulesetEmbedder { _imodel; _parentSubjectId; _schemaPath = path.join(core_backend_1.KnownLocations.nativeAssetsDir, "ECSchemas/Domain/PresentationRules.ecschema.xml"); _rulesetModelName = "PresentationRules"; _rulesetSubjectName = "PresentationRules"; /** * Constructs RulesetEmbedder */ constructor(props) { PresentationRulesDomain_js_1.PresentationRules.registerSchema(); this._imodel = props.imodel; this._parentSubjectId = props.parentSubjectId ?? core_common_1.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 = (0, Utils_js_1.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, core_common_1.QueryBinder.from({ rulesetId: ruleset.id }), { rowFormat: core_common_1.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: (0, Utils_js_1.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) => (0, semver_1.gte)(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 && (0, semver_1.gt)(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 && (0, semver_1.lt)(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); (0, core_bentley_1.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(core_backend_1.DefinitionPartition.createCode(this._imodel, subject.id, this._rulesetModelName)); } querySubject() { const parent = this._imodel.elements.getElement(this._parentSubjectId); const codeSpec = this._imodel.codeSpecs.getByName(core_common_1.BisCodeSpec.subject); const code = new core_common_1.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: core_backend_1.DefinitionModel.classFullName, isPrivate: true, }; return this.insertModel(modelProps, callbacks); } async insertDefinitionPartition(rulesetSubject, callbacks) { const partitionCode = core_backend_1.DefinitionPartition.createCode(this._imodel, rulesetSubject.id, this._rulesetModelName); const definitionPartitionProps = { parent: { id: rulesetSubject.id, relClassName: "BisCore:SubjectOwnsPartitionElements", }, model: rulesetSubject.model, code: partitionCode, classFullName: core_backend_1.DefinitionPartition.classFullName, }; return this.insertElement(definitionPartitionProps, callbacks); } async insertSubject(callbacks) { const parent = this._imodel.elements.getElement(this._parentSubjectId); const codeSpec = this._imodel.codeSpecs.getByName(core_common_1.BisCodeSpec.subject); const subjectCode = new core_common_1.Code({ spec: codeSpec.id, scope: parent.id, value: this._rulesetSubjectName, }); const subjectProps = { classFullName: core_backend_1.Subject.classFullName, model: parent.model, parent: { id: parent.id, relClassName: "BisCore:SubjectOwnsSubjects", }, code: subjectCode, }; return this.insertElement(subjectProps, callbacks); } async handleElementOperationPrerequisites() { let hasChanges = false; if (!this._imodel.containsClass(RulesetElements.Ruleset.classFullName)) { // import PresentationRules ECSchema await this._imodel.importSchemas([this._schemaPath]); hasChanges = true; } if (!this._imodel.codeSpecs.hasName(PresentationRulesDomain_js_1.PresentationRules.CodeSpec.Ruleset)) { // insert CodeSpec for ruleset elements this._imodel.codeSpecs.insert(core_common_1.CodeSpec.create(this._imodel, PresentationRulesDomain_js_1.PresentationRules.CodeSpec.Ruleset, core_common_1.CodeScopeSpec.Type.Model)); hasChanges = true; } if (hasChanges) { 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); } } } exports.RulesetEmbedder = RulesetEmbedder; 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