@itwin/core-backend
Version:
iTwin.js backend components
899 lines • 220 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 { Id64 } from "@itwin/core-bentley";
import { Code, IModel, QueryRowFormat, SubCategoryAppearance } from "@itwin/core-common";
import * as chai from "chai";
import { HubWrappers, IModelTestUtils, KnownTestLocations } from "..";
import { BriefcaseManager, ChannelControl, DrawingCategory, IModelJsFs } from "../../core-backend";
import { EditTxn, withEditTxn } from "../../EditTxn";
import { HubMock } from "../../internal/HubMock";
import { EntityClass } from "@itwin/ecschema-metadata";
import { TestUtils } from "../TestUtils";
function startTestTxn(iModel, description = "semantic rebase") {
const txn = new EditTxn(iModel, description);
txn.start();
return txn;
}
function endTestTxn(txn) {
if (txn.isActive)
txn.end("abandon");
}
async function importSchemaStrings(txn, schemas) {
if (txn.isActive)
txn.saveChanges();
await txn.iModel.importSchemaStrings(schemas);
}
async function pushChanges(txn, description) {
const briefcase = txn.iModel;
endTestTxn(txn);
await briefcase.pushChanges({ description });
}
async function pullChanges(txn) {
const briefcase = txn.iModel;
endTestTxn(txn);
await briefcase.pullChanges();
}
/**
* Test infrastructure for rebase tests in this file.
* Manages two briefcases (far and local)
*/
class TestIModel {
iModelId = "";
drawingModelId = "";
drawingCategoryId = "";
far;
local;
constructor(iModelId, drawingModelId, drawingCategoryId, far, local) {
this.iModelId = iModelId;
this.drawingModelId = drawingModelId;
this.drawingCategoryId = drawingCategoryId;
this.far = far;
this.local = local;
}
/** Reusable schema definitions for testing rebase with schema transformations */
static schemas = {
/** Base schema v01.00.00 with classes A, C, D */
v01x00x00: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01.00.01 - Adds PropC2 to class C (trivial additive change) */
v01x00x01AddPropC2: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropC2" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01.00.02 - Adds PropD2 to class D (trivial additive change) */
v01x00x02AddPropD2: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropC2" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
<ECProperty propertyName="PropD2" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01.00.02 - Moves PropC from C to A (requires data transformation) on top of v01.00.01 */
v01x00x02MovePropCToA: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
<ECProperty propertyName="PropC" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC2" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01.00.03 - Builds on top of v01.00.02 and in addition moves PropD to base, so we can have incoming and local transforming changes */
v01x00x03MovePropCAndD: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.03" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC2" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD2" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01.00.01 (incompatible variant) - Adds PropC3 instead of PropC2 to class C (same version) */
v01x00x01AddPropC3Incompatible: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropC3" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01.00.02 (incompatible variant) - Adds PropC3 instead of PropC2 to class C (higher version) */
v01x00x02AddPropC3Incompatible: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropC3" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01.00.02 (incompatible variant) - Adds PropC2 (higher version, different type) */
v01x00x02AddPropC2Incompatible: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropC2" typeName="int"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01.00.03 - Adds PropC2 as string (used to test incompatibility when reinstated on top of v01.00.02 with PropC2:int) */
v01x00x03AddPropC2: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.03" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropC2" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
};
/** Additional schemas for extended edge-case tests */
static extendedSchemas = {
/** v01x00x01 - Adds CUniqueAspect class (ElementUniqueAspect subclass) with AspectProp for aspect rebase tests */
v01x00x01WithAspect: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="CUniqueAspect" modifier="None">
<BaseClass>bis:ElementUniqueAspect</BaseClass>
<ECProperty propertyName="AspectProp" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01x00x02 - Extends v01WithAspect by adding AspectProp2 to CUniqueAspect (trivial aspect schema evolution) */
v01x00x02WithAspectProp2: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="CUniqueAspect" modifier="None">
<BaseClass>bis:ElementUniqueAspect</BaseClass>
<ECProperty propertyName="AspectProp" typeName="string"/>
<ECProperty propertyName="AspectProp2" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01x00x01 - Adds new entity class E extending A with PropE (tests new class addition) */
v01x00x01AddClassE: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="E">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropE" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01x00x02 - Extends v01AddClassE by adding PropE2 to class E */
v01x00x02AddClassEPropE2: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="E">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropE" typeName="string"/>
<ECProperty propertyName="PropE2" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01x00x01 - Adds multi-type properties (int, double, boolean) to class C for type-variation tests */
v01x00x01MultiTypeProps: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropCInt" typeName="int"/>
<ECProperty propertyName="PropCDouble" typeName="double"/>
<ECProperty propertyName="PropCBool" typeName="boolean"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01x00x02 - Extends v01MultiTypeProps by adding PropD2 to class D */
v01x00x02MultiTypePropsExtended: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropCInt" typeName="int"/>
<ECProperty propertyName="PropCDouble" typeName="double"/>
<ECProperty propertyName="PropCBool" typeName="boolean"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
<ECProperty propertyName="PropD2" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01x00x01 - Adds multi-type properties (int, double, boolean) to class C for type-variation tests */
v01x00x02MultiTypePropsMovePropDToA: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropCInt" typeName="int"/>
<ECProperty propertyName="PropCDouble" typeName="double"/>
<ECProperty propertyName="PropCBool" typeName="boolean"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
</ECEntityClass>
</ECSchema>`,
/** v01x00x02 - Moves PropD from class D to class A (transforming change for D, analogous to MovePropCToA) */
v01x00x02MovePropDToA: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
</ECEntityClass>
</ECSchema>`,
/** v01x00x01 - Adds a binary property (PropCBin) to class C */
v01x00x01WithBinaryProp: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropCBin" typeName="binary"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
/** v01x00x02 - Extends v01x00x01WithBinaryProp with PropD2 on class D (trivial additive change) */
v01x00x02WithBinaryPropAndPropD2: `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
<ECEntityClass typeName="A">
<BaseClass>bis:GraphicalElement2d</BaseClass>
<ECProperty propertyName="PropA" typeName="string"/>
</ECEntityClass>
<ECEntityClass typeName="C">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropC" typeName="string"/>
<ECProperty propertyName="PropCBin" typeName="binary"/>
</ECEntityClass>
<ECEntityClass typeName="D">
<BaseClass>A</BaseClass>
<ECProperty propertyName="PropD" typeName="string"/>
<ECProperty propertyName="PropD2" typeName="string"/>
</ECEntityClass>
</ECSchema>`,
};
/**
* Create and initialize a new test iModel with far and local briefcases.
* @param testName Unique name for this test (passed to HubMock.startup)
* @returns Fully initialized TestIModel with both briefcases open
*/
static async initialize(testName) {
HubMock.startup(testName, KnownTestLocations.outputDir);
let far;
let local;
try {
const iModelId = await HubMock.createNewIModel({
accessToken: "far-user",
iTwinId: HubMock.iTwinId,
iModelName: testName,
description: `Rebase schema update with data transform tests: ${testName}`,
});
// Open far briefcase and use it for initialization
far = await HubWrappers.downloadAndOpenBriefcase({
iTwinId: HubMock.iTwinId,
iModelId,
accessToken: "far-user",
});
far.channels.addAllowedChannel(ChannelControl.sharedChannelName);
// Initialize with base schema
await far.importSchemaStrings([TestIModel.schemas.v01x00x00]);
await far.pushChanges({ description: "import base schema" });
// Create model and category
const modelCode = IModelTestUtils.getUniqueModelCode(far, "DrawingModel");
await far.locks.acquireLocks({ shared: IModel.dictionaryId });
const [drawingModelId, drawingCategoryId] = withEditTxn(far, "create model and category", (txn) => {
const [, newDrawingModelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, modelCode);
const newDrawingCategoryId = DrawingCategory.insert(txn, IModel.dictionaryId, "DrawingCategory", new SubCategoryAppearance());
return [newDrawingModelId, newDrawingCategoryId];
});
await far.pushChanges({ description: "create model and category" });
// Open local briefcase
local = await HubWrappers.downloadAndOpenBriefcase({
iTwinId: HubMock.iTwinId,
iModelId,
accessToken: "local-user",
});
local.channels.addAllowedChannel(ChannelControl.sharedChannelName);
return new TestIModel(iModelId, drawingModelId, drawingCategoryId, far, local);
}
catch (error) {
if (local?.isOpen)
local.close();
if (far?.isOpen)
far.close();
HubMock.shutdown();
throw error;
}
}
insertElement(txn, className, properties) {
const briefcase = txn.iModel;
const elementProps = {
classFullName: className,
model: this.drawingModelId,
category: this.drawingCategoryId,
code: Code.createEmpty(),
...properties,
};
const element = briefcase.elements.createElement(elementProps);
return txn.insertElement(element.toJSON());
}
updateElement(txn, elementId, updates) {
const briefcase = txn.iModel;
const element = briefcase.elements.getElementProps(elementId);
Object.assign(element, updates);
txn.updateElement(element);
}
getElementProps(briefcase, elementId) {
return briefcase.elements.getElementProps(elementId);
}
getModelProps(briefcase, modelId) {
return briefcase.models.tryGetModelProps(modelId);
}
checkIfFolderExists(briefcase, txnId, isSchemaFolder) {
if (isSchemaFolder)
return BriefcaseManager.semanticRebaseSchemaFolderExists(briefcase, txnId);
return BriefcaseManager.semanticRebaseDataFolderExists(briefcase, txnId);
}
checkifRebaseFolderExists(briefcase) {
const folderPath = BriefcaseManager.getBasePathForSemanticRebaseLocalFiles(briefcase);
return IModelJsFs.existsSync(folderPath);
}
/**
* Insert a UniqueAspect onto an element within the given EditTxn.
* @param txn Active EditTxn
* @param elementId Owning element's Id
* @param aspectClassName Full class name, e.g. "TestDomain:CUniqueAspect"
* @param properties Additional property key/value pairs
* @returns The new aspect's ECInstanceId
*/
insertAspect(txn, elementId, aspectClassName, properties) {
const aspectProps = {
classFullName: aspectClassName,
element: { id: elementId, relClassName: "BisCore.ElementOwnsUniqueAspect" },
...properties,
};
return txn.insertAspect(aspectProps);
}
/**
* Update a UniqueAspect property within the given EditTxn.
* Reads the existing aspect, merges updates, writes back.
*/
updateAspect(txn, elementId, aspectClassName, updates) {
const briefcase = txn.iModel;
const aspects = briefcase.elements.getAspects(elementId, aspectClassName);
chai.expect(aspects.length).to.be.greaterThan(0, "Expected at least one aspect to update");
const aspect = aspects[0];
Object.assign(aspect, updates);
txn.updateAspect(aspect.toJSON());
}
/**
* Delete a UniqueAspect within the given EditTxn.
* Reads the existing aspect, then deletes it by instanceId.
*/
deleteAspect(txn, elementId, aspectClassName) {
const briefcase = txn.iModel;
const aspects = briefcase.elements.getAspects(elementId, aspectClassName);
chai.expect(aspects.length).to.be.greaterThan(0, "Expected at least one aspect to delete");
txn.deleteAspect(aspects[0].id);
}
/**
* Read a UniqueAspect from a briefcase and return it as a plain object.
*/
getAspect(briefcase, elementId, aspectClassName) {
const aspects = briefcase.elements.getAspects(elementId, aspectClassName);
return aspects.length > 0 ? aspects[0] : undefined;
}
/**
* Open and return a third briefcase connected to this iModel (useful for three-briefcase tests).
* The caller is responsible for closing the returned briefcase.
*/
async openExtraBriefcase(accessToken = "extra-user") {
const extra = await HubWrappers.downloadAndOpenBriefcase({
iTwinId: HubMock.iTwinId,
iModelId: this.iModelId,
accessToken,
});
extra.channels.addAllowedChannel(ChannelControl.sharedChannelName);
return extra;
}
/**
* Execute an ECSql SELECT and collect all rows into a Map keyed by ECInstanceId.
* The SELECT must list ECInstanceId as the first column. Any additional columns are captured
* by the caller-supplied names and stored in the returned row objects.
*
* Rows are returned using {@link QueryRowFormat.UseJsPropertyNames} so:
* ECInstanceId → row.id
* ec_className(ECClassId) → row.className (when aliased as `className`)
* PropA → row.propA
* PropC2 → row.propC2
* etc.
*
* @param briefcase The open iModel to query.
* @param ecsql Complete ECSql SELECT statement whose first projected column is ECInstanceId.
*/
static async queryToMap(briefcase, ecsql) {
const result = new Map();
// eslint-disable-next-line @typescript-eslint/no-deprecated
const reader = briefcase.createQueryReader(ecsql, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames });
for await (const row of reader) {
const r = row.toRow();
result.set(r.id, r);
}
return result;
}
shutdown() {
this.far.close();
this.local.close();
HubMock.shutdown();
}
}
/**
* Test suite for rebase logic with schema changes that require data transformations.
*/
describe("Semantic Rebase", function () {
this.timeout(90000); // operations can be slow
let t;
before(async () => {
await TestUtils.shutdownBackend(); // Automatically TestUtils.startBackend() is called before every test suite starts we need to shut tht down and startup our new TestUtils with semantic rebase on
await TestUtils.startBackend({ useSemanticRebase: true });
});
afterEach(() => {
if (t) {
t.shutdown();
t = undefined;
}
});
after(async () => {
await TestUtils.shutdownBackend();
await TestUtils.startBackend(); // restart normal backend so subsequent test suites aren't left without IModelHost
});
it("local data changes onto incoming trivial schema change", async () => {
t = await TestIModel.initialize("TrivialSchemaIncoming");
let localTxn = startTestTxn(t.local, "local data changes onto incoming trivial schema change local");
let farTxn = startTestTxn(t.far, "local data changes onto incoming trivial schema change far");
// Local creates an element
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
const elementId = t.insertElement(localTxn, "TestDomain:C", {
propA: "value_a",
propC: "value_c",
});
localTxn.saveChanges("create element");
await pushChanges(localTxn, "create test element");
localTxn = startTestTxn(t.local, "local data changes onto incoming trivial schema change local");
// Far imports updated schema with new property PropC2
await pullChanges(farTxn);
farTxn = startTestTxn(t.far, "local data changes onto incoming trivial schema change far");
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
// Verify that we're holding a shared lock (not exclusive) for semantic rebase
chai.expect(t.far.locks.holdsSharedLock(IModel.repositoryModelId)).to.be.true;
chai.expect(t.far.holdsSchemaLock).to.be.false;
const txnProps = t.far.txns.getLastSavedTxnProps();
chai.expect(txnProps).to.not.be.undefined;
chai.expect(txnProps.type).to.equal("Schema");
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true;
await pushChanges(farTxn, "add PropC2 to class C");
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there
// Local makes local changes to the element
await t.local.locks.acquireLocks({ exclusive: elementId });
t.updateElement(localTxn, elementId, { propA: "local_update_a" });
localTxn.saveChanges("local update to propA");
// Local pulls and rebases local changes onto incoming schema change
await pullChanges(localTxn);
// Verify: local changes preserved, schema updated
const element = t.getElementProps(t.local, elementId);
chai.expect(element.propA).to.equal("local_update_a", "Local property update should be preserved");
chai.expect(element.propC).to.equal("value_c", "Original propC should be preserved");
const schema = t.local.getSchemaProps("TestDomain");
chai.expect(schema.version).to.equal("01.00.01", "Schema should be updated to v01.00.01");
});
it("local trivial schema change onto incoming data changes", async () => {
t = await TestIModel.initialize("TrivialSchemaLocal");
let localTxn = startTestTxn(t.local, "local trivial schema change onto incoming data changes local");
let farTxn = startTestTxn(t.far, "local trivial schema change onto incoming data changes far");
// Local creates an element
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
const elementId = t.insertElement(localTxn, "TestDomain:C", {
propA: "value_a",
propC: "value_c",
});
localTxn.saveChanges("create element");
await pushChanges(localTxn, "create test element");
localTxn = startTestTxn(t.local, "local trivial schema change onto incoming data changes local");
// Local imports updated schema locally
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
// Verify that we're holding a shared lock (not exclusive) for semantic rebase
chai.expect(t.local.locks.holdsSharedLock(IModel.repositoryModelId)).to.be.true;
chai.expect(t.local.holdsSchemaLock).to.be.false;
const txnProps = t.local.txns.getLastSavedTxnProps();
chai.expect(txnProps).to.not.be.undefined;
chai.expect(txnProps.type).to.equal("Schema");
chai.expect(t.checkIfFolderExists(t.local, txnProps.id, true)).to.be.true;
// Far pulls element, then updates it
await pullChanges(farTxn);
farTxn = startTestTxn(t.far, "local trivial schema change onto incoming data changes far");
await t.far.locks.acquireLocks({ exclusive: elementId });
t.updateElement(farTxn, elementId, { propA: "far_update_a" });
farTxn.saveChanges("far update to propA");
await pushChanges(farTxn, "update element propA");
// Local pulls and rebases local schema change onto incoming data changes
await pullChanges(localTxn);
chai.expect(t.checkIfFolderExists(t.local, txnProps.id, true)).to.be.true; // after rebase the folder should be there until push is called
// Verify: incoming data changes applied, local schema preserved
const element = t.getElementProps(t.local, elementId);
chai.expect(element.propA).to.equal("far_update_a", "Incoming property update should be applied");
chai.expect(element.propC).to.equal("value_c", "Original propC should be preserved");
const schema = t.local.getSchemaProps("TestDomain");
chai.expect(schema.version).to.equal("01.00.01", "Local schema update should be preserved");
});
it("local data changes onto incoming data changes", async () => {
t = await TestIModel.initialize("DataOntoData");
let localTxn = startTestTxn(t.local, "local data changes onto incoming data changes local");
let farTxn = startTestTxn(t.far, "local data changes onto incoming data changes far");
// Local creates two elements
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
const elementId1 = t.insertElement(localTxn, "TestDomain:C", {
propA: "value_a1",
propC: "value_c1",
});
const elementId2 = t.insertElement(localTxn, "TestDomain:C", {
propA: "value_a2",
propC: "value_c2",
});
localTxn.saveChanges("create elements");
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used
await pushChanges(localTxn, "create test elements");
localTxn = startTestTxn(t.local, "local data changes onto incoming data changes local");
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used
// Far updates first element
await pullChanges(farTxn);
farTxn = startTestTxn(t.far, "local data changes onto incoming data changes far");
await t.far.locks.acquireLocks({ exclusive: elementId1 });
t.updateElement(farTxn, elementId1, { propC: "far_update_c" });
farTxn.saveChanges("far update to propC");
chai.expect(t.checkifRebaseFolderExists(t.far)).to.be.false; // its data changes on both sides semantic rebase is not used
await pushChanges(farTxn, "update element propC");
chai.expect(t.checkifRebaseFolderExists(t.far)).to.be.false; // its data changes on both sides semantic rebase is not used
// Local makes local changes to second element
await t.local.locks.acquireLocks({ exclusive: elementId2 });
t.updateElement(localTxn, elementId2, { propA: "local_update_a" });
localTxn.saveChanges("local update to propA");
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used
// Local pulls and rebases
await pullChanges(localTxn);
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used
// Verify: both changes applied to their respective elements
const element1 = t.getElementProps(t.local, elementId1);
chai.expect(element1.propA).to.equal("value_a1", "Element 1 propA should be unchanged");
chai.expect(element1.propC).to.equal("far_update_c", "Element 1 incoming update should be applied");
const element2 = t.getElementProps(t.local, elementId2);
chai.expect(element2.propA).to.equal("local_update_a", "Element 2 local update should be preserved");
chai.expect(element2.propC).to.equal("value_c2", "Element 2 propC should be unchanged");
});
it("local trivial schema changes onto incoming trivial schema changes (local newer)", async () => {
t = await TestIModel.initialize("TrivialSchemaLocalNewer");
const localTxn = startTestTxn(t.local, "local trivial schema changes onto incoming trivial schema changes local newer local");
const farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming trivial schema changes local newer far");
// Far imports v01.00.01 (adds PropC2)
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
const txnProps = t.far.txns.getLastSavedTxnProps();
chai.expect(txnProps).to.not.be.undefined;
chai.expect(txnProps.type).to.equal("Schema");
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true;
await pushChanges(farTxn, "add PropC2 to class C");
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there
// Local imports v01.00.02 (adds PropC2 and PropD2 - newer)
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
const txnPropsLocal = t.local.txns.getLastSavedTxnProps();
chai.expect(txnPropsLocal).to.not.be.undefined;
chai.expect(txnPropsLocal.type).to.equal("Schema");
chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true;
// Local pulls and rebases
await pullChanges(localTxn);
chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true; // after rebase the folder should be there because local is newer until push is called
// Verify: local schema preserved (newer version)
const schema = t.local.getSchemaProps("TestDomain");
chai.expect(schema.version).to.equal("01.00.02", "Local schema (newer) should be preserved");
});
it("local trivial schema changes onto incoming trivial schema changes (incoming newer)", async () => {
t = await TestIModel.initialize("TrivialSchemaIncomingNewer");
const localTxn = startTestTxn(t.local, "local trivial schema changes onto incoming trivial schema changes incoming newer local");
const farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming trivial schema changes incoming newer far");
// Far imports v01.00.02 (adds PropC2 and PropD2 - newer)
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
const txnProps = t.far.txns.getLastSavedTxnProps();
chai.expect(txnProps).to.not.be.undefined;
chai.expect(txnProps.type).to.equal("Schema");
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true;
await pushChanges(farTxn, "update schema to v01.00.02");
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there
// Local imports v01.00.01 (adds only PropC2 - older)
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
const txnPropsLocal = t.local.txns.getLastSavedTxnProps();
chai.expect(txnPropsLocal).to.not.be.undefined;
chai.expect(txnPropsLocal.type).to.equal("Schema");
chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true;
// Local pulls and rebases
await pullChanges(localTxn);
chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.false; // after rebase the folder should not be there because incoming is newer so while rebasing it should be a no op
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // there should not be a rebase folder because the rebase folder is deleted after rebase if it contains nothing
// Verify: incoming schema preserved (newer version, local should not override)
const schema = t.local.getSchemaProps("TestDomain");
chai.expect(schema.version).to.equal("01.00.02", "Incoming schema (newer) should win, local should not override");
});
it("local trivial schema changes onto incoming identical schema changes with data changes on both sides", async () => {
t = await TestIModel.initialize("TrivialSchemaIdenticalWithData");
const localTxn = startTestTxn(t.local, "local trivial schema changes onto incoming identical schema changes with data local");
let farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming identical schema changes with data far");
// Far imports v01.00.01 (adds PropC2) and creates an element
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
const txnProps = t.far.txns.getLastSavedTxnProps();
chai.expect(txnProps).to.not.be.undefined;
chai.expect(txnProps.type).to.equal("Schema");
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true;
await pushChanges(farTxn, "add PropC2 to class C");
farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming identical schema changes with data far");
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
const farElementId = t.insertElement(farTxn, "TestDomain:C", {
propA: "far_value_a",
propC: "far_value_c",
propC2: "far_value_c2",
});
farTxn.saveChanges("far creates element with new property");
await pushChanges(farTxn, "far creates element");
// Local imports the same v01.00.01 (adds PropC2) and creates an element
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
const txnPropsLocal = t.local.txns.getLastSavedTxnProps();
chai.expect(txnPropsLocal).to.not.be.undefined;
chai.expect(txnPropsLocal.type).to.equal("Schema");
chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true;
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
const localElementId = t.insertElement(localTxn, "TestDomain:C", {
propA: "local_value_a",
propC: "local_value_c",
propC2: "local_value_c2",
});
localTxn.saveChanges("local creates element with new property");
// Local pulls and rebases
await pullChanges(localTxn);
chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.false; // after rebase the folder should not be there as both are identical
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // there should not be a rebase folder because the rebase folder is deleted after rebase if it contains nothing
// Verify: schema preserved (both sides identical)
const schema = t.local.getSchemaProps("TestDomain");
chai.expect(schema.version).to.equal("01.00.01", "Schema should be v01.00.01");
// Verify: both elements exist with their original properties
const farElement = t.getElementProps(t.local, farElementId);
chai.expect(farElement.propA).to.equal("far_value_a", "Far element propA should be preserved");
chai.expect(farElement.propC).to.equal("far_value_c", "Far element propC should be preserved");
chai.expect(farElement.propC2).to.equal("far_value_c2", "Far element propC2 should be preserved");
const localElement = t.getElementProps(t.local, localElementId);
chai.expect(localElement.propA).to.equal("local_value_a", "Local element propA should be preserved");
chai.expect(localElement.propC).to.equal("local_value_c", "Local element propC should be preserved");
chai.expect(localElement.propC2).to.equal("local_value_c2", "Local element propC2 should be preserved");
});
it("both add different properties, increment to same version number", async () => {
t = await TestIModel.initialize("TrivialSchemaIncompatible");
const localTxn = startTestTxn(t.local, "both add different properties increment to same version local");
const farTxn = startTestTxn(t.far, "both add different properties increment to same version far");
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
await pushChanges(farTxn, "add PropC2 to class C");
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC3Incompatible]);
await pullChanges(localTxn); // TODO: this currently passes, because same version number means no upgrade is attempted
//TODO: this should probably fail instead as both sides made incompatible changes to the same version, but this is unrelated to semantic rebase itself
const schema = t.local.getSchemaProps("TestDomain");
chai.expect(schema.version).to.equal("01.00.01", "Schema should be v01.00.01");
});
it("both add compatible properties, local version number higher", async () => {
t = await TestIModel.initialize("CompatibleSchemaLocalHigher");
const localTxn = startTestTxn(t.local, "both add compatible properties local version higher local");
const farTxn = startTestTxn(t.far, "both add compatible properties local version higher far");
// Far imports v01.00.01 (adds PropC2)
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
await pushChanges(farTxn, "add PropC2 to class C");
// Local imports v01.00.02 (adds PropC2 and PropD2 - compatible higher version)
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
// Local pulls and rebases
await pullChanges(localTxn);
// Verify: Local schema wins (higher version)
const schema = t.local.getSchemaProps("TestDomain");
chai.expect(schema.version).to.equal("01.00.02", "Schema should be v01.00.02 (higher version wins)");
const classC = await t.local.schemaContext.getSchemaItem("TestDomain", "C", EntityClass);
chai.expect(classC).to.not.be.undefined;
chai.expect(await classC.getProperty("PropC2")).to.exist;
const classD = await t.local.schemaContext.getSchemaItem("TestDomain", "D", EntityClass);
chai.expect(classD).to.not.be.undefined;
chai.expect(await classD.getProperty("PropD2")).to.exist;
});
it("both add compatible properties, incoming version number higher", async () => {
t = await TestIModel.initialize("CompatibleSchemaIncomingHigher");
const localTxn = startTestTxn(t.local, "both add compatible properties incoming version higher local");
const farTxn = startTestTxn(t.far, "both add compatible properties incoming version higher far");
// Far imports v01.00.02 (adds PropC2 and PropD2 - higher version)
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
await pushChanges(farTxn, "update schema to v01.00.02");
// Local imports v01.00.01 (adds only PropC2 - compatible lower version)
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
// Local pulls and rebases
await pullChanges(localTxn);
// Verify: Incoming schema wins (higher version)
const schema = t.local.getSchemaProps("TestDomain");
chai.expect(schema.version).to.equal("01.00.02", "Schema should be v01.00.02 (higher version wins)");
const classC = await t.local.schemaContext.getSchemaItem("TestDomain", "C", EntityClass);
chai.expect(classC).to.not.be.undefined;
chai.expect(await classC.getProperty("PropC2")).to.exist;
const classD = await t.local.schemaContext.getSchemaItem("TestDomain", "D", EntityClass);
chai.expect(classD).to.not.be.undefined;
chai.expect(await classD.getProperty("PropD2")).to.exist;
});
it("both add same but incompatible property, local version number higher", async () => {
t = await TestIModel.initialize("Tri