@itwin/core-backend
Version:
iTwin.js backend components
439 lines • 21.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ElementSubTreeDeleter = exports.ElementTreeDeleter = exports.ElementTreeBottomUp = exports.ElementTreeWalkerScope = void 0;
exports.deleteElementTree = deleteElementTree;
exports.deleteElementSubTrees = deleteElementSubTrees;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Elements
*/
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const EditTxn_1 = require("./EditTxn");
const Element_1 = require("./Element");
const IModelDb_1 = require("./IModelDb");
const Model_1 = require("./Model");
const Symbols_1 = require("./internal/Symbols");
const loggerCategory = `${BackendLoggerCategory_1.BackendLoggerCategory.IModelDb}.ElementTreeWalker`;
function sortChildrenBeforeParents(iModel, ids) {
const children = [];
const parents = [];
for (const eid of ids) {
const parentId = iModel.elements.queryParent(eid);
if (parentId !== undefined && ids.includes(parentId))
children.push(eid);
else
parents.push(eid);
}
if (children.length === 0)
return [parents];
return [...sortChildrenBeforeParents(iModel, children), parents];
}
function isModelEmpty(iModel, modelId) {
return iModel.withQueryReader(`select count(*) from ${Element_1.Element.classFullName} where Model.Id = ?`, (reader) => {
reader.step();
return reader.current[0] === 0;
}, new core_common_1.QueryBinder().bindId(1, modelId));
}
function isDefinitionModel(model) {
return (model.id !== core_common_1.IModel.repositoryModelId) && (model instanceof Model_1.DefinitionModel);
}
var ElementPruningClassification;
(function (ElementPruningClassification) {
ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Normal"] = 0] = "PRUNING_CLASS_Normal";
ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Subject"] = 1] = "PRUNING_CLASS_Subject";
ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Definition"] = 2] = "PRUNING_CLASS_Definition";
ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_DefinitionPartition"] = 3] = "PRUNING_CLASS_DefinitionPartition";
})(ElementPruningClassification || (ElementPruningClassification = {}));
function classifyElementForPruning(iModel, elementId) {
const el = iModel.elements.getElement(elementId);
// DefinitionContainer is submodeled by a DefinitionModel and so it must be classified as PRUNING_CLASS_DefinitionPartition for tree-walking purposes.
// Since DefinitionContainer is-a DefinitionElement the (el instanceof DefinitionElement) case below would classify it as PRUNING_CLASS_Definition.
// That is why we special-case it here.
if (el instanceof Element_1.DefinitionContainer)
return ElementPruningClassification.PRUNING_CLASS_DefinitionPartition;
return (el instanceof Element_1.Subject) ? ElementPruningClassification.PRUNING_CLASS_Subject :
(el instanceof Element_1.DefinitionElement) ? ElementPruningClassification.PRUNING_CLASS_Definition :
(el instanceof Element_1.DefinitionPartition) ? ElementPruningClassification.PRUNING_CLASS_DefinitionPartition :
ElementPruningClassification.PRUNING_CLASS_Normal;
}
/** Records the path that a tree search took to reach an element or model. This object is immutable.
* @beta
*/
class ElementTreeWalkerScope {
topElement = "";
/** path of parent elements and enclosing models */
path = [];
/** cached info about the immediately enclosing model (i.e., the last model in path) */
enclosingModelInfo;
constructor(arg1, arg2) {
if (typeof arg1 === "string") {
// normal constructor
(0, core_bentley_1.assert)(arg2 instanceof Model_1.Model);
this.topElement = arg1;
this.path.push(this.enclosingModelInfo = { model: arg2, isDefinitionModel: isDefinitionModel(arg2) });
}
else if (arg1 instanceof ElementTreeWalkerScope) {
// copy-like constructor
this.topElement = arg1.topElement;
this.path = [...arg1.path];
if (typeof arg2 === "string") {
// with new parent
this.path.push(arg2);
this.enclosingModelInfo = arg1.enclosingModelInfo;
}
else {
// with new enclosing model
this.path.push(this.enclosingModelInfo = { model: arg2, isDefinitionModel: isDefinitionModel(arg2) });
}
}
else {
throw new Error("invalid constructor signature");
}
}
get enclosingModel() { return this.enclosingModelInfo.model; }
get inDefinitionModel() { return this.enclosingModelInfo.isDefinitionModel; } // NB: this will return false for the RepositoryModel!
get inRepositoryModel() { return this.enclosingModelInfo.model.id === IModelDb_1.IModelDb.repositoryModelId; }
static createTopScope(iModel, topElementId) {
const topElement = iModel.elements.getElement(topElementId);
const topElementModel = iModel.models.getModel(topElement.model);
return new ElementTreeWalkerScope(topElementId, topElementModel);
}
fmtItem(v) {
if (typeof v === "string")
return `element ${v}`;
return `model ${v.model.id} ${v.isDefinitionModel ? "(DEFN)" : ""}`;
}
toString() {
return `[ ${this.path.map((v) => this.fmtItem(v)).join(" / ")} ]`;
}
}
exports.ElementTreeWalkerScope = ElementTreeWalkerScope;
function fmtElement(iModel, elementId) {
const el = iModel.elements.getElement(elementId);
return `${el.id} ${el.classFullName} ${el.getDisplayLabel()}`;
}
function fmtModel(model) {
return `${model.id} ${model.classFullName} ${model.name}`;
}
let isTraceEnabledChecked = -1;
function isTraceEnabled() {
if (isTraceEnabledChecked === -1)
isTraceEnabledChecked = core_bentley_1.Logger.isEnabled(loggerCategory, core_bentley_1.LogLevel.Trace) ? 1 : 0;
return isTraceEnabledChecked === 1;
}
function logElement(op, iModel, elementId, scope, logChildren) {
if (!isTraceEnabled())
return;
core_bentley_1.Logger.logTrace(loggerCategory, `${op} ${fmtElement(iModel, elementId)} ${scope ? scope.toString() : ""}`);
if (logChildren)
iModel.elements.queryChildren(elementId).forEach((c) => logElement(" - ", iModel, c, undefined, true));
}
function logModel(op, iModel, modelId, scope, logElements) {
if (!isTraceEnabled())
return;
const model = iModel.models.getModel(modelId);
core_bentley_1.Logger.logTrace(loggerCategory, `${op} ${fmtModel(model)} ${scope ? scope.toString() : ""}`);
if (logElements) {
iModel.withQueryReader(`select ecinstanceid from ${Element_1.Element.classFullName} where Model.Id = ?`, (reader) => {
for (const row of reader) {
logElement(" - ", iModel, row[0]);
}
}, new core_common_1.QueryBinder().bindId(1, modelId));
}
}
/** Does a depth-first search on the tree defined by an element and its sub-models and children.
* Sub-models are visited before their modeled elements, and children are visited before their parents.
*
* The following callbacks allow the subclass to exclude elements and sub-trees from the search:
* * [[ElementTreeBottomUp.shouldExploreModel]], [[ElementTreeBottomUp.shouldExploreChildren]]
* * [[ElementTreeBottomUp.shouldVisitElement]], [[ElementTreeBottomUp.shouldVisitModel]]
*
* The [[ElementTreeBottomUp.visitElement]] and [[ElementTreeBottomUp.visitModel]] callbacks allow
* the subclass to process the elements and models that are encountered in the search.
* @beta
*/
class ElementTreeBottomUp {
txn;
constructor(iModelOrTxn) {
this.txn = iModelOrTxn instanceof EditTxn_1.EditTxn ? iModelOrTxn : iModelOrTxn[Symbols_1._implicitTxn];
}
/** Return true if the search should recurse into this model */
shouldExploreModel(_model, _scope) { return true; }
/** Return true if the search should recurse into the children (if any) of this element */
shouldExploreChildren(_parentId, _scope) { return true; }
/** Return true if the search should visit this element */
shouldVisitElement(_elementId, _scope) { return true; }
/** Return true if the search should visit this model */
shouldVisitModel(_model, _scope) { return true; }
/** The main tree-walking function */
processElementTree(element, scope) {
const subModel = this.txn.iModel.models.tryGetModel(element);
if (subModel !== undefined) {
if (this.shouldExploreModel(subModel, scope))
this._processSubModel(subModel, scope);
if (this.shouldVisitModel(subModel, scope))
this.visitModel(subModel, scope);
}
if (this.shouldExploreChildren(element, scope))
this._processChildren(element, scope);
if (this.shouldVisitElement(element, scope))
this.visitElement(element, scope);
}
/** process the children of the specified parent element */
_processChildren(parentElement, parentScope) {
const children = this.txn.iModel.elements.queryChildren(parentElement);
if (children.length === 0)
return;
const childrenScope = new ElementTreeWalkerScope(parentScope, parentElement);
for (const childElement of children)
this.processElementTree(childElement, childrenScope);
}
/** process the elements in the specified model */
_processSubModel(model, parenScope) {
const scope = new ElementTreeWalkerScope(parenScope, model);
// Visit only the top-level parents. processElementTree will visit their children (bottom-up).
model.iModel.withQueryReader(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (reader) => {
for (const row of reader) {
const elementId = row[0];
this.processElementTree(elementId, scope);
}
}, new core_common_1.QueryBinder().bindId(1, model.id));
}
}
exports.ElementTreeBottomUp = ElementTreeBottomUp;
/** Helper class that manages the deletion of definitions and subjects */
class SpecialElements {
_txn;
definitionModels = [];
definitions = [];
subjects = [];
constructor(_txn) {
this._txn = _txn;
}
getTxn(iModel) {
if (undefined !== this._txn)
return this._txn;
return iModel[Symbols_1._implicitTxn];
}
recordSpecialElement(iModel, elementId) {
// Defer Definitions and Subjects
const cls = classifyElementForPruning(iModel, elementId);
if (cls === ElementPruningClassification.PRUNING_CLASS_Subject) {
this.subjects.push(elementId);
return true;
}
else if (cls === ElementPruningClassification.PRUNING_CLASS_Definition) {
this.definitions.push(elementId);
return true;
}
else if (cls === ElementPruningClassification.PRUNING_CLASS_DefinitionPartition) {
this.definitionModels.push(elementId);
return true;
}
return false; // not a special element
}
/** Delete special elements - This calls Elements.deleteDefinitionElements to process the collected definition elements as a group and then
* calls Models.deleteModel on collected definition models. It then deletes all collected Subjects by calling Element.deleteElement on them.
* @note Caller must ensure that the special elements were recorded in a depth-first search.
*/
deleteSpecialElements(iModel) {
const txn = this.getTxn(iModel);
// It's dangerous to pass a mixture of SubCategories and Categories to deleteDefinitionElements.
// That function will delete the Categories first, which automatically deletes all their child
// SubCategories (in native code). If a SubCategory in the list is one of those children, then
// deleteDefinitionElements will try and fail with an exception to delete that SubCategory in a subsequent step.
// To work around this, we delete the SubCategories first, then everything else.
// A similar problem occurs when you pass other kinds of elements to deleteDefinitionElements, where some are
// children and others are parents. deleteDefinitionElements does not preserve the order that you specify,
// and it does not process children before parents.
for (const definitions of sortChildrenBeforeParents(iModel, this.definitions)) {
if (isTraceEnabled())
definitions.forEach((e) => logElement("try delete", iModel, e));
txn.deleteDefinitionElements(definitions); // will not delete definitions that are still in use.
}
for (const m of this.definitionModels) {
if (!isModelEmpty(iModel, m)) {
logModel("Model not empty - cannot delete - may contain Definitions that are still in use", iModel, m, undefined, true);
}
else {
logModel("delete", iModel, m);
txn.deleteModel(m);
txn.deleteElement(m);
}
}
for (const e of this.subjects) {
if (iModel.elements.queryChildren(e).length !== 0) {
logElement("Subject still has children - cannot delete - may have child DefinitionModels", iModel, e, undefined, true);
}
else {
logElement("delete", iModel, e);
txn.deleteElement(e);
}
}
}
}
/** Deletes an entire element tree, including sub-models and child elements.
* Items are deleted in bottom-up order. Definitions and Subjects are deleted after normal elements.
* Call deleteNormalElements on each tree. Then call deleteSpecialElements.
* @see deleteElementTree for a simple way to use this class.
* @beta
*/
class ElementTreeDeleter extends ElementTreeBottomUp {
_special;
constructor(iModelOrTxn) {
super(iModelOrTxn);
this._special = new SpecialElements(iModelOrTxn instanceof EditTxn_1.EditTxn ? iModelOrTxn : undefined);
}
shouldExploreModel(_model) { return true; }
shouldVisitElement(_elementId) { return true; }
visitModel(model, _scope) {
if (isDefinitionModel(model))
return; // we recorded definition models in visitElement when we encountered the DefinitionPartition elements.
// visitElement has already deleted the elements in the model. So, now it's safe to delete the model itself.
logModel("delete", this.txn.iModel, model.id, _scope);
this.txn.deleteModel(model.id);
}
visitElement(elementId, _scope) {
if (!this._special.recordSpecialElement(this.txn.iModel, elementId)) {
logElement("delete", this.txn.iModel, elementId, _scope);
this.txn.deleteElement(elementId);
}
}
/**
* Delete the "normal" elements and record the special elements for deferred processing.
* @param topElement The parent of the sub-tree to be deleted. Top element itself is also deleted.
* @param scope How the parent was found
* @see deleteSpecialElements
*/
deleteNormalElements(topElement, scope) {
const topScope = scope ?? ElementTreeWalkerScope.createTopScope(this.txn.iModel, topElement);
this.processElementTree(topElement, topScope); //
}
/** Delete all special elements that were found and deferred by deleteNormalElements. Call this
* function once after all element trees are processed by deleteNormalElements.
*/
deleteSpecialElements() {
this._special.deleteSpecialElements(this.txn.iModel);
}
}
exports.ElementTreeDeleter = ElementTreeDeleter;
/** Does a breadth-first search on the tree defined by an element and its sub-models and children.
* Parents are visited first, then children, then sub-models.
* The subclass can "prune" sub-trees from the search. When a sub-tree is "pruned" the search does *not* recurse into it.
* If a sub-tree is not pruned, then the search does recurse into it.
* @beta
*/
class ElementTreeTopDown {
_iModel;
constructor(_iModel) {
this._iModel = _iModel;
}
/** Should the search *not* recurse into this sub-tree? */
shouldPrune(_elementId, _scope) { return false; }
processElementTree(element, scope) {
if (this.shouldPrune(element, scope)) {
this.prune(element, scope);
return;
}
this._processChildren(element, scope);
const subModel = this._iModel.models.tryGetModel(element);
if (subModel !== undefined) {
this._processSubModel(subModel, scope);
}
}
_processChildren(element, scope) {
let parentScope;
for (const childElement of this._iModel.elements.queryChildren(element)) {
if (parentScope === undefined)
parentScope = new ElementTreeWalkerScope(scope, element);
this.processElementTree(childElement, parentScope);
}
}
_processSubModel(subModel, scope) {
const subModelScope = new ElementTreeWalkerScope(scope, subModel);
// Visit only the top-level parents. processElementTree will recurse into their children.
this._iModel.withQueryReader(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (reader) => {
for (const row of reader) {
const elementId = row[0];
this.processElementTree(elementId, subModelScope);
}
}, new core_common_1.QueryBinder().bindId(1, subModel.id));
}
}
/** Performs a breadth-first search to visit elements in top-down order.
* When the supplied filter function chooses an element, ElementTreeDeleter is used to delete it and its sub-tree.
* @beta
*/
class ElementSubTreeDeleter extends ElementTreeTopDown {
_treeDeleter;
_shouldPruneCb;
constructor(iModelOrTxn, shouldPruneCb) {
super(iModelOrTxn instanceof EditTxn_1.EditTxn ? iModelOrTxn.iModel : iModelOrTxn);
const effectiveTxn = iModelOrTxn instanceof EditTxn_1.EditTxn ? iModelOrTxn : iModelOrTxn[Symbols_1._implicitTxn];
this._treeDeleter = new ElementTreeDeleter(effectiveTxn);
this._shouldPruneCb = shouldPruneCb;
}
shouldPrune(elementId, scope) { return this._shouldPruneCb(elementId, scope); }
prune(elementId, scope) {
this._treeDeleter.deleteNormalElements(elementId, scope);
}
/** Traverses the tree of elements beginning with the top element, and deletes all selected sub-trees.
* Normal elements are deleted. Any special elements that are encountered are deferred.
* Call deleteSpecialElementSubTrees after all top elements have been processed. */
deleteNormalElementSubTrees(topElement, scope) {
const topScope = scope ?? ElementTreeWalkerScope.createTopScope(this._iModel, topElement);
this.processElementTree(topElement, topScope); // deletes normal elements and their sub-trees, defers special elements
}
/** Delete all special elements and their sub-trees that were found in the course of processing.
* The sub-trees were already expanded by ElementTreeDeleter.deleteNormalElements.
*/
deleteSpecialElementSubTrees() {
this._treeDeleter.deleteSpecialElements();
}
}
exports.ElementSubTreeDeleter = ElementSubTreeDeleter;
/** @internal */
function deleteElementTree(arg0, arg1, arg2) {
let maxPasses;
let txn;
let iModel;
let topElement;
if (arg0 instanceof EditTxn_1.EditTxn) {
(0, core_bentley_1.assert)(typeof arg1 === "string");
txn = arg0;
iModel = txn.iModel;
topElement = arg1;
maxPasses = arg2;
}
else if (arg0 instanceof IModelDb_1.IModelDb) {
(0, core_bentley_1.assert)(typeof arg1 === "string");
iModel = arg0;
topElement = arg1;
}
else {
iModel = arg0.iModel;
txn = "txn" in arg0 ? arg0.txn : undefined;
topElement = arg0.topElement;
maxPasses = arg0.maxPasses;
}
maxPasses = maxPasses ?? 5;
let pass = 0;
do {
const effectiveTxn = txn ?? iModel[Symbols_1._implicitTxn];
const del = new ElementTreeDeleter(effectiveTxn);
del.deleteNormalElements(topElement);
del.deleteSpecialElements();
} while ((iModel.elements.tryGetElement(topElement) !== undefined) && (++pass < maxPasses));
}
function deleteElementSubTrees(arg0, topElement, filter) {
const effectiveTxn = arg0 instanceof EditTxn_1.EditTxn ? arg0 : arg0[Symbols_1._implicitTxn];
const del = new ElementSubTreeDeleter(effectiveTxn, filter);
del.deleteNormalElementSubTrees(topElement);
del.deleteSpecialElementSubTrees();
}
//# sourceMappingURL=ElementTreeWalker.js.map