@itwin/core-backend
Version:
iTwin.js backend components
728 lines • 36.4 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.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EditTxn = exports.BulkDeleteElementsStatus = void 0;
exports.withEditTxn = withEditTxn;
/** @packageDocumentation
* @module iModels
*/
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const core_geometry_1 = require("@itwin/core-geometry");
const Symbols_1 = require("./internal/Symbols");
/**
* Status of a bulk element delete operation, mirroring the C++ `BulkDeleteStatus` enum.
* @beta
*/
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 || (exports.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
*/
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[Symbols_1._activeTxn] === this;
}
constructor(iModel, description) {
this.iModel = iModel;
this.description = description;
}
verifyWriteable() {
// Explicit transactions must always be active before writing.
if (!this.isActive)
core_common_1.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)
core_common_1.EditTxnError.throwError("already-active", "This EditTxn is already active", this.iModel.key);
const activeTxn = this.iModel[Symbols_1._activeTxn];
if (undefined !== activeTxn)
core_common_1.EditTxnError.throwError("already-active", "Cannot start EditTxn while another EditTxn is active", this.iModel.key, activeTxn.description);
if (this.iModel[Symbols_1._nativeDb].hasUnsavedChanges())
core_common_1.EditTxnError.throwError("unsaved-changes", "Cannot start a new EditTxn with unsaved changes", this.iModel.key);
this.iModel[Symbols_1._activeTxn] = this;
}
end(mode = "save", args) {
if (!this.isActive)
core_common_1.EditTxnError.throwError("not-active", "EditTxn is not active", this.iModel.key);
if (mode === "save") {
this.saveChanges(args);
}
else {
this.abandonChanges();
}
this.iModel[Symbols_1._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[Symbols_1._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[Symbols_1._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 === core_bentley_1.OpenMode.Readonly)
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.ReadOnly, "IModelDb was opened read-only");
if (iModel.isBriefcaseDb() && iModel.txns.isIndirectChanges)
throw new core_common_1.IModelError(core_bentley_1.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[Symbols_1._nativeDb].saveChanges(JSON.stringify(saveArgs));
if (core_bentley_1.DbResult.BE_SQLITE_ERROR_PropagateChangesFailed === stat)
throw new core_common_1.IModelError(stat, "Could not save changes due to propagation failure.");
if (core_bentley_1.DbResult.BE_SQLITE_OK !== stat)
throw new core_common_1.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[Symbols_1._cache].delete({
id: elProps.id,
federationGuid: elProps.federationGuid,
code: elProps.code,
});
return elProps.id = this.iModel[Symbols_1._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[Symbols_1._instanceKeyCache].deleteById(elProps.id);
}
else {
this.iModel.elements[Symbols_1._instanceKeyCache].delete({
federationGuid: elProps.federationGuid,
code: elProps.code,
});
}
this.iModel.elements[Symbols_1._cache].delete({
id: elProps.id,
federationGuid: elProps.federationGuid,
code: elProps.code,
});
this.iModel[Symbols_1._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;
core_bentley_1.Id64.toIdSet(ids).forEach((id) => {
try {
this.iModel.elements[Symbols_1._cache].delete({ id });
this.iModel.elements[Symbols_1._instanceKeyCache].deleteById(id);
iModel[Symbols_1._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)
core_common_1.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[Symbols_1._verifyChannel](sourceModelId);
// Invalidate caches for the element being reparented.
iModel.elements[Symbols_1._cache].delete({ id: props.id });
iModel.elements[Symbols_1._instanceKeyCache].deleteById(props.id);
try {
iModel[Symbols_1._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[Symbols_1._cache].delete({ id: props.id });
iModel.elements[Symbols_1._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[Symbols_1._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)
core_common_1.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[Symbols_1._verifyChannel](props.modelId);
// Invalidate caches
iModel.elements[Symbols_1._cache].delete({ id: props.id });
iModel.elements[Symbols_1._instanceKeyCache].deleteById(props.id);
try {
iModel[Symbols_1._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[Symbols_1._cache].delete({ id });
iModel.elements[Symbols_1._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[Symbols_1._cache].delete(sourceModelId);
iModel.models[Symbols_1._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 (!core_bentley_1.Id64.isValidId64(id))
invalidIds.add(id);
}
if (invalidIds.size > 0)
core_bentley_1.ITwinError.throwError({ message: `Invalid element ids: ${Array.from(invalidIds).join(", ")}`, iTwinErrorId: { scope: "imodel", key: "invalid-arguments" } });
const bulkDeletionResult = this.iModel[Symbols_1._nativeDb].deleteElements(ids, deleteOptions);
const finalResult = { ...bulkDeletionResult, failedIds: core_bentley_1.Id64.toIdSet(bulkDeletionResult.failedIds) };
if (finalResult.status === BulkDeleteElementsStatus.DeletionFailed)
return finalResult;
for (const id of ids) {
if (!finalResult.failedIds.has(id)) {
this.iModel.elements[Symbols_1._cache].delete({ id });
this.iModel.elements[Symbols_1._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[Symbols_1._nativeDb].insertElementAspect(aspectProps);
}
catch (err) {
const error = new core_common_1.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[Symbols_1._nativeDb].updateElementAspect(aspectProps);
}
catch (err) {
const error = new core_common_1.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();
core_bentley_1.Id64.toIdSet(aspectInstanceIds).forEach((aspectInstanceId) => {
try {
this.iModel[Symbols_1._nativeDb].deleteElementAspect(aspectInstanceId);
}
catch (err) {
const error = new core_common_1.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[Symbols_1._nativeDb].queryDefinitionElementUsage(definitionElementIds);
if (!usageInfo)
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Error querying for DefinitionElement usage");
const usedIdSet = usageInfo.usedIds ? core_bentley_1.Id64.toIdSet(usageInfo.usedIds) : new Set();
const deleteIfUnused = (ids, used) => {
ids?.forEach((id) => {
if (!used.has(id))
this.deleteElement(id);
});
};
try {
this.iModel[Symbols_1._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[Symbols_1._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[Symbols_1._nativeDb].queryDefinitionElementUsage(viewRelatedIds);
if (viewRelatedUsageInfo) {
const usedViewRelatedIdSet = viewRelatedUsageInfo.usedIds ? core_bentley_1.Id64.toIdSet(viewRelatedUsageInfo.usedIds) : new Set();
try {
this.iModel[Symbols_1._nativeDb].beginPurgeOperation();
deleteIfUnused(viewRelatedUsageInfo.displayStyleIds, usedViewRelatedIdSet);
deleteIfUnused(viewRelatedUsageInfo.categorySelectorIds, usedViewRelatedIdSet);
deleteIfUnused(viewRelatedUsageInfo.modelSelectorIds, usedViewRelatedIdSet);
}
finally {
this.iModel[Symbols_1._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[Symbols_1._nativeDb].insertModel(props);
}
catch (err) {
const error = new core_common_1.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[Symbols_1._cache].delete(props.id);
this.iModel[Symbols_1._nativeDb].updateModel(props);
}
catch (err) {
const error = new core_common_1.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[Symbols_1._cache].delete(modelId);
const error = this.iModel[Symbols_1._nativeDb].updateModelGeometryGuid(modelId);
if (error !== core_bentley_1.IModelStatus.Success)
throw new core_common_1.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();
core_bentley_1.Id64.toIdSet(ids).forEach((id) => {
try {
this.iModel.models[Symbols_1._cache].delete(id);
this.iModel.models[Symbols_1._instanceKeyCache].deleteById(id);
this.iModel[Symbols_1._nativeDb].deleteModel(id);
}
catch (err) {
const error = new core_common_1.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[Symbols_1._nativeDb].isLinkTableRelationship(props.classFullName.replace(".", ":")))
throw new core_common_1.IModelError(core_bentley_1.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[Symbols_1._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[Symbols_1._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[Symbols_1._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[Symbols_1._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 core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot save file property while in an indirect change scope");
}
}
imodel[Symbols_1._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 = core_geometry_1.Range3d.fromJSON(newExtents);
if (extents.isNull)
throw new core_common_1.IModelError(core_bentley_1.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 core_common_1.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[Symbols_1._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");
}
}
exports.EditTxn = EditTxn;
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