UNPKG

@itwin/core-backend

Version:
669 lines • 75.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { assert } from "chai"; import * as path from "path"; import { Id64 } from "@itwin/core-bentley"; import { Point3d } from "@itwin/core-geometry"; import { Code, CodeScopeSpec, ImageSourceFormat, IModel, QueryBinder, QueryRowFormat, SubCategoryAppearance } from "@itwin/core-common"; import { EditTxn } from "../../EditTxn"; import { CategorySelector, ChannelControl, DefinitionContainer, DefinitionModel, DisplayStyle2d, DisplayStyle3d, DocumentListModel, Drawing, DrawingCategory, DrawingGraphic, DrawingViewDefinition, GenericGraphicalType2d, GenericPhysicalType, GeometryPart, GraphicalElement2dIsOfType, IModelJsFs, InformationPartitionElement, ModelSelector, OrthographicViewDefinition, PhysicalElementIsOfType, PhysicalModel, PhysicalPartition, RenderMaterialElement, SnapshotDb, SpatialCategory, SubCategory, Subject, Texture, } from "../../core-backend"; import { ExtensiveTestScenario, IModelTestUtils } from "../IModelTestUtils"; import { KnownTestLocations } from "../KnownTestLocations"; import { withEditTxn } from "../TestEditTxn"; describe("DeleteDefinitionElements", () => { const outputDir = path.join(KnownTestLocations.outputDir, "DeleteDefinitionElements"); let seedDb; let iModelDb; let subjectId; let definitionModelId; let physicalModelId; let spatialCategoryId; let subCategoryId; let spatialCategorySelectorId; let modelSelectorId; let displayStyle3dId; let viewId; let drawingCategoryId; let drawingCategorySelectorId; let displayStyle2dId; let drawingViewId; let geometryPartId; let renderMaterialId; let textureId; let physicalObjectId1; let physicalObjectId2; let physicalObjectId3; before(async () => { if (!IModelJsFs.existsSync(KnownTestLocations.outputDir)) { IModelJsFs.mkdirSync(KnownTestLocations.outputDir); } if (!IModelJsFs.existsSync(outputDir)) { IModelJsFs.mkdirSync(outputDir); } const seedDbFile = IModelTestUtils.prepareOutputFile("DeleteDefinitionElements", "seed.bim"); if (IModelJsFs.existsSync(seedDbFile)) { IModelJsFs.removeSync(seedDbFile); } seedDb = SnapshotDb.createEmpty(seedDbFile, { rootSubject: { name: "DeleteDefinitionElements" } }); await ExtensiveTestScenario.prepareDb(seedDb); await ExtensiveTestScenario.populateDb(seedDb); // Get ElementIds of DefinitionElements created by populateDb subjectId = seedDb.elements.queryElementIdByCode(Subject.createCode(seedDb, IModel.rootSubjectId, "Subject")); definitionModelId = seedDb.elements.queryElementIdByCode(InformationPartitionElement.createCode(seedDb, subjectId, "Definition")); physicalModelId = seedDb.elements.queryElementIdByCode(PhysicalPartition.createCode(seedDb, subjectId, "Physical")); spatialCategoryId = seedDb.elements.queryElementIdByCode(SpatialCategory.createCode(seedDb, definitionModelId, "SpatialCategory")); subCategoryId = seedDb.elements.queryElementIdByCode(SubCategory.createCode(seedDb, spatialCategoryId, "SubCategory")); spatialCategorySelectorId = seedDb.elements.queryElementIdByCode(CategorySelector.createCode(seedDb, definitionModelId, "SpatialCategories")); modelSelectorId = seedDb.elements.queryElementIdByCode(ModelSelector.createCode(seedDb, definitionModelId, "SpatialModels")); displayStyle3dId = seedDb.elements.queryElementIdByCode(DisplayStyle3d.createCode(seedDb, definitionModelId, "DisplayStyle3d")); viewId = seedDb.elements.queryElementIdByCode(OrthographicViewDefinition.createCode(seedDb, definitionModelId, "Orthographic View")); drawingCategoryId = seedDb.elements.queryElementIdByCode(DrawingCategory.createCode(seedDb, definitionModelId, "DrawingCategory")); drawingCategorySelectorId = seedDb.elements.queryElementIdByCode(CategorySelector.createCode(seedDb, definitionModelId, "DrawingCategories")); displayStyle2dId = seedDb.elements.queryElementIdByCode(DisplayStyle2d.createCode(seedDb, definitionModelId, "DisplayStyle2d")); drawingViewId = seedDb.elements.queryElementIdByCode(DrawingViewDefinition.createCode(seedDb, definitionModelId, "Drawing View")); geometryPartId = seedDb.elements.queryElementIdByCode(GeometryPart.createCode(seedDb, definitionModelId, "GeometryPart")); renderMaterialId = seedDb.elements.queryElementIdByCode(RenderMaterialElement.createCode(seedDb, definitionModelId, "RenderMaterial")); textureId = seedDb.elements.queryElementIdByCode(Texture.createCode(seedDb, definitionModelId, "Texture")); physicalObjectId1 = IModelTestUtils.queryByUserLabel(seedDb, "PhysicalObject1"); physicalObjectId2 = IModelTestUtils.queryByUserLabel(seedDb, "PhysicalObject2"); physicalObjectId3 = IModelTestUtils.queryByUserLabel(seedDb, "PhysicalObject3"); assert.isTrue(Id64.isValidId64(physicalModelId)); assert.isTrue(Id64.isValidId64(spatialCategoryId)); assert.isTrue(Id64.isValidId64(subCategoryId)); assert.isTrue(Id64.isValidId64(spatialCategorySelectorId)); assert.isTrue(Id64.isValidId64(modelSelectorId)); assert.isTrue(Id64.isValidId64(displayStyle3dId)); assert.isTrue(Id64.isValidId64(viewId)); assert.isTrue(Id64.isValidId64(drawingCategoryId)); assert.isTrue(Id64.isValidId64(drawingCategorySelectorId)); assert.isTrue(Id64.isValidId64(displayStyle2dId)); assert.isTrue(Id64.isValidId64(drawingViewId)); assert.isTrue(Id64.isValidId64(geometryPartId)); assert.isTrue(Id64.isValidId64(renderMaterialId)); assert.isTrue(Id64.isValidId64(textureId)); assert.isTrue(Id64.isValidId64(physicalObjectId1)); assert.isTrue(Id64.isValidId64(physicalObjectId2)); assert.isTrue(Id64.isValidId64(physicalObjectId3)); withEditTxn(seedDb, "save changes", () => { }); }); beforeEach(async () => { iModelDb = SnapshotDb.createFrom(seedDb, IModelTestUtils.prepareOutputFile("DeleteDefinitionElements", "DeleteDefinitionElements.bim")); assert.isTrue(iModelDb.isOpen); iModelDb.channels.addAllowedChannel(ChannelControl.sharedChannelName); }); afterEach(() => { if (iModelDb.isOpen) iModelDb.close(); }); after(() => { if (seedDb.isOpen) seedDb.close(); }); it("should delete if not used", async () => { const txn = new EditTxn(iModelDb, "delete definition elements"); txn.start(); let usedDefinitionElementIds; const expectDeleteToThrow = (elementId) => assert.throws(() => txn.deleteElement(elementId)); try { // make sure deleteDefinitionElements skips Elements that are not DefinitionElements usedDefinitionElementIds = txn.deleteDefinitionElements([physicalObjectId1, physicalObjectId2, physicalObjectId3, subjectId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isDefined(iModelDb.elements.tryGetElement(physicalObjectId1)); assert.isDefined(iModelDb.elements.tryGetElement(physicalObjectId2)); assert.isDefined(iModelDb.elements.tryGetElement(physicalObjectId3)); assert.isDefined(iModelDb.elements.tryGetElement(subjectId)); // make sure deleteDefinitionElements skips invalid Ids usedDefinitionElementIds = txn.deleteDefinitionElements([Id64.invalid, Id64.invalid]); assert.equal(usedDefinitionElementIds.size, 0); // delete/deleteDefinitionElements for a used GeometryPart should fail expectDeleteToThrow(geometryPartId); usedDefinitionElementIds = txn.deleteDefinitionElements([geometryPartId]); assert.isTrue(usedDefinitionElementIds.has(geometryPartId)); assert.isDefined(iModelDb.elements.tryGetElement(geometryPartId)); // deleteDefinitionElements for an unused GeometryPart should succeed, delete should still fail const unusedGeometryPartProps = { classFullName: GeometryPart.classFullName, model: definitionModelId, code: GeometryPart.createCode(iModelDb, definitionModelId, "Unused GeometryPart"), geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)), }; const unusedGeometryPartId = txn.insertElement(unusedGeometryPartProps); assert.isTrue(Id64.isValidId64(unusedGeometryPartId)); expectDeleteToThrow(unusedGeometryPartId); usedDefinitionElementIds = txn.deleteDefinitionElements([unusedGeometryPartId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedGeometryPartId)); // delete/deleteDefinitionElements for a used RenderMaterial should fail expectDeleteToThrow(renderMaterialId); usedDefinitionElementIds = txn.deleteDefinitionElements([renderMaterialId]); assert.isTrue(usedDefinitionElementIds.has(renderMaterialId)); assert.isDefined(iModelDb.elements.tryGetElement(renderMaterialId)); // deleteDefinitionElements for an unused RenderMaterial should succeed, delete should still fail const unusedRenderMaterial = RenderMaterialElement.create(iModelDb, definitionModelId, "Unused RenderMaterial", { paletteName: "PaletteName" }); const unusedRenderMaterialId = txn.insertElement(unusedRenderMaterial.toJSON()); assert.isTrue(Id64.isValidId64(unusedRenderMaterialId)); expectDeleteToThrow(unusedRenderMaterialId); usedDefinitionElementIds = txn.deleteDefinitionElements([unusedRenderMaterialId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedRenderMaterialId)); // delete/deleteDefinitionElements for a used Texture should fail expectDeleteToThrow(textureId); usedDefinitionElementIds = txn.deleteDefinitionElements([textureId]); assert.isTrue(usedDefinitionElementIds.has(textureId)); assert.isDefined(iModelDb.elements.tryGetElement(textureId)); // deleteDefinitionElements for an unused Texture should succeed, delete should still fail const textureData = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 3, 0, 0, 0, 3, 8, 2, 0, 0, 0, 217, 74, 34, 232, 0, 0, 0, 1, 115, 82, 71, 66, 0, 174, 206, 28, 233, 0, 0, 0, 4, 103, 65, 77, 65, 0, 0, 177, 143, 11, 252, 97, 5, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 14, 195, 0, 0, 14, 195, 1, 199, 111, 168, 100, 0, 0, 0, 24, 73, 68, 65, 84, 24, 87, 99, 248, 15, 4, 12, 12, 64, 4, 198, 64, 46, 132, 5, 162, 254, 51, 0, 0, 195, 90, 10, 246, 127, 175, 154, 145, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]).toString("base64"); const unusedTexture = Texture.createTexture(iModelDb, definitionModelId, "Unused Texture", ImageSourceFormat.Png, textureData, "Description for Unused Texture"); const unusedTextureId = txn.insertElement(unusedTexture.toJSON()); assert.isTrue(Id64.isValidId64(unusedTextureId)); expectDeleteToThrow(unusedTextureId); usedDefinitionElementIds = txn.deleteDefinitionElements([unusedTextureId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedTextureId)); // delete/deleteDefinitionElements for a used SpatialCategory should fail expectDeleteToThrow(spatialCategoryId); usedDefinitionElementIds = txn.deleteDefinitionElements([spatialCategoryId]); assert.isTrue(usedDefinitionElementIds.has(spatialCategoryId)); assert.isDefined(iModelDb.elements.tryGetElement(spatialCategoryId)); // delete/deleteDefinitionElements for a default SubCategory should fail const spatialCategory = iModelDb.elements.getElement(spatialCategoryId, SpatialCategory); const defaultSpatialSubCategoryId = spatialCategory.myDefaultSubCategoryId(); expectDeleteToThrow(defaultSpatialSubCategoryId); usedDefinitionElementIds = txn.deleteDefinitionElements([defaultSpatialSubCategoryId]); assert.isTrue(usedDefinitionElementIds.has(defaultSpatialSubCategoryId)); assert.isDefined(iModelDb.elements.tryGetElement(defaultSpatialSubCategoryId)); // delete/deleteDefinitionElements for a used, non-default SubCategory should fail expectDeleteToThrow(subCategoryId); usedDefinitionElementIds = txn.deleteDefinitionElements([subCategoryId]); assert.isTrue(usedDefinitionElementIds.has(subCategoryId)); assert.isDefined(iModelDb.elements.tryGetElement(subCategoryId)); // deleteDefinitionElements for an unused SubCategory should succeed, delete should still fail const unusedSubCategory = SubCategory.create(iModelDb, spatialCategoryId, "Unused SubCategory", {}); const unusedSubCategoryId = txn.insertElement(unusedSubCategory.toJSON()); assert.isTrue(Id64.isValidId64(unusedSubCategoryId)); expectDeleteToThrow(unusedSubCategoryId); usedDefinitionElementIds = txn.deleteDefinitionElements([unusedSubCategoryId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedSubCategoryId)); // deleteDefinitionElements for an unused SpatialCategory should succeed, delete should still fail const unusedSpatialCategory = SpatialCategory.create(iModelDb, definitionModelId, "Unused SpatialCategory"); const unusedSpatialCategoryId = txn.insertElement(unusedSpatialCategory.toJSON()); assert.isTrue(Id64.isValidId64(unusedSpatialCategoryId)); const reloadedSpatialCategory = iModelDb.elements.getElement(unusedSpatialCategoryId, SpatialCategory); const unusedSpatialCategoryDefaultSubCategoryId = reloadedSpatialCategory.myDefaultSubCategoryId(); assert.isTrue(Id64.isValidId64(unusedSpatialCategoryDefaultSubCategoryId)); expectDeleteToThrow(unusedSpatialCategoryId); expectDeleteToThrow(unusedSpatialCategoryDefaultSubCategoryId); usedDefinitionElementIds = txn.deleteDefinitionElements([unusedSpatialCategoryId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedSpatialCategoryId)); assert.isUndefined(iModelDb.elements.tryGetElement(unusedSpatialCategoryDefaultSubCategoryId)); // delete/deleteDefinitionElements of a used DrawingCategory should fail expectDeleteToThrow(drawingCategoryId); usedDefinitionElementIds = txn.deleteDefinitionElements([drawingCategoryId]); assert.isTrue(usedDefinitionElementIds.has(drawingCategoryId)); assert.isDefined(iModelDb.elements.tryGetElement(drawingCategoryId)); // deleteDefinitionElements for an unused DrawingCategory should succeed, delete should still fail const unusedDrawingCategory = DrawingCategory.create(iModelDb, definitionModelId, "Unused DrawingCategory"); const unusedDrawingCategoryId = txn.insertElement(unusedDrawingCategory.toJSON()); assert.isTrue(Id64.isValidId64(unusedDrawingCategoryId)); const reloadedDrawingCategory = iModelDb.elements.getElement(unusedDrawingCategoryId, DrawingCategory); const unusedDrawingSubCategoryId = reloadedDrawingCategory.myDefaultSubCategoryId(); assert.isTrue(Id64.isValidId64(unusedDrawingSubCategoryId)); expectDeleteToThrow(unusedDrawingSubCategoryId); usedDefinitionElementIds = txn.deleteDefinitionElements([unusedDrawingCategoryId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedDrawingCategoryId)); assert.isUndefined(iModelDb.elements.tryGetElement(unusedDrawingSubCategoryId)); // delete/deleteDefinitionElements of DefinitionElements used by an existing SpatialViewDefinition should fail expectDeleteToThrow(spatialCategorySelectorId); expectDeleteToThrow(modelSelectorId); expectDeleteToThrow(displayStyle3dId); usedDefinitionElementIds = txn.deleteDefinitionElements([spatialCategorySelectorId, modelSelectorId, displayStyle3dId]); assert.isTrue(usedDefinitionElementIds.has(spatialCategorySelectorId)); assert.isTrue(usedDefinitionElementIds.has(modelSelectorId)); assert.isTrue(usedDefinitionElementIds.has(displayStyle3dId)); assert.isDefined(iModelDb.elements.tryGetElement(spatialCategorySelectorId)); assert.isDefined(iModelDb.elements.tryGetElement(modelSelectorId)); assert.isDefined(iModelDb.elements.tryGetElement(displayStyle3dId)); assert.isDefined(iModelDb.elements.tryGetElement(viewId)); // deleteDefinitionElements should succeed when the list includes the SpatialViewDefinition as the only thing referencing other DefinitionElements, delete should still fail expectDeleteToThrow(viewId); usedDefinitionElementIds = txn.deleteDefinitionElements([viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(spatialCategorySelectorId)); assert.isUndefined(iModelDb.elements.tryGetElement(modelSelectorId)); assert.isUndefined(iModelDb.elements.tryGetElement(displayStyle3dId)); assert.isUndefined(iModelDb.elements.tryGetElement(viewId)); // delete/deleteDefinitionElements of DefinitionElements used by an existing DrawingViewDefinition should fail expectDeleteToThrow(drawingCategorySelectorId); expectDeleteToThrow(displayStyle2dId); usedDefinitionElementIds = txn.deleteDefinitionElements([drawingCategorySelectorId, displayStyle2dId]); assert.isTrue(usedDefinitionElementIds.has(drawingCategorySelectorId)); assert.isTrue(usedDefinitionElementIds.has(displayStyle2dId)); assert.isDefined(iModelDb.elements.tryGetElement(drawingCategorySelectorId)); assert.isDefined(iModelDb.elements.tryGetElement(displayStyle2dId)); assert.isDefined(iModelDb.elements.tryGetElement(drawingViewId)); // deleteDefinitionElements should succeed when the list includes the DrawingViewDefinition as the only thing referencing other DefinitionElements, delete should still fail expectDeleteToThrow(drawingViewId); usedDefinitionElementIds = txn.deleteDefinitionElements([drawingViewId, drawingCategorySelectorId, displayStyle2dId]); assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(drawingCategorySelectorId)); assert.isUndefined(iModelDb.elements.tryGetElement(displayStyle2dId)); assert.isUndefined(iModelDb.elements.tryGetElement(drawingViewId)); txn.end(); } finally { if (txn.isActive) txn.end("abandon"); iModelDb.close(); } }); describe("deleteElements", () => { let elementCounter = 0; beforeEach(() => { elementCounter = 0; }); afterEach(() => { if (iModelDb.isOpen) { const txn = new EditTxn(iModelDb, "abandon"); txn.start(); txn.end("abandon"); } }); const insertDefinitionElement = (txn, parentId, modelId) => { const props = { classFullName: GeometryPart.classFullName, model: modelId ?? definitionModelId, code: GeometryPart.createCode(iModelDb, modelId ?? definitionModelId, `TestPart_${++elementCounter}`), geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)), ...(parentId ? { parent: { id: parentId, relClassName: "BisCore:ElementOwnsChildElements" } } : {}), }; return txn.insertElement(props); }; /** * Call deleteElements, verify the returned failed-set, then check which elements survived vs. were deleted. * Rolls back changes with abandonChanges afterwards. */ const executeTestCase = (txn, label, idsToDelete, expectedDeleted, expectedFailed, expectedRetained, abandonChanges = true) => { const failed = txn.deleteElements(idsToDelete); assert.sameMembers(Array.from(failed.failedIds), expectedFailed, `[${label}] failed set mismatch`); for (const id of expectedDeleted) assert.isUndefined(iModelDb.elements.tryGetElement(id), `error reading element`); for (const id of expectedRetained) assert.isDefined(iModelDb.elements.tryGetElement(id), `[${label}] ${id} should have been retained`); if (abandonChanges) txn.abandonChanges(); }; it("should delete only if not used with deleteElements", async () => { let usedDefinitionElementIds; const txn = new EditTxn(iModelDb, "delete definition elements"); txn.start(); // make sure deleteElements only skips Elements that are being used (subjectId) usedDefinitionElementIds = txn.deleteElements([physicalObjectId1, physicalObjectId2, physicalObjectId3, subjectId]).failedIds; assert.equal(usedDefinitionElementIds.size, 1); assert.isUndefined(iModelDb.elements.tryGetElement(physicalObjectId1)); assert.isUndefined(iModelDb.elements.tryGetElement(physicalObjectId2)); assert.isUndefined(iModelDb.elements.tryGetElement(physicalObjectId3)); assert.isDefined(iModelDb.elements.tryGetElement(subjectId)); // Reset the db state txn.abandonChanges(); // make sure deleteElements throws for invalid Ids assert.throws(() => txn.deleteElements([Id64.invalid, Id64.invalid])); // deleteElement/deleteElements for a used GeometryPart should fail assert.throws(() => txn.deleteElement(geometryPartId)); usedDefinitionElementIds = txn.deleteElements([geometryPartId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(geometryPartId)); assert.isDefined(iModelDb.elements.tryGetElement(geometryPartId)); // deleteElements for an unused GeometryPart should succeed, delete should still fail const unusedGeometryPartProps = { classFullName: GeometryPart.classFullName, model: definitionModelId, code: GeometryPart.createCode(iModelDb, definitionModelId, "Unused GeometryPart"), geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)), }; // Reset the db state txn.abandonChanges(); const unusedGeometryPartId = txn.insertElement(unusedGeometryPartProps); assert.isTrue(Id64.isValidId64(unusedGeometryPartId)); assert.throws(() => txn.deleteElement(unusedGeometryPartId)); usedDefinitionElementIds = txn.deleteElements([unusedGeometryPartId]).failedIds; assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedGeometryPartId)); txn.abandonChanges(); // deleteElement/deleteElements for a used RenderMaterial should fail assert.throws(() => txn.deleteElement(renderMaterialId)); usedDefinitionElementIds = txn.deleteElements([renderMaterialId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(renderMaterialId)); assert.isDefined(iModelDb.elements.tryGetElement(renderMaterialId)); txn.abandonChanges(); // deleteElements for an unused RenderMaterial should succeed, delete should still fail const unusedRenderMaterialId = RenderMaterialElement.insert(txn, definitionModelId, "Unused RenderMaterial", { paletteName: "PaletteName" }); assert.isTrue(Id64.isValidId64(unusedRenderMaterialId)); assert.throws(() => txn.deleteElement(unusedRenderMaterialId)); usedDefinitionElementIds = txn.deleteElements([unusedRenderMaterialId]).failedIds; assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedRenderMaterialId)); txn.abandonChanges(); // deleteElement/deleteElements for a used Texture should fail assert.throws(() => txn.deleteElement(textureId)); usedDefinitionElementIds = txn.deleteElements([textureId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(textureId)); assert.isDefined(iModelDb.elements.tryGetElement(textureId)); txn.abandonChanges(); // deleteElements for an unused Texture should succeed, delete should still fail const unusedTextureId = IModelTestUtils.insertTextureElement(txn, definitionModelId, "Unused Texture"); assert.isTrue(Id64.isValidId64(unusedTextureId)); assert.throws(() => txn.deleteElement(unusedTextureId)); usedDefinitionElementIds = txn.deleteElements([unusedTextureId]).failedIds; assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedTextureId)); txn.abandonChanges(); // deleteElement/deleteElements for a used SpatialCategory should fail assert.throws(() => txn.deleteElement(spatialCategoryId)); usedDefinitionElementIds = txn.deleteElements([spatialCategoryId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(spatialCategoryId)); assert.isDefined(iModelDb.elements.tryGetElement(spatialCategoryId)); txn.abandonChanges(); // deleteElement/deleteElements for a default SubCategory should fail const spatialCategory = iModelDb.elements.getElement(spatialCategoryId, SpatialCategory); const defaultSpatialSubCategoryId = spatialCategory.myDefaultSubCategoryId(); assert.throws(() => txn.deleteElement(defaultSpatialSubCategoryId)); usedDefinitionElementIds = txn.deleteElements([defaultSpatialSubCategoryId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(defaultSpatialSubCategoryId)); assert.isDefined(iModelDb.elements.tryGetElement(defaultSpatialSubCategoryId)); txn.abandonChanges(); // deleteElement/deleteElements for a used, non-default SubCategory should fail assert.throws(() => txn.deleteElement(subCategoryId)); usedDefinitionElementIds = txn.deleteElements([subCategoryId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(subCategoryId)); assert.isDefined(iModelDb.elements.tryGetElement(subCategoryId)); txn.abandonChanges(); // deleteElements for an unused SubCategory should succeed, delete should still fail const unusedSubCategoryId = SubCategory.insert(txn, spatialCategoryId, "Unused SubCategory", {}); assert.isTrue(Id64.isValidId64(unusedSubCategoryId)); assert.throws(() => txn.deleteElement(unusedSubCategoryId)); usedDefinitionElementIds = txn.deleteElements([unusedSubCategoryId]).failedIds; assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedSubCategoryId)); txn.abandonChanges(); // deleteElements for an unused SpatialCategory should succeed, delete should still fail const unusedSpatialCategoryId = SpatialCategory.insert(txn, definitionModelId, "Unused SpatialCategory", {}); assert.isTrue(Id64.isValidId64(unusedSpatialCategoryId)); const unusedSpatialCategory = iModelDb.elements.getElement(unusedSpatialCategoryId, SpatialCategory); const unusedSpatialCategoryDefaultSubCategoryId = unusedSpatialCategory.myDefaultSubCategoryId(); assert.isTrue(Id64.isValidId64(unusedSpatialCategoryDefaultSubCategoryId)); assert.throws(() => txn.deleteElement(unusedSpatialCategoryId)); assert.throws(() => txn.deleteElement(unusedSpatialCategoryDefaultSubCategoryId)); usedDefinitionElementIds = txn.deleteElements([unusedSpatialCategoryId]).failedIds; assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedSpatialCategoryId)); assert.isUndefined(iModelDb.elements.tryGetElement(unusedSpatialCategoryDefaultSubCategoryId)); txn.abandonChanges(); // deleteElement/deleteElements of a used DrawingCategory should fail assert.throws(() => txn.deleteElement(drawingCategoryId)); usedDefinitionElementIds = txn.deleteElements([drawingCategoryId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(drawingCategoryId)); assert.isDefined(iModelDb.elements.tryGetElement(drawingCategoryId)); txn.abandonChanges(); // deleteElements for an unused DrawingCategory should succeed, delete should still fail const unusedDrawingCategoryId = DrawingCategory.insert(txn, definitionModelId, "Unused DrawingCategory", {}); assert.isTrue(Id64.isValidId64(unusedDrawingCategoryId)); const unusedDrawingCategory = iModelDb.elements.getElement(unusedDrawingCategoryId, DrawingCategory); const unusedDrawingSubCategoryId = unusedDrawingCategory.myDefaultSubCategoryId(); assert.isTrue(Id64.isValidId64(unusedDrawingSubCategoryId)); assert.throws(() => txn.deleteElement(unusedDrawingSubCategoryId)); usedDefinitionElementIds = txn.deleteElements([unusedDrawingCategoryId]).failedIds; assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(unusedDrawingCategoryId)); assert.isUndefined(iModelDb.elements.tryGetElement(unusedDrawingSubCategoryId)); txn.abandonChanges(); // deleteElement/deleteElements of DefinitionElements used by an existing SpatialViewDefinition should fail assert.throws(() => txn.deleteElement(spatialCategorySelectorId)); assert.throws(() => txn.deleteElement(modelSelectorId)); assert.throws(() => txn.deleteElement(displayStyle3dId)); usedDefinitionElementIds = txn.deleteElements([spatialCategorySelectorId, modelSelectorId, displayStyle3dId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(spatialCategorySelectorId)); assert.isTrue(usedDefinitionElementIds.has(modelSelectorId)); assert.isTrue(usedDefinitionElementIds.has(displayStyle3dId)); assert.isDefined(iModelDb.elements.tryGetElement(spatialCategorySelectorId)); assert.isDefined(iModelDb.elements.tryGetElement(modelSelectorId)); assert.isDefined(iModelDb.elements.tryGetElement(displayStyle3dId)); assert.isDefined(iModelDb.elements.tryGetElement(viewId)); txn.abandonChanges(); // deleteElements should succeed when the list includes the SpatialViewDefinition as the only thing referencing other DefinitionElements, delete should still fail assert.throws(() => txn.deleteElement(viewId)); usedDefinitionElementIds = txn.deleteElements([viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId]).failedIds; assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(spatialCategorySelectorId)); assert.isUndefined(iModelDb.elements.tryGetElement(modelSelectorId)); assert.isUndefined(iModelDb.elements.tryGetElement(displayStyle3dId)); assert.isUndefined(iModelDb.elements.tryGetElement(viewId)); txn.abandonChanges(); // deleteElement/deleteElements of DefinitionElements used by an existing DrawingViewDefinition should fail assert.throws(() => txn.deleteElement(drawingCategorySelectorId)); assert.throws(() => txn.deleteElement(displayStyle2dId)); usedDefinitionElementIds = txn.deleteElements([drawingCategorySelectorId, displayStyle2dId]).failedIds; assert.isTrue(usedDefinitionElementIds.has(drawingCategorySelectorId)); assert.isTrue(usedDefinitionElementIds.has(displayStyle2dId)); assert.isDefined(iModelDb.elements.tryGetElement(drawingCategorySelectorId)); assert.isDefined(iModelDb.elements.tryGetElement(displayStyle2dId)); assert.isDefined(iModelDb.elements.tryGetElement(drawingViewId)); txn.abandonChanges(); // deleteElements should succeed when the list includes the DrawingViewDefinition as the only thing referencing other DefinitionElements, delete should still fail assert.throws(() => txn.deleteElement(drawingViewId)); usedDefinitionElementIds = txn.deleteElements([drawingViewId, drawingCategorySelectorId, displayStyle2dId]).failedIds; assert.equal(usedDefinitionElementIds.size, 0); assert.isUndefined(iModelDb.elements.tryGetElement(drawingCategorySelectorId)); assert.isUndefined(iModelDb.elements.tryGetElement(displayStyle2dId)); assert.isUndefined(iModelDb.elements.tryGetElement(drawingViewId)); txn.end("abandon"); iModelDb.close(); }); describe("basic tests", () => { let txn; beforeEach(() => { txn = new EditTxn(iModelDb, "delete Definition Elements"); txn.start(); }); afterEach(() => { txn.end("abandon"); }); it("deletes a single unused definition element", () => { const element = insertDefinitionElement(txn); executeTestCase(txn, "single unused definition element", [element], [element], [], []); }); it("deletes multiple unused definition elements in one call", () => { const element1 = insertDefinitionElement(txn); const element2 = insertDefinitionElement(txn); const element3 = insertDefinitionElement(txn); executeTestCase(txn, "multiple unused definition elements", [element1, element2, element3], [element1, element2, element3], [], []); }); it("returns empty failed-set when given an empty array", () => { const failed = txn.deleteElements([]).failedIds; assert.isEmpty(Array.from(failed), "empty input should return empty failed set"); }); it("throws for invalid ids", () => { assert.throws(() => txn.deleteElements([Id64.invalid, Id64.invalid])); }); it("keeps an in-use definition element in the failed set and does not delete it", () => { // geometryPartId is referenced by PhysicalObject1's geometry stream const failed = txn.deleteElements([geometryPartId]); assert.isTrue(failed.failedIds.has(geometryPartId), "in-use definition element must be in failed set"); assert.isDefined(iModelDb.elements.tryGetElement(geometryPartId), "in-use definition element must not be deleted"); }); it("returns only the in-use definition element in the failed set when mixing used and unused parts", () => { const unusedElement = insertDefinitionElement(txn); const usedElement = insertDefinitionElement(txn); // Reference usedElement from a physical element geometry stream const physElemProps = { classFullName: "Generic:PhysicalObject", model: physicalModelId, category: spatialCategoryId, code: Code.createEmpty(), placement: { origin: Point3d.create(0, 0, 0).toJSON(), angles: {} }, geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, usedElement), }; const physElemId = txn.insertElement(physElemProps); executeTestCase(txn, "used + unused definition elements", [unusedElement, usedElement], [unusedElement], [usedElement], [usedElement, physElemId]); }); it("keeps an in-use RenderMaterial in the failed set and does not delete it", () => { const failed = txn.deleteElements([renderMaterialId]); assert.isTrue(failed.failedIds.has(renderMaterialId), "in-use RenderMaterial must be in failed set"); assert.isDefined(iModelDb.elements.tryGetElement(renderMaterialId), "in-use RenderMaterial must not be deleted"); }); it("deletes an unused RenderMaterial", () => { const unusedRenderMaterialId = RenderMaterialElement.insert(txn, definitionModelId, "Unused RenderMaterial", { paletteName: "PaletteName" }); executeTestCase(txn, "unused RenderMaterial", [unusedRenderMaterialId], [unusedRenderMaterialId], [], []); }); it("keeps an in-use Texture in the failed set and does not delete it", () => { const failed = txn.deleteElements([textureId]); assert.isTrue(failed.failedIds.has(textureId), "in-use Texture must be in failed set"); assert.isDefined(iModelDb.elements.tryGetElement(textureId), "in-use Texture must not be deleted"); }); it("deletes an unused Texture", () => { const unusedTextureId = IModelTestUtils.insertTextureElement(txn, definitionModelId, "Unused Texture"); executeTestCase(txn, "unused Texture", [unusedTextureId], [unusedTextureId], [], []); }); it("returns the full input as the failed set when every element is an in-use definition element", () => { const usedElement1 = insertDefinitionElement(txn); const usedElement2 = insertDefinitionElement(txn); const usedElement3 = insertDefinitionElement(txn); // Reference usedElements from a physical element geometry stream const physElemProps = { classFullName: "Generic:PhysicalObject", model: physicalModelId, category: spatialCategoryId, code: Code.createEmpty(), placement: { origin: Point3d.create(0, 0, 0).toJSON(), angles: {} }, geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, usedElement1), }; assert.isTrue(Id64.isValid(txn.insertElement(physElemProps))); physElemProps.geom = IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, usedElement2); assert.isTrue(Id64.isValid(txn.insertElement(physElemProps))); physElemProps.geom = IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, usedElement3); assert.isTrue(Id64.isValid(txn.insertElement(physElemProps))); const failedToDelete = txn.deleteElements([usedElement1, usedElement2, usedElement3]); assert.equal(failedToDelete.failedIds.size, 3, "all in-use definition elements should be in failed set"); assert.isTrue(failedToDelete.failedIds.has(usedElement1)); assert.isTrue(failedToDelete.failedIds.has(usedElement2)); assert.isTrue(failedToDelete.failedIds.has(usedElement3)); }); }); describe("category and subcategory tests", () => { let txn; beforeEach(() => { txn = new EditTxn(iModelDb, "delete Definition Elements"); txn.start(); }); afterEach(() => { txn.end("abandon"); }); it("keeps an in-use SpatialCategory in the failed set and does not delete it", () => { const failed = txn.deleteElements([spatialCategoryId]); assert.isTrue(failed.failedIds.has(spatialCategoryId), "in-use SpatialCategory must be in failed set"); assert.isDefined(iModelDb.elements.tryGetElement(spatialCategoryId), "in-use SpatialCategory must not be deleted"); }); it("keeps the default SubCategory in the failed set when its parent SpatialCategory is in use", () => { const spatialCategory = iModelDb.elements.getElement(spatialCategoryId, SpatialCategory); const defaultSubCategoryId = spatialCategory.myDefaultSubCategoryId(); const failed = txn.deleteElements([defaultSubCategoryId]); assert.isTrue(failed.failedIds.has(defaultSubCategoryId), "default SubCategory must be in failed set"); assert.isDefined(iModelDb.elements.tryGetElement(defaultSubCategoryId), "default SubCategory must not be deleted"); }); it("keeps a used non-default SubCategory in the failed set and does not delete it", () => { const failed = txn.deleteElements([subCategoryId]); assert.isTrue(failed.failedIds.has(subCategoryId), "in-use SubCategory must be in failed set"); assert.isDefined(iModelDb.elements.tryGetElement(subCategoryId), "in-use SubCategory must not be deleted"); }); it("deletes an unused non-default SubCategory", () => { const unusedSubCategoryId = SubCategory.insert(txn, spatialCategoryId, "Unused SubCategory", {}); executeTestCase(txn, "unused SubCategory", [unusedSubCategoryId], [unusedSubCategoryId], [], [spatialCategoryId]); }); it("deletes an unused SpatialCategory together with its default SubCategory", () => { const unusedCategoryId = SpatialCategory.insert(txn, definitionModelId, "Unused SpatialCategory", {}); const unusedCategory = iModelDb.elements.getElement(unusedCategoryId, SpatialCategory); const unusedDefaultSubCategoryId = unusedCategory.myDefaultSubCategoryId(); executeTestCase(txn, "unused SpatialCategory + default SubCategory", [unusedCategoryId], [unusedCategoryId, unusedDefaultSubCategoryId], [], []); }); it("deletes an unused DrawingCategory together with its default SubCategory", () => { const unusedDrawingCategoryId = DrawingCategory.insert(txn, definitionModelId, "Unused DrawingCategory", {}); const unusedDrawingCategory = iModelDb.elements.getElement(unusedDrawingCategoryId, DrawingCategory); const unusedDrawingSubCategoryId = unusedDrawingCategory.myDefaultSubCategoryId(); executeTestCase(txn, "unused DrawingCategory + default SubCategory", [unusedDrawingCategoryId], [unusedDrawingCategoryId, unusedDrawingSubCategoryId], [], []); }); it("keeps CategorySelector, ModelSelector, DisplayStyle3d in the failed set when their view still exists", () => { executeTestCase(txn, "view-related still referenced", [spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [], [spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [spatialCategorySelectorId, modelSelectorId, displayStyle3dId, viewId]); }); it("deletes CategorySelector, ModelSelector, DisplayStyle3d and the SpatialViewDefinition in one call", () => { executeTestCase(txn, "view + view-related", [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [], []); }); it("deletes DrawingCategorySelector, DisplayStyle2d and the DrawingViewDefinition in one call", () => { executeTestCase(txn, "drawing view + drawing view-related", [drawingViewId, drawingCategorySelectorId, displayStyle2dId], [drawingViewId, drawingCategorySelectorId, displayStyle2dId], [], []); }); it("deletes orphaned CategorySelector and DisplayStyle2d (no view references them)", () => { // Insert a standalone CategorySelector and DisplayStyle2d that are not referenced by any view const orphanCategorySelectorId = CategorySelector.insert(txn, definitionModelId, "OrphanCategorySelector", [drawingCategoryId]); const orphanDisplayStyle2dId = DisplayStyle2d.insert(txn, definitionModelId, "OrphanDisplayStyle2d"); executeTestCase(txn, "orphaned view-related elements", [orphanCategorySelectorId, orphanDisplayStyle2dId], [orphanCategorySelectorId, orphanDisplayStyle2dId], [], []); }); }); describe("Element hierarchy", () => { let txn; beforeEach(() => { txn = new EditTxn(iModelDb, "test"); txn.start(); }); afterEach(() => { txn.end("abandon"); }); it("deletes a parent DefinitionElement and automatically deletes its child", () => { const parentPart = insertDefinitionElement(txn); const childPart = insertDefinitionElement(txn, parentPart); // Only pass the parent - the child must be pulled in automatically executeTestCase(txn, "parent + child definition elements", [parentPart], [parentPart, childPart], [], []); }); it("deletes a parent DefinitionElement and its child explicitly passed", () => { const parentPart = insertDefinitionElement(txn); const childPart = insertDefinitionElement(txn, parentPart); // Pass both parent and child explicitly executeTestCase(txn, "parent + child definition elements", [parentPart, childPart], [parentPart, childPart], [], []); }); // Intra-set code scope // Both partA and partB are in the input set; partB uses partA as its CodeScope. // The old implementation would fail here because partA's deletion was blocked by partB's // code scope reference. The new native API resolves all intra-set dependencies first. it("deletes two DefinitionElements related by code scope when both are in the same set", () => { const scopeSpecId = iModelDb.codeSpecs.insert(txn, "DefElemScopeSpec", CodeScopeSpec.Type.RelatedElement); const partA = insertDefinitionElement(txn); const partBProps = { classFullName: GeometryPart.classFullName, model: definitionModelId, code: { spec: scopeSpecId, scope: partA, value: "partB-code" }, geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)), }; const partB = txn.insertElement(partBProps); // Both unused externally; the intra-set code-scope link must not block deletion executeTestCase(txn, "intra-set code scope", [partA, partB], [partA, partB], [], []); }); // Multi-level parent-child hierarchy // Passing only the root must automatically pull in all descendants (leaf-first). // The old implementation deleted elements in a fixed ECClass order and could not handle // arbitrary parent-child chains within the set. it("deletes a 3-level parent-child hierarchy by passing only the root", () => { const root = insertDefinitionElement(txn); const child = insertDefinitionElement(txn, root); const grandchild = insertDefinitionElement(txn, child); // Only supply the root - all descendants must be included and deleted leaf-first executeTestCase(txn, "3-level hierarchy via root only", [root], [root, child, grandchild], [], []); }); it("deletes a 3-level parent-child hierarchy by passing all elements explicitly", () => { const root = insertDefinitionElement(txn); const child = insertDefinitionElement(txn, root); const grandchild = insertDefinitionElement(txn, child); // Pass all elements explicitly executeTestCase(txn, "3-level hierarchy with all elements", [root, child, grandchild], [root, child, grandchild], [], []); }); // Mixed inputs it("deletes a mixed batch of unused DefinitionElements of different types in one call", () => { const unusedPart = insertD