UNPKG

@itwin/core-backend

Version:
434 lines • 21.5 kB
"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 Element_1 = require("./Element"); const IModelDb_1 = require("./IModelDb"); const Model_1 = require("./Model"); 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) { // eslint-disable-next-line @typescript-eslint/no-deprecated return iModel.withPreparedStatement(`select count(*) from ${Element_1.Element.classFullName} where Model.Id = ?`, (stmt) => { stmt.bindId(1, modelId); stmt.step(); return stmt.getValue(0).getInteger() === 0; }); } 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) { // eslint-disable-next-line @typescript-eslint/no-deprecated iModel.withPreparedStatement(`select ecinstanceid from ${Element_1.Element.classFullName} where Model.Id = ?`, (stmt) => { stmt.bindId(1, modelId); while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) { logElement(" - ", iModel, stmt.getValue(0).getId()); } }); } } /** 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 { _iModel; constructor(_iModel) { this._iModel = _iModel; } /** 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._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._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). // eslint-disable-next-line @typescript-eslint/no-deprecated model.iModel.withPreparedStatement(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (stmt) => { stmt.bindId(1, model.id); while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) { const elementId = stmt.getValue(0).getId(); this.processElementTree(elementId, scope); } }); } } exports.ElementTreeBottomUp = ElementTreeBottomUp; /** Helper class that manages the deletion of definitions and subjects */ class SpecialElements { definitionModels = []; definitions = []; subjects = []; 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) { // 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)); iModel.elements.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); iModel.models.deleteModel(m); iModel.elements.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); iModel.elements.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 = new SpecialElements(); 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._iModel, model.id, _scope); model.delete(); } visitElement(elementId, _scope) { if (!this._special.recordSpecialElement(this._iModel, elementId)) { logElement("delete", this._iModel, elementId, _scope); this._iModel.elements.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._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._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. // eslint-disable-next-line @typescript-eslint/no-deprecated this._iModel.withPreparedStatement(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (stmt) => { stmt.bindId(1, subModel.id); while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) { const elementId = stmt.getValue(0).getId(); this.processElementTree(elementId, subModelScope); } }); } } /** 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; /** Construct an ElementSubTreeDeleter. * @param iModel The iModel * @param topElement Where to start the search. * @param shouldPruneCb Callback that selects sub-trees that should be deleted. * @see deleteElementSubTrees for a simple way to use this class. */ constructor(iModel, shouldPruneCb) { super(iModel); this._treeDeleter = new ElementTreeDeleter(this._iModel); 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) { let maxPasses; let iModel; let topElement; if (arg0 instanceof IModelDb_1.IModelDb) { (0, core_bentley_1.assert)(typeof arg1 === "string"); iModel = arg0; topElement = arg1; } else { iModel = arg0.iModel; topElement = arg0.topElement; maxPasses = arg0.maxPasses; } maxPasses = maxPasses ?? 5; let pass = 0; do { const del = new ElementTreeDeleter(iModel); del.deleteNormalElements(topElement); del.deleteSpecialElements(); } while ((iModel.elements.tryGetElement(topElement) !== undefined) && (++pass < maxPasses)); } /** Deletes all element sub-trees that are selected by the supplied filter. Uses ElementSubTreeDeleter. * If the filter selects the top element itself, then the entire tree (including the top element) is deleted. * That has the same effect as calling [[deleteElementTree]] on the top element. * @note The caller may have to call this function multiple times if there are multiple layers of dependencies among definition elements. * @param iModel The iModel * @param topElement Where to start the search. * @param filter Callback that selects sub-trees that should be deleted. * @beta */ function deleteElementSubTrees(iModel, topElement, filter) { const del = new ElementSubTreeDeleter(iModel, filter); del.deleteNormalElementSubTrees(topElement); del.deleteSpecialElementSubTrees(); } //# sourceMappingURL=ElementTreeWalker.js.map