@itwin/core-backend
Version:
iTwin.js backend components
909 lines • 108 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { DbResult, Guid } from "@itwin/core-bentley";
import { Code, GeometryStreamBuilder, IModel, QueryBinder, SubCategoryAppearance } from "@itwin/core-common";
import * as chai from "chai";
import * as chaiAsPromised from "chai-as-promised";
import { HubWrappers, IModelTestUtils, KnownTestLocations } from "..";
import { _nativeDb, BriefcaseManager, ChangesetECAdaptor, ChannelControl, DrawingCategory, ElementGroupsMembers, SqliteChangesetReader } from "../../core-backend";
import { HubMock } from "../../internal/HubMock";
import { StashManager } from "../../StashManager";
import { EditTxn } from "../../EditTxn";
import { TestUtils } from "../TestUtils";
import { existsSync, unlinkSync, writeFileSync } from "fs";
import * as path from "path";
import { LineSegment3d, Point3d } from "@itwin/core-geometry";
chai.use(chaiAsPromised);
function startTestTxn(iModel, description = "rebase") {
const txn = new EditTxn(iModel, description);
txn.start();
return txn;
}
async function importSchemaStrings(txn, schemas) {
if (txn.isActive)
txn.saveChanges();
await txn.iModel.importSchemaStrings(schemas);
}
class TestIModel {
iModelId = "";
drawingModelId = "";
drawingCategoryId = "";
briefcases = [];
_data = 0;
_nextAccessToken = 1;
constructor() { }
acquireAccessToken() {
return `user${this._nextAccessToken++}`;
}
async startup() {
HubMock.startup("TestIModel", KnownTestLocations.outputDir);
const accessToken = this.acquireAccessToken();
this.iModelId = await HubMock.createNewIModel({ accessToken, iTwinId: HubMock.iTwinId, iModelName: "Test", description: "TestSubject" });
const b1 = await HubWrappers.downloadAndOpenBriefcase({ accessToken, iTwinId: HubMock.iTwinId, iModelId: this.iModelId });
const b1Txn = startTestTxn(b1, "rebase startup");
b1.channels.addAllowedChannel(ChannelControl.sharedChannelName);
const schema1 = `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="ts" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/>
<ECEntityClass typeName="a1">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="prop1" typeName="string" />
</ECEntityClass>
<ECEntityClass typeName="A1Recipe2d">
<BaseClass>bis:TemplateRecipe2d</BaseClass>
<ECProperty propertyName="prop1" typeName="string" />
</ECEntityClass>
<ECRelationshipClass typeName="A1OwnsA1" modifier="None" strength="embedding">
<BaseClass>bis:ElementOwnsChildElements</BaseClass>
<Source multiplicity="(0..1)" roleLabel="owns" polymorphic="true">
<Class class="a1"/>
</Source>
<Target multiplicity="(0..*)" roleLabel="is owned by" polymorphic="false">
<Class class="a1"/>
</Target>
</ECRelationshipClass>
</ECSchema>`;
await importSchemaStrings(b1Txn, [schema1]);
chai.expect(b1.txns.hasPendingTxns).to.be.true;
await b1.pushChanges({ description: "schema1" });
const codeProps = Code.createEmpty();
codeProps.value = "DrawingModel";
await b1.locks.acquireLocks({ shared: IModel.dictionaryId });
this.drawingModelId = IModelTestUtils.createAndInsertDrawingPartitionAndModel(b1Txn, codeProps, true)[1];
let drawingCategoryId = DrawingCategory.queryCategoryIdByName(b1, IModel.dictionaryId, "MyDrawingCategory");
if (undefined === drawingCategoryId)
drawingCategoryId = DrawingCategory.insert(b1Txn, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance());
this.drawingCategoryId = drawingCategoryId;
b1Txn.saveChanges();
await b1.pushChanges({ description: "drawing category" });
b1.close();
}
async openBriefcase() {
const b = await HubWrappers.downloadAndOpenBriefcase({ accessToken: this.acquireAccessToken(), iTwinId: HubMock.iTwinId, iModelId: this.iModelId });
b.channels.addAllowedChannel(ChannelControl.sharedChannelName);
b[_nativeDb].saveChanges();
this.briefcases.push(b);
return b;
}
async insertRecipe2d(txn, markAsIndirect) {
const b = txn.iModel;
await b.locks.acquireLocks({ shared: [IModel.dictionaryId] });
const baseProps = {
classFullName: "TestDomain:A1Recipe2d",
model: IModel.dictionaryId,
code: Code.createEmpty(),
};
let id = "";
if (markAsIndirect) {
b.txns.withIndirectTxnMode(() => {
id = txn.insertElement({ ...baseProps, prop1: `${this._data++}` });
});
return id;
}
return txn.insertElement({ ...baseProps, prop1: `${this._data++}` });
}
async updateRecipe2d(txn, id, markAsIndirect) {
const b = txn.iModel;
await b.locks.acquireLocks({ shared: [IModel.dictionaryId], exclusive: [id] });
const elProps = b.elements.getElementProps(id);
if (markAsIndirect) {
b.txns.withIndirectTxnMode(() => {
txn.updateElement({ ...elProps, prop1: `${this._data++}` });
});
}
else {
txn.updateElement({ ...elProps, prop1: `${this._data++}` });
}
}
async insertElement(txn, markAsIndirect) {
const b = txn.iModel;
await b.locks.acquireLocks({ shared: [this.drawingModelId] });
const builder = new GeometryStreamBuilder();
const p1 = Point3d.createZero();
const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 5.0, y: 0.0, z: 0.0 });
const circle = LineSegment3d.create(p1, p2);
builder.appendGeometry(circle);
const baseProps = {
classFullName: "TestDomain:a1",
model: this.drawingModelId,
category: this.drawingCategoryId,
code: Code.createEmpty(),
geom: builder.geometryStream,
prop1: `${this._data++}`,
};
let id = "";
if (markAsIndirect) {
b.txns.withIndirectTxnMode(() => {
id = txn.insertElement(baseProps);
});
return id;
}
baseProps.prop1 = `${this._data++}`;
return txn.insertElement(baseProps);
}
async insertElementEx(txn, args) {
const b = txn.iModel;
await b.locks.acquireLocks({ shared: [this.drawingModelId] });
const builder = new GeometryStreamBuilder();
const p1 = Point3d.createZero();
const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 5.0, y: 0.0, z: 0.0 });
const circle = LineSegment3d.create(p1, p2);
builder.appendGeometry(circle);
const props = {
classFullName: "TestDomain:a1",
model: this.drawingModelId,
category: this.drawingCategoryId,
code: Code.createEmpty(),
parent: args?.parent,
geom: builder.geometryStream,
prop1: args?.prop1 ?? `${this._data++}`
};
let id = "";
if (args?.markAsIndirect) {
b.txns.withIndirectTxnMode(() => {
id = txn.insertElement(props);
});
return id;
}
return txn.insertElement(props);
}
async updateElement(txn, id, markAsIndirect, updateGeom) {
const b = txn.iModel;
await b.locks.acquireLocks({ shared: [this.drawingModelId], exclusive: [id] });
const elProps = b.elements.getElementProps(id);
if (updateGeom) {
const builder = new GeometryStreamBuilder();
const p1 = Point3d.createZero();
const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 10.0, y: 0.0, z: 0.0 });
const circle = LineSegment3d.create(p1, p2);
builder.appendGeometry(circle);
elProps.geom = builder.geometryStream;
}
if (markAsIndirect) {
b.txns.withIndirectTxnMode(() => {
txn.updateElement({ ...elProps, prop1: `${this._data++}` });
});
}
else {
txn.updateElement({ ...elProps, prop1: `${this._data++}` });
}
}
async deleteElement(txn, id, markAsIndirect) {
const b = txn.iModel;
await b.locks.acquireLocks({ shared: [this.drawingModelId], exclusive: [id] });
if (markAsIndirect) {
b.txns.withIndirectTxnMode(() => {
txn.deleteElement(id);
});
}
else {
txn.deleteElement(id);
}
}
async shutdown() {
this.briefcases.forEach(b => b.close());
HubMock.shutdown();
}
}
const removePropertyRecursive = (obj, prop) => {
if (obj && typeof obj === "object") {
Object.keys(obj).forEach((key) => {
if (key === prop) {
delete obj[key];
}
else {
removePropertyRecursive(obj[key], prop);
}
});
}
};
for (const enableSemanticRebase of [false, true]) {
describe(`rebase changes & stashing api (useSemanticRebase=${enableSemanticRebase})`, function () {
let testIModel;
before(async () => {
await TestUtils.shutdownBackend();
await TestUtils.startBackend({ useSemanticRebase: enableSemanticRebase });
});
after(async () => {
await TestUtils.shutdownBackend();
await TestUtils.startBackend(); // restart normal backend so subsequent test suites aren't left without IModelHost
});
this.beforeEach(async () => {
testIModel = new TestIModel();
await testIModel.startup();
});
this.afterEach(async () => {
await testIModel.shutdown();
});
it("save changes args", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "save changes args");
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges({
source: "test",
description: "test description",
appData: {
test: "test",
foo: [1, 2, 3],
bar: { baz: "qux" }
}
});
let lastTxn = b1.txns.getLastSavedTxnProps();
chai.assert.isDefined(lastTxn);
if (lastTxn) {
chai.expect(lastTxn.props.source).to.be.equals("test");
chai.expect(lastTxn.props.description).to.be.equals("test description");
chai.expect(lastTxn.props.appData).to.not.be.undefined;
chai.expect(lastTxn.props.appData?.test).to.be.eq("test");
chai.expect(lastTxn.props.appData?.foo).to.be.deep.eq([1, 2, 3]);
chai.expect(lastTxn.props.appData?.bar).to.be.deep.eq({ baz: "qux" });
chai.expect(lastTxn.nextId).to.be.undefined;
chai.expect(lastTxn.prevId).to.be.undefined;
chai.expect(lastTxn.type).to.be.eq("Data");
chai.expect(lastTxn.id).to.be.eq('0x100000000');
chai.expect(lastTxn.reversed).to.be.false;
chai.expect(lastTxn.grouped).to.be.false;
}
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges({
source: "test2",
description: "test description 2",
appData: {
test: "test 2",
foo: [11, 12, 13],
bar: { baz: "qux2" }
}
});
lastTxn = b1.txns.getLastSavedTxnProps();
chai.assert.isDefined(lastTxn);
if (lastTxn) {
chai.expect(lastTxn.props.source).to.be.equals("test2");
chai.expect(lastTxn.props.description).to.be.equals("test description 2");
chai.expect(lastTxn.props.appData).to.not.be.undefined;
chai.expect(lastTxn.props.appData?.test).to.be.eq("test 2");
chai.expect(lastTxn.props.appData?.foo).to.be.deep.eq([11, 12, 13]);
chai.expect(lastTxn.props.appData?.bar).to.be.deep.eq({ baz: "qux2" });
chai.expect(lastTxn.nextId).to.be.undefined;
chai.expect(lastTxn.prevId).to.be.equal('0x100000000');
chai.expect(lastTxn.type).to.be.eq("Data");
chai.expect(lastTxn.id).to.be.eq('0x100000001');
chai.expect(lastTxn.reversed).to.be.false;
chai.expect(lastTxn.grouped).to.be.false;
}
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges("new element");
lastTxn = b1.txns.getLastSavedTxnProps();
chai.assert.isDefined(lastTxn);
if (lastTxn) {
chai.expect(lastTxn.props.source).is.undefined;
chai.expect(lastTxn.props.description).to.be.equals("new element");
chai.expect(lastTxn.props.appData).to.be.undefined;
chai.expect(lastTxn.nextId).to.be.undefined;
chai.expect(lastTxn.prevId).to.be.equal('0x100000001');
chai.expect(lastTxn.type).to.be.eq("Data");
chai.expect(lastTxn.id).to.be.eq('0x100000002');
chai.expect(lastTxn.reversed).to.be.false;
chai.expect(lastTxn.grouped).to.be.false;
}
await b1.pushChanges({ description: "new element" });
chai.expect(b1.txns.isUndoPossible).is.false;
chai.expect(b1.txns.isRedoPossible).is.false;
lastTxn = b1.txns.getLastSavedTxnProps();
chai.assert.isUndefined(lastTxn);
});
it("direct / indirect", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "direct indirect");
const directElId = await testIModel.insertElement(b1Txn);
const indirectElId = await testIModel.insertElement(b1Txn, true);
chai.expect(directElId).to.not.be.undefined;
chai.expect(indirectElId).to.not.be.undefined;
b1Txn.saveChanges({ description: "insert element 1 direct and 1 indirect" });
const txn = b1.txns.getLastSavedTxnProps();
chai.assert.isDefined(txn);
if (txn) {
let checkCount = 0;
const reader = SqliteChangesetReader.openTxn({ txnId: txn?.id, db: b1 });
while (reader.step()) {
if (reader.primaryKeyValues.length === 0)
continue;
if (reader.tableName !== "bis_Element")
continue;
const iid = reader.primaryKeyValues[0];
if (iid === directElId) {
chai.expect(reader.isIndirect).to.be.false;
}
if (iid === indirectElId) {
chai.expect(reader.isIndirect).to.be.true;
}
checkCount++;
}
chai.expect(checkCount).to.be.equals(2);
}
await b1.pushChanges({ description: "insert element 1 direct and 1 indirect" });
chai.expect(b1.txns.isUndoPossible).is.false;
chai.expect(b1.txns.isRedoPossible).is.false;
const lastTxn = b1.txns.getLastSavedTxnProps();
chai.assert.isUndefined(lastTxn);
});
it("rebase handler", async () => {
const b1 = await testIModel.openBriefcase();
const b2 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "rebase handler b1");
const b2Txn = startTestTxn(b2, "rebase handler b2");
const e1 = await testIModel.insertElement(b1Txn);
const e2 = await testIModel.insertElement(b1Txn, true);
b1Txn.saveChanges();
await b1.pushChanges({ description: "insert element 1 direct and 1 indirect" });
await b2.pullChanges();
await testIModel.updateElement(b1Txn, e1);
await testIModel.updateElement(b1Txn, e2, true);
b1Txn.saveChanges();
await b1.pushChanges({ description: "update element 1 direct and 1 indirect" });
await testIModel.insertElement(b2Txn);
await testIModel.insertElement(b2Txn, true);
b2Txn.saveChanges("first change");
await testIModel.insertElement(b2Txn);
await testIModel.insertElement(b2Txn, true);
b2Txn.saveChanges("second change");
await testIModel.insertElement(b2Txn);
await testIModel.insertElement(b2Txn, true);
b2Txn.saveChanges("third change");
b2.txns.rebaser.setCustomHandler({
shouldReinstate: (_txn) => {
return true;
},
recompute: async (_txn) => {
await testIModel.insertElement(b2Txn);
await testIModel.insertElement(b2Txn, true);
},
});
await b1.pullChanges();
});
it("stash & drop", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "stash drop");
const e1 = await testIModel.insertElement(b1Txn);
b1Txn.saveChanges();
await b1.pushChanges({ description: "insert element 1 direct and 1 indirect" });
const e2 = await testIModel.insertElement(b1Txn);
b1Txn.saveChanges();
await b1.pushChanges({ description: "insert element 1 direct and 1 indirect" });
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges(`first`);
await testIModel.updateElement(b1Txn, e1);
b1Txn.saveChanges(`second`);
await testIModel.deleteElement(b1Txn, e2);
b1Txn.saveChanges(`third`);
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges(`fourth`);
const stash1 = await StashManager.stash({ db: b1, description: "stash test 1" });
chai.expect(stash1).to.exist;
chai.assert(Guid.isGuid(stash1.id));
chai.expect(stash1.description).to.equals("stash test 1");
chai.expect(stash1.briefcaseId).equals(b1.briefcaseId);
chai.expect(stash1.iModelId).to.equals(b1.iModelId);
chai.expect(stash1.timestamp).to.exist;
chai.expect(stash1.description).to.exist;
chai.expect(stash1.hash).length(64);
chai.expect(stash1.parentChangeset).to.exist;
chai.expect(stash1.idSequences.element).to.equals("0x30000000004");
chai.expect(stash1.idSequences.instance).to.equals("0x30000000000");
chai.expect(stash1.acquiredLocks).equals(4);
chai.expect(stash1.txns).to.exist;
chai.expect(stash1.txns).to.have.lengthOf(4);
chai.expect(stash1.txns[0].props.description).to.equal("first");
chai.expect(stash1.txns[1].props.description).to.equal("second");
chai.expect(stash1.txns[2].props.description).to.equal("third");
chai.expect(stash1.txns[3].props.description).to.equal("fourth");
chai.expect(stash1.txns[0].id).to.equals("0x100000000");
chai.expect(stash1.txns[1].id).to.equals("0x100000001");
chai.expect(stash1.txns[2].id).to.equals("0x100000002");
chai.expect(stash1.txns[3].id).to.equals("0x100000003");
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges(`fifth`);
await testIModel.updateElement(b1Txn, e1);
b1Txn.saveChanges(`sixth`);
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges(`seventh`);
const stash2 = await StashManager.stash({ db: b1, description: "stash test 2" });
chai.expect(stash2).to.exist;
chai.expect(stash2.description).to.equals("stash test 2");
chai.expect(stash2.hash).length(64);
chai.expect(stash2.parentChangeset).to.exist;
chai.expect(stash2.idSequences.element).to.equals("0x30000000006");
chai.expect(stash2.idSequences.instance).to.equals("0x30000000000");
chai.expect(stash2.acquiredLocks).equals(4);
chai.expect(stash2.txns).to.exist;
chai.expect(stash2.txns).to.have.lengthOf(7);
chai.expect(stash2.txns[0].props.description).to.equal("first");
chai.expect(stash2.txns[1].props.description).to.equal("second");
chai.expect(stash2.txns[2].props.description).to.equal("third");
chai.expect(stash2.txns[3].props.description).to.equal("fourth");
chai.expect(stash2.txns[4].props.description).to.equal("fifth");
chai.expect(stash2.txns[5].props.description).to.equal("sixth");
chai.expect(stash2.txns[6].props.description).to.equal("seventh");
chai.expect(stash2.txns[0].id).to.equals("0x100000000");
chai.expect(stash2.txns[1].id).to.equals("0x100000001");
chai.expect(stash2.txns[2].id).to.equals("0x100000002");
chai.expect(stash2.txns[3].id).to.equals("0x100000003");
chai.expect(stash2.txns[4].id).to.equals("0x100000004");
chai.expect(stash2.txns[5].id).to.equals("0x100000005");
chai.expect(stash2.txns[6].id).to.equals("0x100000006");
const stashes = StashManager.getStashes(b1);
chai.expect(stashes).to.have.lengthOf(2);
chai.expect(stashes[0].description).to.equals("stash test 2");
chai.expect(stashes[1].description).to.equals("stash test 1");
chai.expect(stashes[0]).to.deep.equal(stash2);
chai.expect(stashes[1]).to.deep.equal(stash1);
StashManager.dropAllStashes(b1);
chai.expect(StashManager.getStashes(b1)).to.have.lengthOf(0);
});
it("recursively calling withIndirectTxnMode()", async () => {
const b1 = await testIModel.openBriefcase();
chai.expect(b1.txns.getMode()).to.equal("direct");
b1.txns.withIndirectTxnMode(() => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
chai.expect(b1.txns.getMode()).to.equal("direct");
b1.txns.withIndirectTxnMode(() => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
b1.txns.withIndirectTxnMode(() => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
b1.txns.withIndirectTxnMode(() => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
b1.txns.withIndirectTxnMode(() => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
});
chai.expect(b1.txns.getMode()).to.equal("direct");
chai.expect(() => b1.txns.withIndirectTxnMode(() => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
throw new Error("Test error");
})).to.throw();
chai.expect(b1.txns.getMode()).to.equal("direct");
});
it("should fail to importSchemas() & importSchemaStrings() in indirect scope", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "indirect schema import failures");
const schema = `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="MySchema" alias="ms1" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/>
<ECEntityClass typeName="my_class">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="prop1" typeName="string" />
</ECEntityClass>
</ECSchema>`;
const schemaFile = path.join(KnownTestLocations.outputDir, "MySchema.01.00.00.ecschema.xml");
if (existsSync(schemaFile)) {
unlinkSync(schemaFile);
}
writeFileSync(schemaFile, schema, { encoding: "utf8" });
b1Txn.end("abandon");
await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => {
await b1.importSchemas([schema]);
})).to.be.rejectedWith("Cannot import schemas while in an indirect change scope");
b1Txn.start();
b1Txn.end("abandon");
await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => {
await b1.importSchemaStrings([schema]);
})).to.be.rejectedWith("Cannot import schemas while in an indirect change scope");
await b1.importSchemaStrings([schema]);
await b1.pushChanges({ description: "import schema" });
});
it("should fail to saveChanges() & pushChanges() in indirect scope", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "indirect save push failures");
await testIModel.insertElement(b1Txn);
await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => {
b1Txn.saveChanges();
})).to.be.rejectedWith("Cannot save changes while in an indirect change scope");
chai.expect(() => b1.txns.withIndirectTxnMode(() => {
b1Txn.saveChanges();
})).to.be.throws("Cannot save changes while in an indirect change scope");
b1Txn.saveChanges();
await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => {
await b1.pushChanges({ description: "test" });
})).to.be.rejectedWith("Cannot pull and apply changeset while in an indirect change scope");
await b1.pushChanges({ description: "test" });
});
it("should fail to saveFileProperty/deleteFileProperty in indirect scope", async () => {
// pull/push/saveFileProperty/deleteFileProperty should be called inside indirect change scope.
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "indirect file property failures");
b1Txn.saveFileProperty({ namespace: "test", name: "test" }, "Hello, World");
await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => {
b1Txn.saveFileProperty({ namespace: "test", name: "test" }, "This should fail 1");
})).to.be.rejectedWith("Cannot save file property while in an indirect change scope");
chai.expect(b1.queryFilePropertyString({ namespace: "test", name: "test" })).to.equal("Hello, World");
await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => {
b1Txn.deleteFileProperty({ namespace: "test", name: "test" });
})).to.be.rejectedWith("Cannot save file property while in an indirect change scope");
chai.expect(b1.queryFilePropertyString({ namespace: "test", name: "test" })).to.equal("Hello, World");
chai.expect(() => b1.txns.withIndirectTxnMode(() => {
b1Txn.saveFileProperty({ namespace: "test", name: "test" }, "This should fail 2");
})).to.be.throws("Cannot save file property while in an indirect change scope");
chai.expect(b1.queryFilePropertyString({ namespace: "test", name: "test" })).to.equal("Hello, World");
chai.expect(() => b1.txns.withIndirectTxnMode(() => {
b1Txn.deleteFileProperty({ namespace: "test", name: "test" });
})).to.be.throws("Cannot save file property while in an indirect change scope");
b1Txn.saveChanges();
});
it("recursively calling withIndirectTxnModeAsync()", async () => {
const b1 = await testIModel.openBriefcase();
chai.expect(b1.txns.getMode()).to.equal("direct");
await b1.txns.withIndirectTxnModeAsync(async () => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
chai.expect(b1.txns.getMode()).to.equal("direct");
await b1.txns.withIndirectTxnModeAsync(async () => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
await b1.txns.withIndirectTxnModeAsync(async () => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
await b1.txns.withIndirectTxnModeAsync(async () => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
await b1.txns.withIndirectTxnModeAsync(async () => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
await b1.txns.withIndirectTxnModeAsync(async () => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
});
b1.txns.withIndirectTxnMode(() => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
});
});
chai.expect(b1.txns.getMode()).to.equal("direct");
await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => {
chai.expect(b1.txns.getMode()).to.equal("indirect");
throw new Error("Test error");
})).rejectedWith(Error);
chai.expect(b1.txns.getMode()).to.equal("direct");
});
it("should restore mutually exclusive stashes", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "restore mutually exclusive stashes");
// stash 1
const e1 = await testIModel.insertElement(b1Txn);
chai.expect(e1).to.exist;
b1Txn.saveChanges("first");
const stash1 = await StashManager.stash({ db: b1, description: "stash test 1", discardLocalChanges: true, retainLocks: true });
chai.expect(stash1).to.exist;
chai.expect(b1.elements.tryGetElement(e1)).to.undefined;
chai.expect(b1.txns.isUndoPossible).to.be.false;
chai.expect(b1.txns.isRedoPossible).to.be.false;
// stash 2
const e2 = await testIModel.insertElement(b1Txn);
chai.expect(e2).to.exist;
b1Txn.saveChanges("second");
const stash2 = await StashManager.stash({ db: b1, description: "stash test 2", discardLocalChanges: true, retainLocks: true });
chai.expect(stash2).to.exist;
chai.expect(b1.elements.tryGetElement(e1)).to.undefined;
chai.expect(b1.elements.tryGetElement(e2)).to.undefined;
chai.expect(b1.txns.isUndoPossible).to.be.false;
chai.expect(b1.txns.isRedoPossible).to.be.false;
// stash 3
const e3 = await testIModel.insertElement(b1Txn);
chai.expect(e3).to.exist;
b1Txn.saveChanges("third");
const stash3 = await StashManager.stash({ db: b1, description: "stash test 3", discardLocalChanges: true, retainLocks: true });
chai.expect(stash3).to.exist;
chai.expect(b1.elements.tryGetElement(e1)).to.undefined;
chai.expect(b1.elements.tryGetElement(e2)).to.undefined;
chai.expect(b1.elements.tryGetElement(e3)).to.undefined;
chai.expect(b1.txns.isUndoPossible).to.be.false;
chai.expect(b1.txns.isRedoPossible).to.be.false;
chai.expect(e1).not.equals(e2);
chai.expect(e1).not.equals(e3);
chai.expect(e2).not.equals(e3);
const stashes = StashManager.getStashes(b1);
chai.expect(stashes).to.have.lengthOf(3);
chai.expect(stashes[0].description).to.equals("stash test 3");
chai.expect(stashes[1].description).to.equals("stash test 2");
chai.expect(stashes[2].description).to.equals("stash test 1");
// restore stash 1
await StashManager.restore({ db: b1, stash: stash1 });
chai.expect(b1.elements.tryGetElement(e1)).to.exist;
chai.expect(b1.elements.tryGetElement(e2)).to.undefined;
chai.expect(b1.elements.tryGetElement(e3)).to.undefined;
// restore stash 2
await StashManager.restore({ db: b1, stash: stash2 });
chai.expect(b1.elements.tryGetElement(e1)).to.undefined;
chai.expect(b1.elements.tryGetElement(e2)).to.exist;
chai.expect(b1.elements.tryGetElement(e3)).to.undefined;
// restore stash 3
await StashManager.restore({ db: b1, stash: stash3 });
chai.expect(b1.elements.tryGetElement(e1)).to.undefined;
chai.expect(b1.elements.tryGetElement(e2)).to.undefined;
chai.expect(b1.elements.tryGetElement(e3)).to.exist;
});
it("should restore stash in any order", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "restore stash any order");
// stash 1
const e1 = await testIModel.insertElement(b1Txn);
chai.expect(e1).to.exist;
b1Txn.saveChanges("first");
// do not discard local changes
const stash1 = await StashManager.stash({ db: b1, description: "stash test 1" });
chai.expect(stash1).to.exist;
chai.expect(b1.elements.tryGetElement(e1)).to.exist;
chai.expect(b1.txns.isUndoPossible).to.be.true;
chai.expect(b1.txns.isRedoPossible).to.be.false;
// stash 2
const e2 = await testIModel.insertElement(b1Txn);
chai.expect(e2).to.exist;
b1Txn.saveChanges("second");
// do not discard local changes
const stash2 = await StashManager.stash({ db: b1, description: "stash test 2" });
chai.expect(stash2).to.exist;
chai.expect(b1.elements.tryGetElement(e1)).to.exist;
chai.expect(b1.elements.tryGetElement(e2)).to.exist;
chai.expect(b1.txns.isUndoPossible).to.be.true;
chai.expect(b1.txns.isRedoPossible).to.be.false;
// stash 3
const e3 = await testIModel.insertElement(b1Txn);
chai.expect(e3).to.exist;
b1Txn.saveChanges("third");
// do not discard local changes
const stash3 = await StashManager.stash({ db: b1, description: "stash test 3" });
chai.expect(stash3).to.exist;
chai.expect(b1.elements.tryGetElement(e1)).to.exist;
chai.expect(b1.elements.tryGetElement(e2)).to.exist;
chai.expect(b1.elements.tryGetElement(e3)).to.exist;
chai.expect(b1.txns.isUndoPossible).to.be.true;
chai.expect(b1.txns.isRedoPossible).to.be.false;
const stashes = StashManager.getStashes(b1);
chai.expect(stashes).to.have.lengthOf(3);
chai.expect(stashes[0].description).to.equals("stash test 3");
chai.expect(stashes[1].description).to.equals("stash test 2");
chai.expect(stashes[2].description).to.equals("stash test 1");
await b1.discardChanges({ retainLocks: true });
chai.expect(b1.elements.tryGetElement(e1)).to.undefined;
chai.expect(b1.elements.tryGetElement(e2)).to.undefined;
chai.expect(b1.elements.tryGetElement(e3)).to.undefined;
chai.expect(b1.txns.isUndoPossible).to.be.false;
chai.expect(b1.txns.isRedoPossible).to.be.false;
// restore stash 1
await StashManager.restore({ db: b1, stash: stash1 });
chai.expect(b1.elements.tryGetElement(e1)).to.exist;
chai.expect(b1.elements.tryGetElement(e2)).to.undefined;
chai.expect(b1.elements.tryGetElement(e3)).to.undefined;
// restore stash 2
await StashManager.restore({ db: b1, stash: stash2 });
chai.expect(b1.elements.tryGetElement(e1)).to.exist;
chai.expect(b1.elements.tryGetElement(e2)).to.exist;
chai.expect(b1.elements.tryGetElement(e3)).to.undefined;
// restore stash 3
await StashManager.restore({ db: b1, stash: stash3 });
chai.expect(b1.elements.tryGetElement(e1)).to.exist;
chai.expect(b1.elements.tryGetElement(e2)).to.exist;
chai.expect(b1.elements.tryGetElement(e3)).to.exist;
});
it("should restore stash when briefcase has advanced to latest changeset", async () => {
const b1 = await testIModel.openBriefcase();
const b2 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "restore stash latest changeset b1");
const b2Txn = startTestTxn(b2, "restore stash latest changeset b2");
chai.expect(b1.changeset.index).to.equals(2);
chai.expect(b2.changeset.index).to.equals(2);
const e1 = await testIModel.insertElement(b1Txn);
chai.expect(e1).to.exist;
b1Txn.saveChanges();
await b1.pushChanges({ description: `${e1} inserted` });
chai.expect(b1.changeset.index).to.equals(3);
const e2 = await testIModel.insertElement(b2Txn);
chai.expect(e2).to.exist;
b2Txn.saveChanges();
chai.expect(b2.elements.tryGetElement(e1)).to.undefined;
chai.expect(b2.elements.tryGetElement(e2)).to.exist;
const b2Stash1 = await StashManager.stash({ db: b2, description: "stash test 1", discardLocalChanges: true });
chai.expect(b2Stash1.parentChangeset.index).to.equals(2);
chai.expect(b2.elements.tryGetElement(e1)).to.undefined;
chai.expect(b2.elements.tryGetElement(e2)).to.undefined;
await b2.pullChanges();
chai.expect(b2.changeset.index).to.equals(3);
chai.expect(b2.elements.tryGetElement(e1)).to.exist;
chai.expect(b2.elements.tryGetElement(e2)).to.undefined;
// stash restore should downgrade briefcase to older changeset as specified in stash
await StashManager.restore({ db: b2, stash: b2Stash1 });
chai.expect(b2.changeset.index).to.equals(2);
chai.expect(b2.elements.tryGetElement(e1)).to.undefined;
chai.expect(b2.elements.tryGetElement(e2)).to.exist;
await b2.pullChanges();
chai.expect(b2.changeset.index).to.equals(3);
chai.expect(b2.elements.tryGetElement(e1)).to.exist;
chai.expect(b2.elements.tryGetElement(e2)).to.exist;
await b2.pushChanges({ description: "test" });
chai.expect(b2.changeset.index).to.equals(4);
});
it("restore stash that has element changed by another briefcase", async () => {
const b1 = await testIModel.openBriefcase();
const b2 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "restore stash changed element b1");
const b2Txn = startTestTxn(b2, "restore stash changed element b2");
chai.expect(b1.changeset.index).to.equals(2);
chai.expect(b2.changeset.index).to.equals(2);
const e1 = await testIModel.insertElement(b1Txn);
chai.expect(e1).to.exist;
b1Txn.saveChanges();
await b1.pushChanges({ description: `${e1} inserted` });
chai.expect(b1.changeset.index).to.equals(3);
await b2.pullChanges();
chai.expect(b2.changeset.index).to.equals(3);
await testIModel.updateElement(b2Txn, e1);
b2Txn.saveChanges();
chai.expect(b2.locks.holdsExclusiveLock(e1)).to.be.true;
const b2Stash1 = await StashManager.stash({ db: b2, description: "stash test 1", discardLocalChanges: true });
chai.expect(b2Stash1.parentChangeset.index).to.equals(3);
chai.expect(b2.locks.holdsExclusiveLock(e1)).to.be.false;
// stash release lock so b2 should have released lock and b1 should be able to update.
await testIModel.updateElement(b1Txn, e1);
b1Txn.saveChanges();
// restore stash should fail because of lock not obtained on e1
await chai.expect(StashManager.restore({ db: b2, stash: b2Stash1 })).to.be.rejectedWith("exclusive lock is already held");
// push b1 changes to release lock
await b1.pushChanges({ description: `${e1} inserted` });
// restore stash should fail because pull is required to obtain lock
await chai.expect(StashManager.restore({ db: b2, stash: b2Stash1 })).to.be.rejectedWith("pull is required to obtain lock");
await b2.pullChanges();
chai.expect(b2.changeset.index).to.equals(4);
const elBefore = b2.elements.tryGetElementProps(e1);
chai.expect(elBefore.prop1).to.equals("3");
// restore stash should succeed as now it can obtain lock
await StashManager.restore({ db: b2, stash: b2Stash1 });
const elAfter = b2.elements.tryGetElementProps(e1);
chai.expect(elAfter.prop1).to.equals("2");
await b2.pushChanges({ description: `${e1} updated` });
});
it("schema change should not be stashed", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "schema change should not be stashed");
const schema1 = `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="ts" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/>
<ECEntityClass typeName="a1">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="prop1" typeName="string" />
<ECProperty propertyName="prop2" typeName="string" />
</ECEntityClass>
<ECEntityClass typeName="A1Recipe2d">
<BaseClass>bis:TemplateRecipe2d</BaseClass>
<ECProperty propertyName="prop1" typeName="string" />
</ECEntityClass>
<ECRelationshipClass typeName="A1OwnsA1" modifier="None" strength="embedding">
<BaseClass>bis:ElementOwnsChildElements</BaseClass>
<Source multiplicity="(0..1)" roleLabel="owns" polymorphic="true">
<Class class="a1"/>
</Source>
<Target multiplicity="(0..*)" roleLabel="is owned by" polymorphic="false">
<Class class="a1"/>
</Target>
</ECRelationshipClass>
</ECSchema>`;
await importSchemaStrings(b1Txn, [schema1]);
await chai.expect(StashManager.stash({ db: b1, description: "stash test 1" })).to.not.rejectedWith("Bad Arg: Pending schema changeset stashing is not currently supported");
});
it("abort rebase", async () => {
const b1 = await testIModel.openBriefcase();
const b2 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "abort rebase b1");
const b2Txn = startTestTxn(b2, "abort rebase b2");
const e1 = await testIModel.insertElement(b1Txn);
b1Txn.saveChanges();
await b1.pushChanges({ description: `${e1} inserted` });
const e2 = await testIModel.insertElement(b2Txn);
chai.expect(e2).to.exist;
let e3 = "";
b2Txn.saveChanges();
b2.txns.rebaser.setCustomHandler({
shouldReinstate: (_txnProps) => {
return true;
},
recompute: async (_txnProps) => {
chai.expect(BriefcaseManager.containsRestorePoint(b2, BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME)).is.true;
e3 = await testIModel.insertElement(b2Txn);
throw new Error("Rebase failed");
},
});
chai.expect(b2.elements.tryGetElementProps(e1)).to.undefined;
chai.expect(b2.elements.tryGetElementProps(e2)).to.exist;
chai.expect(b2.elements.tryGetElementProps(e3)).to.undefined;
chai.expect(b2.changeset.index).to.equals(2);
await chai.expect(b2.pullChanges()).to.be.rejectedWith("Rebase failed");
chai.expect(b2.changeset.index).to.equals(3);
chai.expect(e3).to.exist;
chai.expect(b2.elements.tryGetElementProps(e1)).to.exist; // came from incoming changeset
chai.expect(b2.elements.tryGetElementProps(e2)).to.undefined; // was local change and reversed during rebase.
chai.expect(b2.elements.tryGetElementProps(e3)).to.undefined; // was insert by reCompute() but due to exception the rebase attempt was abandoned.
chai.expect(BriefcaseManager.containsRestorePoint(b2, BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME)).is.true;
chai.expect(b2.txns.rebaser.canAbort()).is.true;
await b2.txns.rebaser.abort();
chai.expect(b2.changeset.index).to.equals(2);
chai.expect(b2.elements.tryGetElementProps(e1)).to.undefined; // reset briefcase should move tip back to where it was before pull
chai.expect(b2.elements.tryGetElementProps(e2)).to.exist; // abort should put back e2 which was only change at the time of pull
chai.expect(b2.elements.tryGetElementProps(e3)).to.undefined; // add by rebase so should not exist either
chai.expect(BriefcaseManager.containsRestorePoint(b2, BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME)).is.false;
});
it("calling discardChanges() from inside indirect scope is not allowed", async () => {
const b1 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "discard changes inside indirect scope");
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges();
const p1 = b1.txns.withIndirectTxnModeAsync(async () => {
await b1.discardChanges();
});
await chai.expect(p1).to.be.rejectedWith("Cannot discard changes when there are indirect changes");
b1Txn.saveChanges();
});
it("calling discardChanges() during rebasing is not allowed", async () => {
const b1 = await testIModel.openBriefcase();
const b2 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "discard changes during rebasing b1");
const b2Txn = startTestTxn(b2, "discard changes during rebasing b2");
await testIModel.insertElement(b1Txn);
await testIModel.insertElement(b1Txn);
b1Txn.saveChanges();
await b1.pushChanges({ description: "inserted element" });
await testIModel.insertElement(b2Txn);
await testIModel.insertElement(b2Txn);
b2Txn.saveChanges();
b2.txns.rebaser.setCustomHandler({
shouldReinstate: (_txnProps) => {
return true;
},
recompute: async (_txnProps) => {
chai.expect(b2.txns.rebaser.isAborting).is.false;
await b2.discardChanges();
},
});
chai.expect(b2.txns.rebaser.isAborting).is.false;
const p1 = b2.pullChanges();
await chai.expect(p1).to.be.rejectedWith("Cannot discard changes while a rebase is in progress");
chai.expect(b2.txns.rebaser.canAbort()).is.true;
await b2.txns.rebaser.abort();
});
it("getStash() should throw exception", async () => {
const b1 = await testIModel.openBriefcase();
chai.expect(() => StashManager.getStash({ db: b1, stash: "invalid_stash" })).to.throw("No stashes exist for this briefcase");
chai.expect(StashManager.tryGetStash({ db: b1, stash: "invalid_stash" })).to.be.undefined;
});
it("edge case: a indirect update can cause FK violation", async () => {
const b1 = await testIModel.openBriefcase();
const b2 = await testIModel.openBriefcase();
const b1Txn = startTestTxn(b1, "indirect update fk violation b1");
const b2Txn = startTestTxn(b2, "indirect update fk violation b2");
const parentId = await testIModel.insertElement(b1Txn);
const childId = await testIModel.insertElementEx(b1Txn, { parent: { id: parentId, relClassName: "TestDomain:A1OwnsA1" } });
b1Txn.saveChanges("insert parent and child");
await b1.pushChanges({ description: `inserted parent ${parentId} and child ${childId}` });
await b2.pull