UNPKG

@itwin/core-backend

Version:
847 lines (846 loc) • 53.7 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { DbResult, Id64 } from "@itwin/core-bentley"; import { Code, ColorDef, IModel, IModelVersion, LockState, QueryRowFormat, SchemaState, SubCategoryAppearance, } from "@itwin/core-common"; import { Arc3d, IModelJson, Point2d, Point3d } from "@itwin/core-geometry"; import * as chai from "chai"; import { assert, expect } from "chai"; import * as chaiAsPromised from "chai-as-promised"; import * as fs from "fs"; import * as semver from "semver"; import * as sinon from "sinon"; import { HubWrappers, KnownTestLocations } from "../"; import { DrawingCategory } from "../../Category"; import { HubMock } from "../../internal/HubMock"; import { _nativeDb, BriefcaseDb, BriefcaseManager, ChannelControl, CodeService, DefinitionModel, DocumentListModel, Drawing, DrawingGraphic, SpatialCategory, Subject, } from "../../core-backend"; import { IModelTestUtils, TestUserType } from "../IModelTestUtils"; chai.use(chaiAsPromised); export async function createNewModelAndCategory(rwIModel, parent) { // Create a new physical model. const [, modelId] = await IModelTestUtils.createAndInsertPhysicalPartitionAndModelAsync(rwIModel, IModelTestUtils.getUniqueModelCode(rwIModel, "newPhysicalModel"), true, parent); // Find or create a SpatialCategory. const dictionary = rwIModel.models.getModel(IModel.dictionaryId); const newCategoryCode = IModelTestUtils.getUniqueSpatialCategoryCode(dictionary, "ThisTestSpatialCategory"); const category = SpatialCategory.create(rwIModel, IModel.dictionaryId, newCategoryCode.value); const spatialCategoryId = rwIModel.elements.insertElement(category.toJSON()); category.setDefaultAppearance(new SubCategoryAppearance({ color: 0xff0000 })); // const spatialCategoryId: Id64String = SpatialCategory.insert(rwIModel, IModel.dictionaryId, newCategoryCode.value!, new SubCategoryAppearance({ color: 0xff0000 })); return { modelId, spatialCategoryId }; } describe("IModelWriteTest", () => { let managerAccessToken; let superAccessToken; let iTwinId; before(() => { HubMock.startup("IModelWriteTest", KnownTestLocations.outputDir); iTwinId = HubMock.iTwinId; }); after(() => HubMock.shutdown()); it("Check busyTimeout option", async () => { const iModelProps = { iModelName: "ReadWriteTest", iTwinId, }; const iModelId = await HubMock.createNewIModel(iModelProps); const briefcaseProps = await BriefcaseManager.downloadBriefcase({ accessToken: "test token", iTwinId, iModelId }); const tryOpen = async (args) => { const start = performance.now(); let didThrow = false; try { await BriefcaseDb.open(args); } catch (e) { assert.strictEqual(e.errorNumber, DbResult.BE_SQLITE_BUSY, "Expect error 'Db is busy'"); didThrow = true; } assert.isTrue(didThrow); return performance.now() - start; }; const seconds = (s) => s * 1000; const db = await BriefcaseDb.open({ fileName: briefcaseProps.fileName }); db.saveChanges(); // lock db so another connection cannot write to it. db.saveFileProperty({ name: "test", namespace: "test" }, ""); assert.isAtMost(await tryOpen({ fileName: briefcaseProps.fileName, busyTimeout: seconds(0) }), seconds(1), "open should fail with busy error instantly"); assert.isAtLeast(await tryOpen({ fileName: briefcaseProps.fileName, busyTimeout: seconds(1) }), seconds(1), "open should fail with atleast 1 sec delay due to retry"); assert.isAtLeast(await tryOpen({ fileName: briefcaseProps.fileName, busyTimeout: seconds(2) }), seconds(2), "open should fail with atleast 2 sec delay due to retry"); assert.isAtLeast(await tryOpen({ fileName: briefcaseProps.fileName, busyTimeout: seconds(3) }), seconds(3), "open should fail with atleast 3 sec delay due to retry"); db.abandonChanges(); db.close(); }); it("WatchForChanges", async () => { const iModelProps = { iModelName: "ReadWriteTest", iTwinId, }; const iModelId = await HubMock.createNewIModel(iModelProps); const briefcaseProps = await BriefcaseManager.downloadBriefcase({ accessToken: "test token", iTwinId, iModelId }); let nClosed = 0; const fsWatcher = { callback: () => { }, close: () => ++nClosed, }; const watchStub = (_filename, _opts, fn) => { fsWatcher.callback = fn; return fsWatcher; }; const watchStubResult = sinon.stub(fs, "watch").callsFake(watchStub); const bc = await BriefcaseDb.open({ fileName: briefcaseProps.fileName }); bc.channels.addAllowedChannel(ChannelControl.sharedChannelName); const roBC = await BriefcaseDb.open({ fileName: briefcaseProps.fileName, watchForChanges: true }); const code1 = IModelTestUtils.getUniqueModelCode(bc, "newPhysicalModel1"); await IModelTestUtils.createAndInsertPhysicalPartitionAndModelAsync(bc, code1, true); bc.saveChanges(); // immediately after save changes the current txnId in the writeable briefcase changes, but it isn't reflected // in the readonly briefcase until the file watcher fires. expect(bc[_nativeDb].getCurrentTxnId()).not.equal(roBC[_nativeDb].getCurrentTxnId()); // trigger watcher via stub fsWatcher.callback(); // now they should match because restartDefaultTxn in the readonly briefcase reads the changes from the writeable connection expect(bc[_nativeDb].getCurrentTxnId()).equal(roBC[_nativeDb].getCurrentTxnId()); roBC.close(); expect(nClosed).equal(1); bc.close(); // NOTE: Since HubMock.startup() is called in the before() block and not beforeEach(), we CANNOT // call sinon.restore() here. This is because sinon.restore() will restore the stubs for // CloudSqlite that HubMock.startup() put in place. watchStubResult.restore(); }); function expectEqualChangesets(a, b) { expect(a.id).to.equal(b.id); expect(a.index).to.equal(b.index); } it("WatchForChanges - push", async () => { const adminAccessToken = await HubWrappers.getAccessToken(TestUserType.SuperManager); const iModelProps = { iModelName: "ReadWriteTest", iTwinId, }; const iModelId = await HubMock.createNewIModel(iModelProps); const briefcaseProps = await BriefcaseManager.downloadBriefcase({ accessToken: adminAccessToken, iTwinId, iModelId }); let nClosed = 0; const fsWatcher = { callback: () => { }, close: () => ++nClosed, }; const watchStub = (_filename, _opts, fn) => { fsWatcher.callback = fn; return fsWatcher; }; const watchStubResult = sinon.stub(fs, "watch").callsFake(watchStub); const bc = await BriefcaseDb.open({ fileName: briefcaseProps.fileName }); bc.channels.addAllowedChannel(ChannelControl.sharedChannelName); const roBC = await BriefcaseDb.open({ fileName: briefcaseProps.fileName, watchForChanges: true }); const code1 = IModelTestUtils.getUniqueModelCode(bc, "newPhysicalModel1"); await IModelTestUtils.createAndInsertPhysicalPartitionAndModelAsync(bc, code1, true); bc.saveChanges(); // immediately after save changes the current txnId in the writeable briefcase changes, but it isn't reflected // in the readonly briefcase until the file watcher fires. expect(bc[_nativeDb].getCurrentTxnId()).not.equal(roBC[_nativeDb].getCurrentTxnId()); // trigger watcher via stub fsWatcher.callback(); // now they should match because restartDefaultTxn in the readonly briefcase reads the changes from the writeable connection expect(bc[_nativeDb].getCurrentTxnId()).equal(roBC[_nativeDb].getCurrentTxnId()); // Push the changes to the hub const prePushChangeset = bc.changeset; let eventRaised = false; roBC.onChangesetChanged.addOnce((prevCS) => { expectEqualChangesets(prevCS, prePushChangeset); eventRaised = true; }); await bc.pushChanges({ accessToken: adminAccessToken, description: "test" }); const postPushChangeset = bc.changeset; assert(!!postPushChangeset); expect(prePushChangeset !== postPushChangeset, "changes should be pushed"); // trigger watcher via stub fsWatcher.callback(); expectEqualChangesets(roBC.changeset, postPushChangeset); expect(roBC[_nativeDb].getCurrentTxnId(), "txn should be updated").equal(bc[_nativeDb].getCurrentTxnId()); expect(eventRaised).to.be.true; roBC.close(); expect(nClosed).equal(1); bc.close(); // NOTE: Since HubMock.startup() is called in the before() block and not beforeEach(), we CANNOT // call sinon.restore() here. This is because sinon.restore() will restore the stubs for // CloudSqlite that HubMock.startup() put in place. watchStubResult.restore(); }); it("WatchForChanges - pull", async () => { const adminAccessToken = await HubWrappers.getAccessToken(TestUserType.SuperManager); const pathname = IModelTestUtils.resolveAssetFile("CompatibilityTestSeed.bim"); const hubName = "CompatibilityTest"; const iModelId = await HubWrappers.pushIModel(managerAccessToken, iTwinId, pathname, hubName, true); // Download two copies of the briefcase - manager and super const args = { iTwinId, iModelId }; const initialDb = await BriefcaseManager.downloadBriefcase({ accessToken: adminAccessToken, ...args }); const briefcaseProps = await BriefcaseManager.downloadBriefcase({ accessToken: adminAccessToken, ...args }); // Push some changes - prep for pull workflow. const bc1 = await BriefcaseDb.open({ fileName: initialDb.fileName }); bc1.channels.addAllowedChannel(ChannelControl.sharedChannelName); const code2 = IModelTestUtils.getUniqueModelCode(bc1, "newPhysicalModel2"); await IModelTestUtils.createAndInsertPhysicalPartitionAndModelAsync(bc1, code2, true); const prePushChangeset = bc1.changeset; bc1.saveChanges(); await bc1.pushChanges({ accessToken: adminAccessToken, description: "test" }); const postPushChangeset = bc1.changeset; assert(!!prePushChangeset); expect(prePushChangeset !== postPushChangeset, "changes should be pushed"); bc1.close(); // Writer that pulls + watcher. let nClosed = 0; const fsWatcher = { callback: () => { }, close: () => ++nClosed, }; const watchStub = (_filename, _opts, fn) => { fsWatcher.callback = fn; return fsWatcher; }; const watchStubResult = sinon.stub(fs, "watch").callsFake(watchStub); const bc = await BriefcaseDb.open({ fileName: briefcaseProps.fileName }); bc.channels.addAllowedChannel(ChannelControl.sharedChannelName); const roBC = await BriefcaseDb.open({ fileName: briefcaseProps.fileName, watchForChanges: true }); const prePullChangeset = bc.changeset; let eventRaised = false; roBC.onChangesetChanged.addOnce((prevCS) => { expectEqualChangesets(prevCS, prePushChangeset); eventRaised = true; }); await bc.pullChanges(); const postPullChangeset = bc.changeset; assert(!!postPullChangeset); expect(prePullChangeset !== postPullChangeset, "changes should be pulled"); // trigger watcher via stub fsWatcher.callback(); expectEqualChangesets(roBC.changeset, postPullChangeset); expect(roBC[_nativeDb].getCurrentTxnId(), "txn should be updated").equal(bc[_nativeDb].getCurrentTxnId()); expect(eventRaised).to.be.true; roBC.close(); expect(nClosed).equal(1); bc.close(); // NOTE: Since HubMock.startup() is called in the before() block and not beforeEach(), we CANNOT // call sinon.restore() here. This is because sinon.restore() will restore the stubs for // CloudSqlite that HubMock.startup() put in place. watchStubResult.restore(); }); it("should handle undo/redo", async () => { const adminAccessToken = await HubWrappers.getAccessToken(TestUserType.SuperManager); // Delete any existing iModels with the same name as the read-write test iModel const iModelName = "CodesUndoRedoPushTest"; // Create a new empty iModel on the Hub & obtain a briefcase const rwIModelId = await HubMock.createNewIModel({ accessToken: adminAccessToken, iTwinId, iModelName, description: "TestSubject" }); assert.isNotEmpty(rwIModelId); const rwIModel = await HubWrappers.downloadAndOpenBriefcase({ accessToken: adminAccessToken, iTwinId, iModelId: rwIModelId }); rwIModel.channels.addAllowedChannel(ChannelControl.sharedChannelName); // create and insert a new model with code1 const code1 = IModelTestUtils.getUniqueModelCode(rwIModel, "newPhysicalModel1"); await IModelTestUtils.createAndInsertPhysicalPartitionAndModelAsync(rwIModel, code1, true); assert.isTrue(rwIModel.elements.getElement(code1) !== undefined); // throws if element is not found // create a local txn with that change rwIModel.saveChanges("inserted newPhysicalModel"); // Reverse that local txn rwIModel.txns.reverseSingleTxn(); try { // The model that I just created with code1 should no longer be there. const theNewModel = rwIModel.elements.getElement(code1); // throws if element is not found assert.isTrue(theNewModel === undefined); // really should not be here. assert.fail(); // should not be here. } catch { // this is what I expect } // Create and insert a model with code2 const code2 = IModelTestUtils.getUniqueModelCode(rwIModel, "newPhysicalModel2"); await IModelTestUtils.createAndInsertPhysicalPartitionAndModelAsync(rwIModel, code2, true); rwIModel.saveChanges("inserted generic objects"); // The iModel should have a model with code1 and not code2 assert.isTrue(rwIModel.elements.getElement(code2) !== undefined); // throws if element is not found // Push the changes to the hub const prePushChangeset = rwIModel.changeset; await rwIModel.pushChanges({ accessToken: adminAccessToken, description: "test" }); const postPushChangeset = rwIModel.changeset; assert(!!postPushChangeset); expect(prePushChangeset !== postPushChangeset); rwIModel.close(); }); it("should be able to upgrade a briefcase with an older schema", async () => { /** * Test validates that - * - User "manager" upgrades the BisCore schema in the briefcase from version 1.0.0 to 1.0.10+ * - User "super" can get the upgrade "manager" made */ /* Setup test - Push an iModel with an old BisCore schema up to the Hub */ const pathname = IModelTestUtils.resolveAssetFile("CompatibilityTestSeed.bim"); const hubName = "CompatibilityTest"; const iModelId = await HubWrappers.pushIModel(managerAccessToken, iTwinId, pathname, hubName, true); // Download two copies of the briefcase - manager and super const args = { iTwinId, iModelId }; const managerBriefcaseProps = await BriefcaseManager.downloadBriefcase({ accessToken: managerAccessToken, ...args }); const superBriefcaseProps = await BriefcaseManager.downloadBriefcase({ accessToken: superAccessToken, ...args }); /* User "manager" upgrades the briefcase */ // Validate the original state of the BisCore schema in the briefcase let iModel = await BriefcaseDb.open({ fileName: managerBriefcaseProps.fileName }); const beforeVersion = iModel.querySchemaVersion("BisCore"); assert.isTrue(semver.satisfies(beforeVersion, "= 1.0.0")); assert.isFalse(iModel[_nativeDb].hasPendingTxns()); iModel.close(); // Validate that the BisCore schema is recognized as a recommended upgrade let schemaState = BriefcaseDb.validateSchemas(managerBriefcaseProps.fileName, true); assert.strictEqual(schemaState, SchemaState.UpgradeRecommended); // Upgrade the schemas await BriefcaseDb.upgradeSchemas(managerBriefcaseProps); // Validate state after upgrade iModel = await BriefcaseDb.open({ fileName: managerBriefcaseProps.fileName }); const afterVersion = iModel.querySchemaVersion("BisCore"); assert.isTrue(semver.satisfies(afterVersion, ">= 1.0.10")); assert.isFalse(iModel[_nativeDb].hasPendingTxns()); assert.isFalse(iModel.holdsSchemaLock); assert.isFalse(iModel[_nativeDb].hasUnsavedChanges()); iModel.close(); /* User "super" can get the upgrade "manager" made */ // Validate that the BisCore schema is recognized as a recommended upgrade schemaState = BriefcaseDb.validateSchemas(superBriefcaseProps.fileName, true); assert.strictEqual(schemaState, SchemaState.UpgradeRecommended); // Open briefcase and pull change sets to upgrade const superIModel = await BriefcaseDb.open({ fileName: superBriefcaseProps.fileName }); superBriefcaseProps.changeset = await superIModel.pullChanges({ accessToken: superAccessToken }); const superVersion = superIModel.querySchemaVersion("BisCore"); assert.isTrue(semver.satisfies(superVersion, ">= 1.0.10")); assert.isFalse(superIModel[_nativeDb].hasUnsavedChanges()); // Validate no changes were made assert.isFalse(superIModel[_nativeDb].hasPendingTxns()); // Validate no changes were made superIModel.close(); // Validate that there are no upgrades required schemaState = BriefcaseDb.validateSchemas(superBriefcaseProps.fileName, true); assert.strictEqual(schemaState, SchemaState.UpToDate); // Upgrade the schemas - ensure this is a no-op await BriefcaseDb.upgradeSchemas(superBriefcaseProps); await HubMock.deleteIModel({ accessToken: managerAccessToken, iTwinId, iModelId }); }); it("changeset size and ec schema version change", async () => { const adminToken = "super manager token"; const iModelName = "changeset_size"; const rwIModelId = await HubMock.createNewIModel({ iTwinId, iModelName, description: "TestSubject", accessToken: adminToken }); assert.isNotEmpty(rwIModelId); const rwIModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId: rwIModelId, accessToken: adminToken }); assert.equal(rwIModel[_nativeDb].enableChangesetSizeStats(true), DbResult.BE_SQLITE_OK); const schema = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="ts" version="01.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1"> <ECSchemaReference name="BisCore" version="01.00" alias="bis"/> <ECEntityClass typeName="Test2dElement"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="s" typeName="string"/> </ECEntityClass> </ECSchema>`; await rwIModel.importSchemaStrings([schema]); rwIModel.channels.addAllowedChannel(ChannelControl.sharedChannelName); rwIModel.saveChanges("user 1: schema changeset"); if (true || "push changes") { // Push the changes to the hub const prePushChangeSetId = rwIModel.changeset.id; await rwIModel.pushChanges({ description: "push schema changeset", accessToken: adminToken }); const postPushChangeSetId = rwIModel.changeset.id; assert(!!postPushChangeSetId); expect(prePushChangeSetId !== postPushChangeSetId); const changesets = await HubMock.queryChangesets({ iModelId: rwIModelId, accessToken: superAccessToken }); assert.equal(changesets.length, 1); } await rwIModel.locks.acquireLocks({ shared: IModel.dictionaryId }); const codeProps = Code.createEmpty(); codeProps.value = "DrawingModel"; let totalEl = 0; const [, drawingModelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(rwIModel, codeProps, true); let drawingCategoryId = DrawingCategory.queryCategoryIdByName(rwIModel, IModel.dictionaryId, "MyDrawingCategory"); if (undefined === drawingCategoryId) drawingCategoryId = DrawingCategory.insert(rwIModel, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance({ color: ColorDef.fromString("rgb(255,0,0)").toJSON() })); const insertElements = (imodel, className = "Test2dElement", noOfElements = 10, userProp) => { for (let m = 0; m < noOfElements; ++m) { const geomArray = [ Arc3d.createXY(Point3d.create(0, 0), 5), Arc3d.createXY(Point3d.create(5, 5), 2), Arc3d.createXY(Point3d.create(-5, -5), 20), ]; const geometryStream = []; for (const geom of geomArray) { const arcData = IModelJson.Writer.toIModelJson(geom); geometryStream.push(arcData); } const prop = userProp(++totalEl); // Create props const geomElement = { classFullName: `TestDomain:${className}`, model: drawingModelId, category: drawingCategoryId, code: Code.createEmpty(), geom: geometryStream, ...prop, }; const id = imodel.elements.insertElement(geomElement); assert.isTrue(Id64.isValidId64(id), "insert worked"); } }; const str = new Array(1024).join("x"); insertElements(rwIModel, "Test2dElement", 1024, () => { return { s: str }; }); assert.equal(1357648, rwIModel[_nativeDb].getChangesetSize()); rwIModel.saveChanges("user 1: data"); assert.equal(0, rwIModel[_nativeDb].getChangesetSize()); await rwIModel.pushChanges({ description: "schema changeset", accessToken: adminToken }); rwIModel.close(); }); it("should set a fake verifyCode for codeService that throws error for operations that affect code, if failed to open codeService ", async () => { const iModelProps = { iModelName: "codeServiceTest", iTwinId, }; const iModelId = await HubMock.createNewIModel(iModelProps); const briefcaseProps = await BriefcaseManager.downloadBriefcase({ accessToken: "codeServiceTest", iTwinId, iModelId }); const originalCreateForIModel = CodeService.createForIModel; // can be any errors except 'NoCodeIndex' CodeService.createForIModel = async () => { throw new CodeService.Error("MissingCode", 0x10000 + 1, " "); }; const briefcaseDb = await BriefcaseDb.open({ fileName: briefcaseProps.fileName }); briefcaseDb.channels.addAllowedChannel(ChannelControl.sharedChannelName); let firstNonRootElement = { id: undefined, codeValue: "test" }; // eslint-disable-next-line @typescript-eslint/no-deprecated briefcaseDb.withPreparedStatement("SELECT * from Bis.Element LIMIT 1 OFFSET 1", (stmt) => { if (stmt.step() === DbResult.BE_SQLITE_ROW) { firstNonRootElement = stmt.getRow(); } }); // make change to the briefcaseDb that does not affect code, e.g., save file property // expect no error from verifyCode expect(() => briefcaseDb.saveFileProperty({ name: "codeServiceProp", namespace: "codeService", id: 1, subId: 1 }, "codeService test")).to.not.throw(); // make change to the briefcaseDb that affects code that will invoke verifyCode, e.g., update an element with a non-null code // expect error from verifyCode let newProps = { id: firstNonRootElement.id, code: { ...Code.createEmpty(), value: firstNonRootElement.codeValue }, classFullName: undefined, model: undefined }; await briefcaseDb.locks.acquireLocks({ exclusive: firstNonRootElement.id }); expect(() => briefcaseDb.elements.updateElement(newProps)).to.throw(CodeService.Error); // make change to the briefcaseDb that will invoke verifyCode with a null(empty) code, e.g., update an element with a null(empty) code // expect no error from verifyCode newProps = { id: firstNonRootElement.id, code: Code.createEmpty(), classFullName: undefined, model: undefined }; expect(() => briefcaseDb.elements.updateElement(newProps)).to.not.throw(); briefcaseDb.close(); // throw "NoCodeIndex", this error should get ignored because it means the iModel isn't enforcing codes. updating an element with an empty code and a non empty code should work without issue. CodeService.createForIModel = async () => { throw new CodeService.Error("NoCodeIndex", 0x10000 + 1, " "); }; const briefcaseDb2 = await BriefcaseDb.open({ fileName: briefcaseProps.fileName }); briefcaseDb2.channels.addAllowedChannel(ChannelControl.sharedChannelName); await briefcaseDb2.locks.acquireLocks({ exclusive: firstNonRootElement.id }); // expect no error from verifyCode for empty code expect(() => briefcaseDb2.elements.updateElement(newProps)).to.not.throw(); newProps = { id: firstNonRootElement.id, code: { ...Code.createEmpty(), value: firstNonRootElement.codeValue }, classFullName: undefined, model: undefined }; // make change to the briefcaseDb that affects code that will invoke verifyCode, e.g., update an element with a non-null code // expect no error from verifyCode expect(() => briefcaseDb2.elements.updateElement(newProps)).to.not.throw(); // clean up CodeService.createForIModel = originalCreateForIModel; briefcaseDb2.close(); }); it("clear cache on schema changes", async () => { const adminToken = await HubWrappers.getAccessToken(TestUserType.SuperManager); const userToken = await HubWrappers.getAccessToken(TestUserType.Super); // Delete any existing iModels with the same name as the OptimisticConcurrencyTest iModel const iModelName = "SchemaChanges"; // Create a new empty iModel on the Hub & obtain a briefcase const rwIModelId = await HubMock.createNewIModel({ iTwinId, iModelName, description: "TestSubject" }); assert.isNotEmpty(rwIModelId); const rwIModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId: rwIModelId, accessToken: adminToken }); const rwIModel2 = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId: rwIModelId, accessToken: userToken }); // enable change tracking assert.equal(rwIModel[_nativeDb].enableChangesetSizeStats(true), DbResult.BE_SQLITE_OK); assert.equal(rwIModel2[_nativeDb].enableChangesetSizeStats(true), DbResult.BE_SQLITE_OK); const schema = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="ts" version="01.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1"> <ECSchemaReference name="BisCore" version="01.00" alias="bis"/> <ECEntityClass typeName="Test2dElement"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="s" typeName="string"/> </ECEntityClass> </ECSchema>`; await rwIModel.importSchemaStrings([schema]); rwIModel.channels.addAllowedChannel(ChannelControl.sharedChannelName); rwIModel2.channels.addAllowedChannel(ChannelControl.sharedChannelName); rwIModel.saveChanges("user 1: schema changeset"); if (true || "push changes") { // Push the changes to the hub const prePushChangeSetId = rwIModel.changeset.id; await rwIModel.pushChanges({ description: "schema changeset", accessToken: adminToken }); const postPushChangeSetId = rwIModel.changeset.id; assert(!!postPushChangeSetId); expect(prePushChangeSetId !== postPushChangeSetId); const changesets = await HubMock.queryChangesets({ iModelId: rwIModelId, accessToken: superAccessToken }); assert.equal(changesets.length, 1); } const codeProps = Code.createEmpty(); codeProps.value = "DrawingModel"; let totalEl = 0; await rwIModel.locks.acquireLocks({ shared: IModel.dictionaryId }); const [, drawingModelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(rwIModel, codeProps, true); let drawingCategoryId = DrawingCategory.queryCategoryIdByName(rwIModel, IModel.dictionaryId, "MyDrawingCategory"); if (undefined === drawingCategoryId) drawingCategoryId = DrawingCategory.insert(rwIModel, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance({ color: ColorDef.fromString("rgb(255,0,0)").toJSON() })); const insertElements = (imodel, className = "Test2dElement", noOfElements = 10, userProp) => { for (let m = 0; m < noOfElements; ++m) { const geomArray = [ Arc3d.createXY(Point3d.create(0, 0), 5), Arc3d.createXY(Point3d.create(5, 5), 2), Arc3d.createXY(Point3d.create(-5, -5), 20), ]; const geometryStream = []; for (const geom of geomArray) { const arcData = IModelJson.Writer.toIModelJson(geom); geometryStream.push(arcData); } const prop = userProp(++totalEl); // Create props const geomElement = { classFullName: `TestDomain:${className}`, model: drawingModelId, category: drawingCategoryId, code: Code.createEmpty(), geom: geometryStream, ...prop, }; const id = imodel.elements.insertElement(geomElement); assert.isTrue(Id64.isValidId64(id), "insert worked"); } }; insertElements(rwIModel, "Test2dElement", 10, (n) => { return { s: `s-${n}` }; }); assert.equal(3889, rwIModel[_nativeDb].getChangesetSize()); rwIModel.saveChanges("user 1: data changeset"); if (true || "push changes") { // Push the changes to the hub const prePushChangeSetId = rwIModel.changeset.id; await rwIModel.pushChanges({ description: "10 instances of test2dElement", accessToken: adminToken }); const postPushChangeSetId = rwIModel.changeset.id; assert(!!postPushChangeSetId); expect(prePushChangeSetId !== postPushChangeSetId); const changesets = await HubMock.queryChangesets({ iModelId: rwIModelId, accessToken: superAccessToken }); assert.equal(changesets.length, 2); } let rows = []; // eslint-disable-next-line @typescript-eslint/no-deprecated rwIModel.withPreparedStatement("SELECT * FROM TestDomain.Test2dElement", (stmt) => { while (stmt.step() === DbResult.BE_SQLITE_ROW) { rows.push(stmt.getRow()); } }); assert.equal(rows.length, 10); assert.equal(rows.map((r) => r.s).filter((v) => v).length, 10); rows = []; for await (const queryRow of rwIModel.createQueryReader("SELECT * FROM TestDomain.Test2dElement", undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { rows.push(queryRow.toRow()); } assert.equal(rows.length, 10); assert.equal(rows.map((r) => r.s).filter((v) => v).length, 10); if (true || "user pull/merge") { // pull and merge changes await rwIModel2.pullChanges({ accessToken: userToken }); rows = []; // eslint-disable-next-line @typescript-eslint/no-deprecated rwIModel2.withPreparedStatement("SELECT * FROM TestDomain.Test2dElement", (stmt) => { while (stmt.step() === DbResult.BE_SQLITE_ROW) { rows.push(stmt.getRow()); } }); assert.equal(rows.length, 10); assert.equal(rows.map((r) => r.s).filter((v) => v).length, 10); rows = []; for await (const queryRow of rwIModel2.createQueryReader("SELECT * FROM TestDomain.Test2dElement", undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { rows.push(queryRow.toRow()); } assert.equal(rows.length, 10); assert.equal(rows.map((r) => r.s).filter((v) => v).length, 10); // create some element and push those changes await rwIModel2.locks.acquireLocks({ shared: drawingModelId }); insertElements(rwIModel2, "Test2dElement", 10, (n) => { return { s: `s-${n}` }; }); assert.equal(0, rwIModel[_nativeDb].getChangesetSize()); rwIModel2.saveChanges("user 2: data changeset"); if (true || "push changes") { // Push the changes to the hub const prePushChangeSetId = rwIModel2.changeset.id; await rwIModel2.pushChanges({ accessToken: userToken, description: "10 instances of test2dElement" }); const postPushChangeSetId = rwIModel2.changeset.id; assert(!!postPushChangeSetId); expect(prePushChangeSetId !== postPushChangeSetId); const changesets = await HubMock.queryChangesets({ iModelId: rwIModelId, accessToken: userToken }); assert.equal(changesets.length, 3); } } await rwIModel.pullChanges({ accessToken: adminToken }); // second schema import ============================================================== const schemaV2 = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="ts" version="01.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1"> <ECSchemaReference name="BisCore" version="01.00" alias="bis"/> <ECEntityClass typeName="Test2dElement"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="s" typeName="string"/> <ECProperty propertyName="v" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="Test2dElement2nd"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="t" typeName="string"/> <ECProperty propertyName="r" typeName="string"/> </ECEntityClass> </ECSchema>`; await rwIModel.importSchemaStrings([schemaV2]); assert.equal(0, rwIModel[_nativeDb].getChangesetSize()); rwIModel.saveChanges("user 1: schema changeset2"); if (true || "push changes") { // Push the changes to the hub const prePushChangeSetId = rwIModel.changeset.id; await rwIModel.pushChanges({ accessToken: adminToken, description: "schema changeset" }); const postPushChangeSetId = rwIModel.changeset.id; assert(!!postPushChangeSetId); expect(prePushChangeSetId !== postPushChangeSetId); const changesets = await HubMock.queryChangesets({ iModelId: rwIModelId, accessToken: superAccessToken }); assert.equal(changesets.length, 4); } // create some element and push those changes await rwIModel.locks.acquireLocks({ shared: drawingModelId }); insertElements(rwIModel, "Test2dElement", 10, (n) => { return { s: `s-${n}`, v: `v-${n}`, }; }); // create some element and push those changes insertElements(rwIModel, "Test2dElement2nd", 10, (n) => { return { t: `t-${n}`, r: `r-${n}`, }; }); assert.equal(6266, rwIModel[_nativeDb].getChangesetSize()); rwIModel.saveChanges("user 1: data changeset"); if (true || "push changes") { // Push the changes to the hub const prePushChangeSetId = rwIModel.changeset.id; await rwIModel.pushChanges({ accessToken: adminToken, description: "10 instances of test2dElement" }); const postPushChangeSetId = rwIModel.changeset.id; assert(!!postPushChangeSetId); expect(prePushChangeSetId !== postPushChangeSetId); const changesets = await HubMock.queryChangesets({ iModelId: rwIModelId, accessToken: superAccessToken }); assert.equal(changesets.length, 5); } rows = []; // eslint-disable-next-line @typescript-eslint/no-deprecated rwIModel.withPreparedStatement("SELECT * FROM TestDomain.Test2dElement", (stmt) => { while (stmt.step() === DbResult.BE_SQLITE_ROW) { rows.push(stmt.getRow()); } }); assert.equal(rows.length, 30); assert.equal(rows.map((r) => r.s).filter((v) => v).length, 30); assert.equal(rows.map((r) => r.v).filter((v) => v).length, 10); rows = []; for await (const queryRow of rwIModel.createQueryReader("SELECT * FROM TestDomain.Test2dElement", undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { rows.push(queryRow.toRow()); } assert.equal(rows.length, 30); assert.equal(rows.map((r) => r.s).filter((v) => v).length, 30); assert.equal(rows.map((r) => r.v).filter((v) => v).length, 10); rows = []; // eslint-disable-next-line @typescript-eslint/no-deprecated rwIModel.withPreparedStatement("SELECT * FROM TestDomain.Test2dElement2nd", (stmt) => { while (stmt.step() === DbResult.BE_SQLITE_ROW) { rows.push(stmt.getRow()); } }); assert.equal(rows.length, 10); assert.equal(rows.map((r) => r.t).filter((v) => v).length, 10); assert.equal(rows.map((r) => r.r).filter((v) => v).length, 10); rows = []; for await (const queryRow of rwIModel.createQueryReader("SELECT * FROM TestDomain.Test2dElement2nd", undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { rows.push(queryRow.toRow()); } assert.equal(rows.length, 10); assert.equal(rows.map((r) => r.t).filter((v) => v).length, 10); assert.equal(rows.map((r) => r.r).filter((v) => v).length, 10); if (true || "user pull/merge") { // pull and merge changes await rwIModel2.pullChanges({ accessToken: userToken }); rows = []; // Following fail without the fix in briefcase manager where we clear statement cache on schema changeset apply // eslint-disable-next-line @typescript-eslint/no-deprecated rwIModel2.withPreparedStatement("SELECT * FROM TestDomain.Test2dElement", (stmt) => { while (stmt.step() === DbResult.BE_SQLITE_ROW) { rows.push(stmt.getRow()); } }); assert.equal(rows.length, 30); assert.equal(rows.map((r) => r.s).filter((v) => v).length, 30); assert.equal(rows.map((r) => r.v).filter((v) => v).length, 10); rows = []; // Following fail without native side fix where we clear concurrent query cache on schema changeset apply for await (const queryRow of rwIModel2.createQueryReader("SELECT * FROM TestDomain.Test2dElement", undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { rows.push(queryRow.toRow()); } assert.equal(rows.length, 30); assert.equal(rows.map((r) => r.s).filter((v) => v).length, 30); assert.equal(rows.map((r) => r.v).filter((v) => v).length, 10); for (const row of rows) { const el = rwIModel2.elements.getElementProps(row.id); assert.isDefined(el); if (row.s) { assert.equal(row.s, el.s); } else { assert.isUndefined(el.s); } if (row.v) { assert.equal(row.v, el.v); } else { assert.isUndefined(el.v); } } rows = []; // eslint-disable-next-line @typescript-eslint/no-deprecated rwIModel2.withPreparedStatement("SELECT * FROM TestDomain.Test2dElement2nd", (stmt) => { while (stmt.step() === DbResult.BE_SQLITE_ROW) { rows.push(stmt.getRow()); } }); assert.equal(rows.length, 10); assert.equal(rows.map((r) => r.t).filter((v) => v).length, 10); assert.equal(rows.map((r) => r.r).filter((v) => v).length, 10); for (const row of rows) { const el = rwIModel2.elements.getElementProps(row.id); assert.isDefined(el); if (row.s) { assert.equal(row.s, el.s); } else { assert.isUndefined(el.s); } if (row.v) { assert.equal(row.v, el.v); } else { assert.isUndefined(el.v); } } rows = []; for await (const queryRow of rwIModel2.createQueryReader("SELECT * FROM TestDomain.Test2dElement2nd", undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { rows.push(queryRow.toRow()); } assert.equal(rows.length, 10); assert.equal(rows.map((r) => r.t).filter((v) => v).length, 10); assert.equal(rows.map((r) => r.r).filter((v) => v).length, 10); for (const row of rows) { const el = rwIModel2.elements.getElementProps(row.id); assert.isDefined(el); if (row.t) { assert.equal(row.t, el.t); } else { assert.isUndefined(el.t); } if (row.r) { assert.equal(row.r, el.r); } else { assert.isUndefined(el.r); } } } rwIModel.close(); rwIModel2.close(); }); it("pulling a changeset with extents changes should update the extents of the opened imodel", async () => { const accessToken = await HubWrappers.getAccessToken(TestUserType.Regular); const version0 = IModelTestUtils.resolveAssetFile("mirukuru.ibim"); const iModelId = await HubMock.createNewIModel({ iTwinId, iModelName: "projectExtentsTest", version0 }); const iModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId }); const changesetIdBeforeExtentsChange = iModel.changeset.id; const extents = iModel.projectExtents; const newExtents = extents.clone(); newExtents.low.x += 100; newExtents.low.y += 100; newExtents.high.x += 100; newExtents.high.y += 100; iModel.updateProjectExtents(newExtents); iModel.saveChanges("update project extents"); await iModel.pushChanges({ description: "update project extents" }); await HubWrappers.closeAndDeleteBriefcaseDb(accessToken, iModel); const iModelBeforeExtentsChange = await HubWrappers.downloadAndOpenBriefcase({ accessToken, iTwinId, iModelId, asOf: IModelVersion.asOfChangeSet(changesetIdBeforeExtentsChange).toJSON() }); const extentsBeforePull = iModelBeforeExtentsChange.projectExtents; // Read the extents fileProperty. const extentsStrBeforePull = iModelBeforeExtentsChange.queryFilePropertyString({ name: "Extents", namespace: "dgn_Db" }); const ecefLocationBeforeExtentsChange = iModelBeforeExtentsChange.ecefLocation; await iModelBeforeExtentsChange.pullChanges(); // Pulls the extents change. const extentsAfterPull = iModelBeforeExtentsChange.projectExtents; const extentsStrAfterPull = iModelBeforeExtentsChange.queryFilePropertyString({ name: "Extents", namespace: "dgn_Db" }); const ecefLocationAfterExtentsChange = iModelBeforeExtentsChange.ecefLocation; expect(ecefLocationBeforeExtentsChange).to.not.be.undefined; expect(ecefLocationAfterExtentsChange).to.not.be.undefined; expect(ecefLocationBeforeExtentsChange?.isAlmostEqual(ecefLocationAfterExtentsChange)).to.be.false; expect(extentsStrAfterPull).to.not.equal(extentsStrBeforePull); expect(extentsAfterPull.isAlmostEqual(extentsBeforePull)).to.be.false; await HubWrappers.closeAndDeleteBriefcaseDb(accessToken, iModelBeforeExtentsChange); }); it("parent lock should suffice when inserting into deeply nested sub-model", async () => { const version0 = IModelTestUtils.resolveAssetFile("test.bim"); const iModelId = await HubMock.createNewIModel({ iTwinId, iModelName: "subModelCoveredByParentLockTest", version0 }); let iModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId }); iModel.channels.addAllowedChannel(ChannelControl.sharedChannelName); /* Job Subject +- DefinitionPartition -- [DefinitionModel] */ await iModel.locks.acquireLocks({ shared: IModel.repositoryModelId }); const jobSubjectId = IModelTestUtils.createJobSubjectElement(iModel, "JobSubject").insert(); const definitionModelId = DefinitionModel.insert(iModel, jobSubjectId, "Definition"); iModel.saveChanges(); const locks = iModel.locks; expect(locks.isServerBased).true; await iModel.pushChanges({ description: "create model" }); expect(iModel.locks).equal(locks); // pushing should not change your locks /* Job Subject <--- Lock this +- DefinitionPartition -- [DefinitionModel] SpatialCategory <=== insert this DrawingCategory " */ assert.isFalse(iModel.locks.holdsExclusiveLock(jobSubjectId)); assert.isFalse(iModel.locks.holdsExclusiveLock(definitionModelId)); assert.isFalse(iModel.locks.holdsSharedLock(definitionModelId)); await iModel.locks.acquireLocks({ exclusive: jobSubjectId }); iModel.locks.checkExclusiveLock(jobSubjectId, "", ""); iModel.locks.checkSharedLock(jobSubjectId, "", ""); iModel.locks.checkSharedLock(definitionModelId, "", ""); iModel.locks.checkExclusiveLock(definitionModelId, "", ""); const spatialCategoryId = SpatialCategory.insert(iModel, definitionModelId, "SpatialCategory", new SubCategoryAppearance()); // throws if we get locking error const drawingCategoryId = DrawingCategory.insert(iModel, definitionModelId, "DrawingCategory", new SubCategoryAppearance()); assert.isTrue(iModel.elements.getElement(spatialCategoryId).model === definitionModelId); assert.isTrue(iModel.elements.getElement(drawingCategoryId).model === definitionModelId); iModel.saveChanges(); await iModel.pushChanges({ description: "insert category" }); /* Create some more nesting. Job Subject <--- Lock this +- DefinitionPartition -- [DefinitionModel] | SpatialCategory +- Child Subject <== Insert +- DocumentList -- [DocumentListModel] " Drawing -- [DrawingModel] " */ assert.isFalse(iModel.locks.holdsExclusiveLock(jobSubjectId)); assert.isFalse(iModel.locks.holdsExclusiveLock(definitionModelId)); assert.isFalse(iModel.locks.holdsSharedLock(definitionModelId)); await iModel.locks.acquireLocks({ exclusive: jobSubjectId }); iModel.locks.checkExclusiveLock(jobSubjectId, "", ""); iModel.locks.checkSharedLock(IModel.repositoryModelId, "", ""); const childSubjectId = Subject.insert(iModel, jobSubjectId, "Child Subject"); const documentListModelId = DocumentListModel.insert(iModel, childSubjectId, "Document"); // creates DocumentList and DocumentListModel assert.isTrue(Id64.isValidId64(documentListModelId)); const drawingModelId = Drawing.insert(iModel, documentListModelId, "Drawing"); // creates Drawing and DrawingModel assert.isTrue(iModel.elements.getElement(childSubjectId).parent?.i