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