UNPKG

@itwin/core-backend

Version:
723 lines 35.1 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module iModels */ import { DbResult, Id64, IModelStatus, ITwinError, OpenMode } from "@itwin/core-bentley"; import { EcefLocation, EditTxnError, ElementError, IModelError } from "@itwin/core-common"; import { Range3d } from "@itwin/core-geometry"; import { _activeTxn, _cache, _instanceKeyCache, _nativeDb, _verifyChannel } from "./internal/Symbols"; /** * Status of a bulk element delete operation, mirroring the C++ `BulkDeleteStatus` enum. * @beta */ export var BulkDeleteElementsStatus; (function (BulkDeleteElementsStatus) { /** All supplied elements were deleted successfully. */ BulkDeleteElementsStatus[BulkDeleteElementsStatus["Success"] = 0] = "Success"; /** Some elements were deleted but others could not be, typically due to foreign key constraints on the elements not being deleted. */ BulkDeleteElementsStatus[BulkDeleteElementsStatus["PartialSuccess"] = 1] = "PartialSuccess"; /** No elements were deleted. This occurs when the SQL DELETE statement itself fails, e.g. due to a FK constraint violation that prevents the entire batch from being processed. */ BulkDeleteElementsStatus[BulkDeleteElementsStatus["DeletionFailed"] = 2] = "DeletionFailed"; })(BulkDeleteElementsStatus || (BulkDeleteElementsStatus = {})); /** * Represents an explicit editing transaction for an iModel. * * An explicit EditTxn lets callers define a deliberate unit of work by choosing when editing * starts (`start`) and how it ends (`end()` / `end("save")` or `end("abandon")`). This avoids mixing * unrelated edits into one implicit unit of work and makes save/rollback boundaries explicit. * * Explicit EditTxn instances must be active before mutating operations are performed, regardless of enforcement level. * In other words, explicit transaction behavior is independent of `implicitWriteEnforcement`. * * @see [EditTxn transaction model and migration guidance]($docs/learning/backend/EditTxn.md) * * *During indirect changes (commit processing):* Use callback args (`indirectEditTxn`) in callbacks like * [[Relationship.onRootChangedArg]] and [[Relationship.onDeletedDependencyArg]] that fire during indirect processing. * * @beta */ export class EditTxn { /** Controls how writes through the implicit transaction are handled. * * This does not relax activation requirements for explicit transactions: explicit EditTxn writes * must always come from the active EditTxn. * * - `allow`: allow implicit writes for backwards compatibility, even while an explicit EditTxn is active. * - `log`: allow implicit writes but log `implicit-txn-write-disallowed` errors. * - `throw`: reject implicit writes with `implicit-txn-write-disallowed`. * * This is initialized from [[IModelHostOptions.implicitWriteEnforcement]] during [[IModelHost.startup]]. * * Defaults to `allow` for backwards compatibility. * @beta */ static implicitWriteEnforcement = "allow"; /** The iModel this EditTxn may modify. */ iModel; /** Default description passed to [[saveChanges]] when saving this transaction. */ description; /** True if this transaction currently owns the iModel write surface. */ get isActive() { return this.iModel[_activeTxn] === this; } constructor(iModel, description) { this.iModel = iModel; this.description = description; } verifyWriteable() { // Explicit transactions must always be active before writing. if (!this.isActive) EditTxnError.throwError("not-active", "EditTxn is not active", this.iModel.key); } /** Start this EditTxn, making it the active transaction for the iModel. * @throws EditTxnError if this EditTxn is already active, another EditTxn is already active, or if unsaved changes are present. */ start() { if (this.isActive) EditTxnError.throwError("already-active", "This EditTxn is already active", this.iModel.key); const activeTxn = this.iModel[_activeTxn]; if (undefined !== activeTxn) EditTxnError.throwError("already-active", "Cannot start EditTxn while another EditTxn is active", this.iModel.key, activeTxn.description); if (this.iModel[_nativeDb].hasUnsavedChanges()) EditTxnError.throwError("unsaved-changes", "Cannot start a new EditTxn with unsaved changes", this.iModel.key); this.iModel[_activeTxn] = this; } end(mode = "save", args) { if (!this.isActive) EditTxnError.throwError("not-active", "EditTxn is not active", this.iModel.key); if (mode === "save") { this.saveChanges(args); } else { this.abandonChanges(); } this.iModel[_activeTxn] = undefined; } /** Invoked when the owning iModel is closing. * The base implementation commits unsaved changes. Subclasses may override to customize how * their changes are handled before the iModel closes. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if saving on close fails. */ onClose() { if (!this.iModel.isReadonly && this.iModel[_nativeDb].hasUnsavedChanges()) this.saveChanges(); } /** Abandon database changes while keeping this EditTxn active. * @throws EditTxnError if this EditTxn is not active. */ abandonChanges() { this.verifyWriteable(); this.iModel.clearCaches({ instanceCachesOnly: true }); this.iModel[_nativeDb].abandonChanges(); } /** Save changes with additional arguments. * @param args Save changes arguments. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if the iModel is readonly, if indirect changes are active, or if the native save fails. */ saveChanges(args) { this.verifyWriteable(); const iModel = this.iModel; if (iModel.openMode === OpenMode.Readonly) throw new IModelError(IModelStatus.ReadOnly, "IModelDb was opened read-only"); if (iModel.isBriefcaseDb() && iModel.txns.isIndirectChanges) throw new IModelError(IModelStatus.BadRequest, "Cannot save changes while in an indirect change scope"); args ??= this.description; const saveArgs = typeof args === "string" ? { description: args } : args; const stat = iModel[_nativeDb].saveChanges(JSON.stringify(saveArgs)); if (DbResult.BE_SQLITE_ERROR_PropagateChangesFailed === stat) throw new IModelError(stat, "Could not save changes due to propagation failure."); if (DbResult.BE_SQLITE_OK !== stat) throw new IModelError(stat, `Could not save changes (${saveArgs.description})`); } /** Insert a new element into the iModel. * @param elProps The properties of the new element. * @returns The newly inserted element's Id. * @throws EditTxnError if this EditTxn is not active. * @throws [[ITwinError]] if insertion fails. */ insertElement(elProps, options) { this.verifyWriteable(); try { this.iModel.elements[_cache].delete({ id: elProps.id, federationGuid: elProps.federationGuid, code: elProps.code, }); return elProps.id = this.iModel[_nativeDb].insertElement(elProps, options); } catch (err) { err.message = `Error inserting element [${err.message}]`; err.metadata = { elProps }; throw err; } } /** Update an existing element in the iModel. * @param elProps The properties to update. * @throws EditTxnError if this EditTxn is not active. * @throws [[ITwinError]] if update fails. */ updateElement(elProps) { this.verifyWriteable(); try { if (elProps.id) { this.iModel.elements[_instanceKeyCache].deleteById(elProps.id); } else { this.iModel.elements[_instanceKeyCache].delete({ federationGuid: elProps.federationGuid, code: elProps.code, }); } this.iModel.elements[_cache].delete({ id: elProps.id, federationGuid: elProps.federationGuid, code: elProps.code, }); this.iModel[_nativeDb].updateElement(elProps); } catch (err) { err.message = `Error updating element [${err.message}], id: ${elProps.id}`; err.metadata = { elProps }; throw err; } } /** Delete elements from the iModel. * @param ids The Ids of the elements to delete. * @throws EditTxnError if this EditTxn is not active. * @throws [[ITwinError]] if deletion fails. */ deleteElement(ids) { this.verifyWriteable(); const iModel = this.iModel; Id64.toIdSet(ids).forEach((id) => { try { this.iModel.elements[_cache].delete({ id }); this.iModel.elements[_instanceKeyCache].deleteById(id); iModel[_nativeDb].deleteElement(id); } catch (err) { err.message = `Error deleting element [${err.message}], id: ${id}`; err.metadata = { elementId: id }; throw err; } }); } /** Change the parent of an element within its model. * * The new parent must be in the same model as the element. Cross-model reparenting is not allowed; * use [[changeElementModel]] only to move root elements between models. * Only the target element is reparented — its children and their model membership are unaffected. * * **Blocked cases** (will throw): * - The new parent is in a different model than the element. * - Element has a `ParentElement`-scoped code (code uniqueness is tied to the parent; use delete+insert instead). * * **Allowed cases**: * - Element has a `Repository`-scoped code (unique across entire iModel — unaffected by the parent change). * - Element has a `RelatedElement`-scoped code (scope element is independent of the parent). * - Element has a `Model`-scoped code (the model does not change, so the code remains valid). * - Element has no meaningful code (empty code). * * Channel verification is performed on the element's model. * Lock enforcement: requires an exclusive lock on the element, and a shared lock on the new parent. * @param props The reparent parameters: element id and new parent id. * @throws EditTxnError if this EditTxn is not active. * @throws [[ITwinError]] if the operation fails. * @beta */ changeElementParent(props) { this.verifyWriteable(); const iModel = this.iModel; // Lock enforcement: exclusive lock on the element being reparented, shared lock on the new parent. iModel.locks.checkExclusiveLock(props.id, "element", "changeParent"); iModel.locks.checkSharedLock(props.parentId, "parent", "changeParent"); // The new parent must be in the same model as the element. Cross-model reparenting is not // allowed here; changeElementModel only moves root elements between models. Check this up // front so consumers get a clear error instead of the addon's lower-level "wrong model" status. const sourceModelId = iModel.elements.getElementProps({ id: props.id }).model; const parentModelId = iModel.elements.getElementProps({ id: props.parentId }).model; if (sourceModelId !== parentModelId) ElementError.throwError("invalid-arguments", `cannot reparent element '${props.id}' to a parent in a different model ('${parentModelId}' != '${sourceModelId}'); changeElementModel only moves root elements between models`); // Channel verification on the element's model. iModel.channels[_verifyChannel](sourceModelId); // Invalidate caches for the element being reparented. iModel.elements[_cache].delete({ id: props.id }); iModel.elements[_instanceKeyCache].deleteById(props.id); try { iModel[_nativeDb].changeElementParent({ id: props.id, parentId: props.parentId }); } catch (err) { err.message = `Error changing element parent [${err.message}], id: ${props.id}, parentId: ${props.parentId}`; err.metadata = { props }; throw err; } // The model is unchanged and descendants are not moved, so only the reparented element's cache is stale. iModel.elements[_cache].delete({ id: props.id }); iModel.elements[_instanceKeyCache].deleteById(props.id); } /** Change the model of a root element, making it a root element in the new model. * * The element must not have a parent. * The element's entire subtree moves with it: BIS requires a parent and all of its children to reside * in the same model, so every descendant of the element is relocated into the target model as well. * The parent-child hierarchy is preserved. The whole subtree is validated before anything is moved, so * a rejected change leaves the iModel untouched. * * **Blocked cases** (will throw): * - Element has a parent (only root elements can be moved between models). * - Any element in the subtree has a `Model`-scoped code (code uniqueness is tied to the source model; use delete+insert instead). * - The moved (root) element has a `ParentElement`-scoped code (use delete+insert instead). A descendant's `ParentElement`-scoped code is allowed, because its parent moves with it. * * **Allowed cases** (for any element in the subtree): * - A `Repository`-scoped code (unique across entire iModel — unaffected by the model change). * - A `RelatedElement`-scoped code (scope element is independent of the model). * - No meaningful code (empty code). * * The source and target models must be of the same class (classFullName must match exactly). * Channel verification is performed on both the source and target models. * Lock enforcement: requires an exclusive lock on every element in the moved subtree, and a shared lock on the target model. * @param props The model change parameters: element id and target model id. * @throws EditTxnError if this EditTxn is not active. * @throws [[ITwinError]] if the operation fails. * @beta */ changeElementModel(props) { this.verifyWriteable(); const iModel = this.iModel; // Resolve the source model const sourceModelId = iModel.elements.getElementProps({ id: props.id }).model; // Channel verification on the source model iModel.channels[_verifyChannel](sourceModelId); // Model type check: source and target models must be the same class const sourceModel = iModel.models.getModel(sourceModelId); const targetModel = iModel.models.getModel(props.modelId); if (sourceModel.classFullName !== targetModel.classFullName) ElementError.throwError("model-type-mismatch", `cannot move element from model of type '${sourceModel.classFullName}' to model of type '${targetModel.classFullName}'`); // Shared lock on target model iModel.locks.checkSharedLock(props.modelId, "model", "changeModel"); // Lock enforcement: every element in the subtree is updated by the native bulk move. const subtreeIds = this.collectSubtreeIds(props.id); for (const id of subtreeIds) iModel.locks.checkExclusiveLock(id, "element", "changeModel"); // Channel verification on the target model iModel.channels[_verifyChannel](props.modelId); // Invalidate caches iModel.elements[_cache].delete({ id: props.id }); iModel.elements[_instanceKeyCache].deleteById(props.id); try { iModel[_nativeDb].changeElementModel({ id: props.id, modelId: props.modelId }); } catch (err) { err.message = `Error changing element model [${err.message}], id: ${props.id}, modelId: ${props.modelId}`; err.metadata = { props }; throw err; } // The move relocates the element's entire subtree into the target model (BIS requires a parent and // its children to reside in the same model). Every descendant's cached props therefore hold a stale // `model`, so invalidate the whole subtree rather than just the target element. for (const id of subtreeIds) { iModel.elements[_cache].delete({ id }); iModel.elements[_instanceKeyCache].deleteById(id); } // Moving the subtree changes the membership - and therefore the geometry-derived state such as // GeometricModel.geometryGuid - of both the source and target models. Element insert/update/delete // invalidate models[_cache] via Element.onInserted/onUpdated/onDeleted for the same reason, but the // native changeElementModel does not fire those element callbacks, so invalidate both affected models // here to keep the model cache contract local to this wrapper. iModel.models[_cache].delete(sourceModelId); iModel.models[_cache].delete(props.modelId); } /** Collect an element together with all of its descendants by walking the `ElementOwnsChildElements` * hierarchy depth-first. Used to invalidate the cached props of every element affected by a subtree move. */ collectSubtreeIds(rootId) { const ids = []; const stack = [rootId]; for (let id = stack.pop(); undefined !== id; id = stack.pop()) { ids.push(id); for (const childId of this.iModel.elements.queryChildren(id)) stack.push(childId); } return ids; } /** * Delete multiple elements from the iModel. * @param ids The ids of the elements to delete. All ids must be well-formed and valid [[Id64String]]s. * @param deleteOptions Options for the delete operation. * @returns A result object containing information about the deletion operation success and the element ids that failed to delete (if any). * @throws [[ITwinError]] if any of the supplied ids are not well-formed/valid [[Id64String]]s. * @beta */ deleteElements(ids, deleteOptions) { this.verifyWriteable(); const invalidIds = new Set(); for (const id of ids) { if (!Id64.isValidId64(id)) invalidIds.add(id); } if (invalidIds.size > 0) ITwinError.throwError({ message: `Invalid element ids: ${Array.from(invalidIds).join(", ")}`, iTwinErrorId: { scope: "imodel", key: "invalid-arguments" } }); const bulkDeletionResult = this.iModel[_nativeDb].deleteElements(ids, deleteOptions); const finalResult = { ...bulkDeletionResult, failedIds: Id64.toIdSet(bulkDeletionResult.failedIds) }; if (finalResult.status === BulkDeleteElementsStatus.DeletionFailed) return finalResult; for (const id of ids) { if (!finalResult.failedIds.has(id)) { this.iModel.elements[_cache].delete({ id }); this.iModel.elements[_instanceKeyCache].deleteById(id); } } return finalResult; } /** Insert a new aspect into the iModel. * @param aspectProps The properties of the new aspect. * @returns The newly inserted aspect Id. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if insertion fails. */ insertAspect(aspectProps) { this.verifyWriteable(); try { return this.iModel[_nativeDb].insertElementAspect(aspectProps); } catch (err) { const error = new IModelError(err.errorNumber, `Error inserting ElementAspect [${err.message}], class: ${aspectProps.classFullName}`, aspectProps); error.cause = err; throw error; } } /** Update an existing aspect in the iModel. * @param aspectProps The properties of the aspect to update. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if update fails. */ updateAspect(aspectProps) { this.verifyWriteable(); try { this.iModel[_nativeDb].updateElementAspect(aspectProps); } catch (err) { const error = new IModelError(err.errorNumber, `Error updating ElementAspect [${err.message}], id: ${aspectProps.id}`, aspectProps); error.cause = err; throw error; } } /** Delete one or more aspects from the iModel. * @param aspectInstanceIds The Ids of the aspects to delete. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if deletion fails. */ deleteAspect(aspectInstanceIds) { this.verifyWriteable(); Id64.toIdSet(aspectInstanceIds).forEach((aspectInstanceId) => { try { this.iModel[_nativeDb].deleteElementAspect(aspectInstanceId); } catch (err) { const error = new IModelError(err.errorNumber, `Error deleting ElementAspect [${err.message}], id: ${aspectInstanceId}`); error.cause = err; throw error; } }); } /** Delete definition elements from the iModel when they are not referenced. * @param definitionElementIds The Ids of the definition elements to attempt to delete. * @returns The set of definition elements that were still in use and therefore not deleted. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if usage queries fail. */ deleteDefinitionElements(definitionElementIds) { this.verifyWriteable(); const usageInfo = this.iModel[_nativeDb].queryDefinitionElementUsage(definitionElementIds); if (!usageInfo) throw new IModelError(IModelStatus.BadRequest, "Error querying for DefinitionElement usage"); const usedIdSet = usageInfo.usedIds ? Id64.toIdSet(usageInfo.usedIds) : new Set(); const deleteIfUnused = (ids, used) => { ids?.forEach((id) => { if (!used.has(id)) this.deleteElement(id); }); }; try { this.iModel[_nativeDb].beginPurgeOperation(); deleteIfUnused(usageInfo.spatialCategoryIds, usedIdSet); deleteIfUnused(usageInfo.drawingCategoryIds, usedIdSet); deleteIfUnused(usageInfo.viewDefinitionIds, usedIdSet); deleteIfUnused(usageInfo.geometryPartIds, usedIdSet); deleteIfUnused(usageInfo.lineStyleIds, usedIdSet); deleteIfUnused(usageInfo.renderMaterialIds, usedIdSet); deleteIfUnused(usageInfo.subCategoryIds, usedIdSet); deleteIfUnused(usageInfo.textureIds, usedIdSet); deleteIfUnused(usageInfo.displayStyleIds, usedIdSet); deleteIfUnused(usageInfo.categorySelectorIds, usedIdSet); deleteIfUnused(usageInfo.modelSelectorIds, usedIdSet); if (usageInfo.otherDefinitionElementIds) this.deleteElement(usageInfo.otherDefinitionElementIds); } finally { this.iModel[_nativeDb].endPurgeOperation(); } if (usageInfo.viewDefinitionIds) { // Recheck view-related definitions after deleting view definitions that may have been their last reference. let viewRelatedIds = []; if (usageInfo.displayStyleIds) viewRelatedIds = viewRelatedIds.concat(usageInfo.displayStyleIds.filter((id) => usedIdSet.has(id))); if (usageInfo.categorySelectorIds) viewRelatedIds = viewRelatedIds.concat(usageInfo.categorySelectorIds.filter((id) => usedIdSet.has(id))); if (usageInfo.modelSelectorIds) viewRelatedIds = viewRelatedIds.concat(usageInfo.modelSelectorIds.filter((id) => usedIdSet.has(id))); if (viewRelatedIds.length > 0) { const viewRelatedUsageInfo = this.iModel[_nativeDb].queryDefinitionElementUsage(viewRelatedIds); if (viewRelatedUsageInfo) { const usedViewRelatedIdSet = viewRelatedUsageInfo.usedIds ? Id64.toIdSet(viewRelatedUsageInfo.usedIds) : new Set(); try { this.iModel[_nativeDb].beginPurgeOperation(); deleteIfUnused(viewRelatedUsageInfo.displayStyleIds, usedViewRelatedIdSet); deleteIfUnused(viewRelatedUsageInfo.categorySelectorIds, usedViewRelatedIdSet); deleteIfUnused(viewRelatedUsageInfo.modelSelectorIds, usedViewRelatedIdSet); } finally { this.iModel[_nativeDb].endPurgeOperation(); } viewRelatedIds.forEach((id) => { if (!usedViewRelatedIdSet.has(id)) usedIdSet.delete(id); }); } } } return usedIdSet; } /** Insert a new model into the iModel. * @param props The data for the new model. * @returns The newly inserted model's Id. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if insertion fails. */ insertModel(props) { this.verifyWriteable(); try { return props.id = this.iModel[_nativeDb].insertModel(props); } catch (err) { const error = new IModelError(err.errorNumber, `Error inserting model [${err.message}], class=${props.classFullName}`); error.cause = err; throw error; } } /** Update an existing model in the iModel. * @param props the properties of the model to change * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if update fails. */ updateModel(props) { this.verifyWriteable(); try { if (props.id) this.iModel.models[_cache].delete(props.id); this.iModel[_nativeDb].updateModel(props); } catch (err) { const error = new IModelError(err.errorNumber, `Error updating model [${err.message}], id: ${props.id}`); error.cause = err; throw error; } } /** Update the geometry guid of a model. * @param modelId The Id of the model to update. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if the update fails. */ updateGeometryGuid(modelId) { this.verifyWriteable(); this.iModel.models[_cache].delete(modelId); const error = this.iModel[_nativeDb].updateModelGeometryGuid(modelId); if (error !== IModelStatus.Success) throw new IModelError(error, `Error updating geometry guid for model ${modelId}`); } /** Delete models from the iModel. * @param ids The Ids of the models to delete. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if deletion fails. */ deleteModel(ids) { this.verifyWriteable(); Id64.toIdSet(ids).forEach((id) => { try { this.iModel.models[_cache].delete(id); this.iModel.models[_instanceKeyCache].deleteById(id); this.iModel[_nativeDb].deleteModel(id); } catch (err) { const error = new IModelError(err.errorNumber, `Error deleting model [${err.message}], id: ${id}`); error.cause = err; throw error; } }); } /** Insert a new relationship into the iModel. * @param props The properties of the new relationship. * @returns The Id of the newly inserted relationship. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if the class is invalid for link-table insertion. */ insertRelationship(props) { this.verifyWriteable(); if (!this.iModel[_nativeDb].isLinkTableRelationship(props.classFullName.replace(".", ":"))) throw new IModelError(DbResult.BE_SQLITE_ERROR, `Class '${props.classFullName}' must be a relationship class and it should be subclass of BisCore:ElementRefersToElements or BisCore:ElementDrivesElement.`); return props.id = this.iModel[_nativeDb].insertLinkTableRelationship(props); } /** Update an existing relationship in the iModel. * @param props the properties of the relationship to update. * @throws EditTxnError if this EditTxn is not active. */ updateRelationship(props) { this.verifyWriteable(); this.iModel[_nativeDb].updateLinkTableRelationship(props); } /** Delete a relationship from the iModel. * @param props The properties of the relationship to delete. * @throws EditTxnError if this EditTxn is not active. */ deleteRelationship(props) { this.verifyWriteable(); this.iModel[_nativeDb].deleteLinkTableRelationship(props); } /** Delete multiple relationships from the iModel. * @param props The properties of the relationships to delete. * @throws EditTxnError if this EditTxn is not active. */ deleteRelationships(props) { this.verifyWriteable(); this.iModel[_nativeDb].deleteLinkTableRelationships(props); } /** Save a file property to the iModel. * @param prop The file property to save. * @param strValue String value. * @param blobVal Blob value. * @throws EditTxnError if this EditTxn is not active. */ saveFileProperty(prop, strValue, blobVal) { this.verifyWriteable(); const imodel = this.iModel; if (imodel.isBriefcaseDb()) { if (imodel.txns.isIndirectChanges) { throw new IModelError(IModelStatus.BadRequest, "Cannot save file property while in an indirect change scope"); } } imodel[_nativeDb].saveFileProperty(prop, strValue, blobVal); } /** Delete a file property from the iModel. * @param prop The file property to delete. * @throws EditTxnError if this EditTxn is not active. */ deleteFileProperty(prop) { this.saveFileProperty(prop, undefined, undefined); } /** Update the project extents of the iModel. * @param newExtents The new project extents. * @throws EditTxnError if this EditTxn is not active. * @throws IModelError if extents are invalid. */ updateProjectExtents(newExtents) { this.verifyWriteable(); const extents = Range3d.fromJSON(newExtents); if (extents.isNull) throw new IModelError(DbResult.BE_SQLITE_ERROR, "Invalid project extents"); this.iModel.projectExtents = extents; this.updateIModelProps(); } /** Update the ECEF location of the iModel. * @param ecef The new ECEF location. * @throws EditTxnError if this EditTxn is not active. */ updateEcefLocation(ecef) { this.verifyWriteable(); this.iModel.setEcefLocation(new EcefLocation(ecef)); this.updateIModelProps(); } /** Update the iModel props in the database from the current in-memory state. * @throws EditTxnError if this EditTxn is not active. */ updateIModelProps() { this.verifyWriteable(); this.iModel[_nativeDb].updateIModelProps(this.iModel.toJSON()); } static _settingPropNamespace = "settings"; static _viewStoreProperty = { namespace: "itwinjs", name: "DefaultViewStore" }; /** Save a `SettingDictionary` in this iModel. * @param name The name for the SettingDictionary. If a dictionary by that name already exists, its value is replaced. * @param dict The SettingDictionary object to stringify and save. * @throws EditTxnError if this EditTxn is not active. * @beta */ saveSettingDictionary(name, dict) { this.verifyWriteable(); this.iModel.withSqliteStatement("REPLACE INTO be_Prop(id,SubId,TxnMode,Namespace,Name,strData) VALUES(0,0,0,?,?,?)", (stmt) => { stmt.bindString(1, EditTxn._settingPropNamespace); stmt.bindString(2, name); stmt.bindString(3, JSON.stringify(dict)); stmt.stepForWrite(); }); this.saveChanges("add settings"); } /** Delete a SettingDictionary from this iModel. * @param name The name of the dictionary to delete. * @throws EditTxnError if this EditTxn is not active. * @beta */ deleteSettingDictionary(name) { this.verifyWriteable(); this.iModel.withSqliteStatement("DELETE FROM be_Prop WHERE Namespace=? AND Name=?", (stmt) => { stmt.bindString(1, EditTxn._settingPropNamespace); stmt.bindString(2, name); stmt.stepForWrite(); }); this.saveChanges("delete settings"); } /** Save a default ViewStore container reference in this iModel. * @param arg The cloud container properties for the ViewStore. * @throws EditTxnError if this EditTxn is not active. * @beta */ saveDefaultViewStore(arg) { this.verifyWriteable(); const props = { baseUri: arg.baseUri, containerId: arg.containerId, storageType: arg.storageType }; // sanitize to only known properties this.saveFileProperty(EditTxn._viewStoreProperty, JSON.stringify(props)); this.saveChanges("update default ViewStore"); } } export function withEditTxn(iModel, saveArgsOrFn, maybeFn) { const saveArgs = "function" === typeof saveArgsOrFn ? undefined : saveArgsOrFn; const fn = "function" === typeof saveArgsOrFn ? saveArgsOrFn : maybeFn; if (undefined === fn) throw new Error("withEditTxn requires a callback"); const txn = new EditTxn(iModel, ""); txn.start(); try { const result = fn(txn); if (result instanceof Promise) { return result.then((value) => { txn.end("save", saveArgs); return value; }, (err) => { if (txn.isActive) txn.end("abandon"); throw err; }); } txn.end("save", saveArgs); return result; } catch (err) { if (txn.isActive) txn.end("abandon"); throw err; } } //# sourceMappingURL=EditTxn.js.map