@itwin/presentation-backend
Version:
Backend of iTwin.js Presentation library
302 lines • 13.7 kB
JavaScript
"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