@itwin/core-backend
Version:
iTwin.js backend components
781 lines (772 loc) • 42 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 { assert, expect } from "chai";
import * as sinon from "sinon";
import * as path from "path";
import { BisCodeSpec, Code, ConcreteEntityTypes, ECJsNames, RelatedElement, SchemaState, } from "@itwin/core-common";
import { DefinitionElement, DefinitionModel, ElementRefersToElements, Entity, EntityReferences, IModelDb, IModelJsFs, RepositoryLink, Schema, SpatialViewDefinition, StandaloneDb, UrlLink, ViewDefinition3d, } from "../../core-backend";
import { IModelTestUtils } from "../IModelTestUtils";
import { KnownTestLocations } from "../KnownTestLocations";
import { Element } from "../../Element";
import { Schemas } from "../../Schema";
import { ClassRegistry } from "../../ClassRegistry";
import { OpenMode } from "@itwin/core-bentley";
import { EntityClass, SchemaItemKey, SchemaKey } from "@itwin/ecschema-metadata";
describe("Class Registry", () => {
let imodel;
before(() => {
const seedFileName = IModelTestUtils.resolveAssetFile("test.bim");
const testFileName = IModelTestUtils.prepareOutputFile("ClassRegistry", "ClassRegistryTest.bim");
imodel = IModelTestUtils.createSnapshotFromSeed(testFileName, seedFileName);
assert.exists(imodel);
});
after(() => {
imodel?.close();
});
it("should verify the Entity metadata of known element subclasses", async () => {
const code1 = new Code({ spec: "0x10", scope: "0x11", value: "RF1.dgn" });
const el = imodel.elements.getElement(code1);
assert.exists(el);
if (el) {
const metaData = await el.getMetaData();
assert.exists(metaData);
assert.equal(metaData.fullName, el.classFullName.replace(":", "."));
// I happen to know that this is a BisCore:RepositoryLink
assert.equal(metaData.fullName, RepositoryLink.classFullName.replace(":", "."));
// Check the metadata on the class itself
assert.isDefined(metaData.baseClass);
assert.equal(metaData.baseClass?.fullName, UrlLink.classFullName.replace(":", "."));
assert.isTrue(metaData.customAttributes?.has("BisCore.ClassHasHandler"));
// Check the metadata on the one property that RepositoryLink defines, RepositoryGuid
const repositoryGuidProperty = await metaData.getProperty("repositoryGuid");
assert.isDefined(repositoryGuidProperty);
assert.equal(repositoryGuidProperty.extendedTypeName, "BeGuid");
assert.isTrue(repositoryGuidProperty.customAttributes?.has("CoreCustomAttributes.HiddenProperty"));
}
const el2 = imodel.elements.getElement("0x34");
assert.exists(el2);
if (el2) {
const metaData = await el2.getMetaData();
assert.exists(metaData);
assert.equal(metaData.fullName, el2.classFullName.replace(":", "."));
// I happen to know that this is a BisCore.SpatialViewDefinition
assert.equal(metaData.fullName, SpatialViewDefinition.classFullName.replace(":", "."));
assert.isDefined(metaData.baseClass);
assert.equal(metaData.baseClass?.fullName, ViewDefinition3d.classFullName.replace(":", "."));
const modelSelectorProperty = await metaData.getProperty("modelSelector");
assert.isDefined(modelSelectorProperty);
assert.equal(modelSelectorProperty.relationshipClass.fullName, "BisCore.SpatialViewDefinitionUsesModelSelector");
}
});
it("should verify Entity metadata with both base class and mixin properties", async () => {
const schemaPathname = path.join(KnownTestLocations.assetsDir, "TestDomain.ecschema.xml");
await imodel.importSchemas([schemaPathname]); // will throw an exception if import fails
const testDomainClass = await imodel.schemaContext.getSchemaItem("TestDomain", "TestDomainClass", EntityClass);
assert.isDefined(testDomainClass);
assert.isDefined(testDomainClass?.baseClass);
assert.equal(testDomainClass?.baseClass?.fullName, DefinitionElement.classFullName.replace(":", "."));
assert.isDefined(testDomainClass?.mixins);
assert.equal(testDomainClass?.mixins.length, 1);
assert.equal(testDomainClass?.mixins[0].fullName, "TestDomain.IMixin");
// Verify that the forEach method which is called when constructing an entity
// is picking up all expected properties.
const testData = [];
// eslint-disable-next-line @typescript-eslint/no-deprecated
IModelDb.forEachMetaData(imodel, "TestDomain:TestDomainClass", true, (propName, _property) => {
testData.push(ECJsNames.toJsName(propName));
}, false);
const expectedString = testData.find((testString) => {
return testString === "testMixinProperty";
});
assert.isDefined(expectedString);
});
});
describe("Class Registry - getRootMetaData", () => {
let imodel;
before(async () => {
const seedFileName = IModelTestUtils.resolveAssetFile("test.bim");
const testFileName = IModelTestUtils.prepareOutputFile("ClassRegistry", "GetRootMetaData.bim");
IModelJsFs.copySync(seedFileName, testFileName);
const schemaState = StandaloneDb.validateSchemas(testFileName, true);
assert.strictEqual(schemaState, SchemaState.UpgradeRecommended);
StandaloneDb.upgradeStandaloneSchemas(testFileName);
imodel = StandaloneDb.openFile(testFileName, OpenMode.ReadWrite);
assert.exists(imodel);
await imodel.importSchemaStrings([
`<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestSchema1" alias="ts1" version="01.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1">
<ECSchemaReference name="BisCore" version="01.00" alias="bis"/>
<ECSchemaReference name="CoreCustomAttributes" version="01.00.03" alias="CoreCA"/>
<ECEntityClass typeName="TestBase">
<BaseClass>bis:PhysicalElement</BaseClass>
</ECEntityClass>
<ECEntityClass typeName="TestDerived">
<BaseClass>TestBase</BaseClass>
</ECEntityClass>
<ECEntityClass typeName="ITestMixinForAspectsBase">
<ECCustomAttributes>
<IsMixin xmlns="CoreCustomAttributes.01.00.03">
<AppliesToEntityClass>bis:ElementUniqueAspect</AppliesToEntityClass>
</IsMixin>
</ECCustomAttributes>
</ECEntityClass>
<ECEntityClass typeName="ITestMixinForAspectsDerived">
<BaseClass>ITestMixinForAspectsBase</BaseClass>
<ECCustomAttributes>
<IsMixin xmlns="CoreCustomAttributes.01.00.03">
<AppliesToEntityClass>bis:ElementUniqueAspect</AppliesToEntityClass>
</IsMixin>
</ECCustomAttributes>
</ECEntityClass>
<ECEntityClass typeName="ITestMixinForElements" modifier="Abstract">
<ECCustomAttributes>
<IsMixin xmlns="CoreCustomAttributes.01.00.03">
<AppliesToEntityClass>bis:Element</AppliesToEntityClass>
</IsMixin>
</ECCustomAttributes>
</ECEntityClass>
<ECEntityClass typeName="TestMixedInAndDerived">
<BaseClass>TestBase</BaseClass>
<BaseClass>ITestMixinForElements</BaseClass>
</ECEntityClass>
</ECSchema>
`,
]); // will throw an exception if import fails
});
after(() => {
imodel?.close();
});
it("should get the root metadata", async () => {
for (const [testClass, expectedRoot] of [
["TestSchema1.TestBase", "BisCore:Element"],
["TestSchema1.TestDerived", "BisCore:Element"],
["TestSchema1.ITestMixinForAspectsBase", "BisCore:ElementAspect"],
["TestSchema1.ITestMixinForAspectsDerived", "BisCore:ElementAspect"],
["TestSchema1.ITestMixinForElements", "BisCore:Element"],
["TestSchema1.TestMixedInAndDerived", "BisCore:Element"],
]) {
const rootMetaData = ClassRegistry.getRootEntity(imodel, testClass);
expect(rootMetaData.replace(".", ":")).to.equal(expectedRoot);
}
});
});
describe("Class Registry - generated classes", () => {
let imodel;
const testSchemaPath = path.join(KnownTestLocations.assetsDir, "TestGeneratedClasses.ecschema.xml");
before(async () => {
const seedFileName = IModelTestUtils.resolveAssetFile("test.bim");
const testFileName = IModelTestUtils.prepareOutputFile("ClassRegistry", "GeneratedClasses.bim");
imodel = IModelTestUtils.createSnapshotFromSeed(testFileName, seedFileName);
assert.exists(imodel);
await imodel.importSchemas([testSchemaPath]); // will throw an exception if import fails
});
after(() => {
imodel?.close();
});
class TestGeneratedClasses extends Schema {
static get schemaName() { return "TestGeneratedClasses"; }
static get classes() {
return [TestModelWithNavProp, TestElementWithNavProp, LinkTableRelWithNavProp, DerivedWithNavProp, Derived2, Derived3, Derived4, Derived5, Derived6];
}
static registerSchema() {
if (this !== Schemas.getRegisteredSchema(this.schemaName)) {
Schemas.unregisterSchema(this.schemaName);
Schemas.registerSchema(this);
// eslint-disable-next-line @typescript-eslint/naming-convention
for (const class_ of this.classes) {
ClassRegistry.register(class_, this);
}
}
}
static unregisterSchema() {
Schemas.unregisterSchema(this.schemaName);
}
}
class TestElementWithNavProp extends DefinitionElement {
static get className() { return "TestElementWithNavProp"; }
static schema = TestGeneratedClasses;
navProp;
constructor(props, inIModel) {
super(props, inIModel);
this.navProp = new RelatedElement(props.navProp);
}
}
class TestModelWithNavProp extends DefinitionModel {
static get className() { return "TestModelWithNavProp"; }
static schema = TestGeneratedClasses;
elemNavProp;
aspectNavProp;
relNavProp;
constructor(props, inIModel) {
super(props, inIModel);
this.elemNavProp = new RelatedElement(props.elemNavProp);
this.aspectNavProp = new RelatedElement(props.aspectNavProp);
this.relNavProp = new RelatedElement(props.relNavProp);
}
}
class LinkTableRelWithNavProp extends ElementRefersToElements {
static get className() { return "LinkTableRelWithNavProp"; }
static schema = TestGeneratedClasses;
elemNavProp;
aspectNavProp;
modelNavProp;
constructor(props, inIModel) {
super(props, inIModel);
this.elemNavProp = new RelatedElement(props.elemNavProp);
this.aspectNavProp = new RelatedElement(props.aspectNavProp);
this.modelNavProp = new RelatedElement(props.modelNavProp);
}
}
class DerivedWithNavProp extends TestElementWithNavProp {
static get className() { return "DerivedWithNavProp"; }
static schema = TestGeneratedClasses;
derivedNavProp;
constructor(props, inIModel) {
super(props, inIModel);
this.derivedNavProp = new RelatedElement(props.derivedNavProp);
}
}
class Derived2 extends DerivedWithNavProp {
static get className() { return "Derived2"; }
}
class Derived3 extends Derived2 {
static get className() { return "Derived3"; }
}
class Derived4 extends Derived3 {
static get className() { return "Derived4"; }
}
class Derived5 extends Derived4 {
static get className() { return "Derived5"; }
}
class Derived6 extends Derived5 {
static get className() { return "Derived6"; }
}
class NonGeneratedElement extends DefinitionElement {
static get className() { return "NonGeneratedElement"; }
}
it("Should provide correct schemas and full-names on generated classes", async () => {
await imodel.importSchemaStrings([
`<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="CustomA" alias="custA" version="1.0.0"
xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1">
<ECSchemaReference name="BisCore" version="01.00" alias="bis"/>
<ECSchemaReference name="Functional" version="1.00" alias="func"/>
<ECEntityClass typeName="NonGeneratedElement">
<BaseClass>bis:DefinitionElement</BaseClass>
</ECEntityClass>
<ECEntityClass typeName="GeneratedElement">
<BaseClass>NonGeneratedElement</BaseClass>
</ECEntityClass>
<ECEntityClass typeName="ElementC">
<BaseClass>GeneratedElement</BaseClass>
</ECEntityClass>
</ECSchema>
`,
`<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="CustomB" alias="custB" version="1.0.0"
xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1">
<ECSchemaReference name="BisCore" version="01.00" alias="bis"/>
<ECSchemaReference name="Functional" version="1.00" alias="func"/>
<ECSchemaReference name="CustomA" version="01.00" alias="custA"/>
<ECEntityClass typeName="DummyElement">
<BaseClass>bis:DefinitionElement</BaseClass>
</ECEntityClass>
<ECEntityClass typeName="ErrorElement">
<BaseClass>custA:ElementC</BaseClass>
</ECEntityClass>
</ECSchema>
`,
]);
class CustomASchema extends Schema {
static get schemaName() { return "CustomA"; }
static get classes() {
return [NonGeneratedElement];
}
static registerSchema() {
if (this !== Schemas.getRegisteredSchema(this.schemaName)) {
Schemas.unregisterSchema(this.schemaName);
Schemas.registerSchema(this);
for (const ecClass of this.classes) {
ClassRegistry.register(ecClass, this);
}
}
}
static unregisterSchema() {
Schemas.unregisterSchema(this.schemaName);
}
}
CustomASchema.registerSchema();
class DummyElement extends DefinitionElement {
static get className() { return "DummyElement"; }
}
class CustomBSchema extends Schema {
static get schemaName() { return "CustomB"; }
static get classes() {
return [DummyElement];
}
static registerSchema() {
if (this !== Schemas.getRegisteredSchema(this.schemaName)) {
Schemas.unregisterSchema(this.schemaName);
Schemas.registerSchema(this);
for (const ecClass of this.classes) {
ClassRegistry.register(ecClass, this);
}
}
}
static unregisterSchema() {
Schemas.unregisterSchema(this.schemaName);
}
}
CustomBSchema.registerSchema();
const nonGeneratedElement = imodel.getJsClass("CustomA:NonGeneratedElement");
assert.equal(nonGeneratedElement.classFullName, "CustomA:NonGeneratedElement");
const generatedElement = imodel.getJsClass("CustomA:GeneratedElement");
assert.equal(generatedElement.classFullName, "CustomA:GeneratedElement");
const elementC = imodel.getJsClass("CustomA:ElementC");
assert.equal(elementC.classFullName, "CustomA:ElementC");
const dummyElement = imodel.getJsClass("CustomB:DummyElement");
assert.equal(dummyElement.classFullName, "CustomB:DummyElement");
//This is the class that had the wrong schema
const errorElement = imodel.getJsClass("CustomB:ErrorElement");
assert.equal(errorElement.classFullName, "CustomB:ErrorElement");
assert.isTrue(errorElement.schemaItemKey.matches(new SchemaItemKey("ErrorElement", new SchemaKey("CustomB", 1, 0, 0))));
assert.equal(errorElement.className, "ErrorElement");
assert.equal(errorElement.schema.schemaName, "CustomB");
// Create an instance of ErrorElement
const errorElementInstance = Entity.instantiate(errorElement, { classFullName: errorElement.classFullName }, imodel);
assert.exists(errorElementInstance);
assert.instanceOf(errorElementInstance, errorElement);
assert.instanceOf(errorElementInstance, Entity);
assert.equal(errorElementInstance.className, "ErrorElement");
assert.equal(errorElementInstance.schemaName, "CustomB");
const metadata = await errorElementInstance.getMetaData();
assert.exists(metadata);
assert.equal(metadata.fullName, "CustomB.ErrorElement");
});
// if a single inherited class is not generated, the entire hierarchy is considered not-generated
it("should only generate automatic collectReferenceIds implementations for generated classes", async () => {
await imodel.importSchemas([testSchemaPath]); // will throw an exception if import fails
class GeneratedTestElementWithNavProp extends imodel.getJsClass("TestGeneratedClasses:TestElementWithNavProp") {
constructor(props) {
super(props, imodel);
}
}
const testEntityId = imodel.elements.insertElement({
classFullName: "TestGeneratedClasses:TestEntity",
prop: "sample-value",
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
});
const elemWithNavProp = new GeneratedTestElementWithNavProp({
classFullName: "TestGeneratedClasses:TestElementWithNavProp",
navProp: {
id: testEntityId,
relClassName: "TestGeneratedClasses:ElemRel",
},
});
// eslint-disable-next-line @typescript-eslint/dot-notation
assert.isDefined(GeneratedTestElementWithNavProp.prototype["collectReferenceIds"]);
expect([...elemWithNavProp.getReferenceIds()]).to.have.members([
EntityReferences.fromEntityType(elemWithNavProp.model, ConcreteEntityTypes.Model),
EntityReferences.fromEntityType(elemWithNavProp.code.scope, ConcreteEntityTypes.Element),
EntityReferences.fromEntityType(testEntityId, ConcreteEntityTypes.Element),
]);
// eslint-disable-next-line @typescript-eslint/naming-convention
const GeneratedTestAspectWithNavProp = imodel.getJsClass("TestGeneratedClasses:TestAspectWithNavProp");
assert.isTrue(GeneratedTestAspectWithNavProp.prototype.hasOwnProperty("collectReferenceIds"));
});
it("should not override collectReferenceIds for BisCore schema classes", async () => {
// AnnotationFrameStyle is an example of an unregistered bis class without an implementation of collectReferenceIds
assert.isTrue(imodel.getJsClass("BisCore:AnnotationFrameStyle").prototype.hasOwnProperty("collectReferenceIds"));
});
it("should get references from its bis superclass", async () => {
await imodel.importSchemas([testSchemaPath]); // will throw an exception if import fails
class GeneratedTestElementWithNavProp extends imodel.getJsClass("TestGeneratedClasses:TestElementWithNavProp") {
constructor(props) {
super(props, imodel);
}
}
const testEntityId = imodel.elements.insertElement({
classFullName: "TestGeneratedClasses:TestEntity",
prop: "sample-value",
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
});
const elemWithNavProp = new GeneratedTestElementWithNavProp({
classFullName: "TestGeneratedClasses:TestElementWithNavProp",
navProp: new RelatedElement({
id: testEntityId,
relClassName: "TestGeneratedClasses:ElemRel",
}),
model: IModelDb.dictionaryId,
code: new Code({
scope: IModelDb.rootSubjectId,
spec: imodel.codeSpecs.getByName(BisCodeSpec.spatialCategory).id,
value: "",
}),
parent: new RelatedElement({
// since we don't actually insert this element in this test, using an arbitrary id string
id: "0x0000ffff",
relClassName: "BisCore:ElementOwnsChildElements",
}),
});
// super class here is Element so we should get the code.scope, model and parent as references
expect([...elemWithNavProp.getReferenceIds()]).to.have.members([
EntityReferences.fromEntityType(elemWithNavProp.model, ConcreteEntityTypes.Model),
EntityReferences.fromEntityType(elemWithNavProp.code.scope, ConcreteEntityTypes.Element),
elemWithNavProp.parent && EntityReferences.fromEntityType(elemWithNavProp.parent?.id, ConcreteEntityTypes.Element),
EntityReferences.fromEntityType(testEntityId, ConcreteEntityTypes.Element),
].filter((x) => x !== undefined));
const modelTestEntityIds = new Array(2).fill(undefined).map((_, index) => imodel.elements.insertElement({
classFullName: "TestGeneratedClasses:TestEntity",
prop: `model-value-${index}`,
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
}));
// eslint-disable-next-line @typescript-eslint/naming-convention
const GeneratedTestAspectWithNavProp = imodel.getJsClass("TestGeneratedClasses:TestAspectWithNavProp");
const aspectWithNavPropId = imodel.elements.insertAspect({
classFullName: GeneratedTestAspectWithNavProp.classFullName,
navProp: { id: modelTestEntityIds[0], relClassName: "TestGeneratedClasses:NonElemRel" },
element: { id: modelTestEntityIds[1] },
});
class GeneratedTestModelWithNavProp extends imodel.getJsClass("TestGeneratedClasses:TestModelWithNavProp") {
constructor(props) {
super(props, imodel);
}
}
const modelWithNavProp = new GeneratedTestModelWithNavProp({
classFullName: GeneratedTestModelWithNavProp.classFullName,
modeledElement: { id: modelTestEntityIds[0] },
parentModel: IModelDb.dictionaryId,
elemNavProp: { id: modelTestEntityIds[1], relClassName: "TestGeneratedClasses:ModelToElemNavRel" },
aspectNavProp: { id: aspectWithNavPropId, relClassName: "TestGeneratedClasses:ModelToAspectNavRel" },
// removed due to a bug
// relNavProp: { id: relWithNavPropId, relClassName: "TestGeneratedClasses:ModelToRelNavRel" },
});
const modelWithNavPropId = modelWithNavProp.insert();
expect([...modelWithNavProp.getReferenceIds()]).to.have.members([
EntityReferences.fromEntityType(modelTestEntityIds[1], ConcreteEntityTypes.Element),
EntityReferences.fromEntityType(IModelDb.dictionaryId, ConcreteEntityTypes.Model),
EntityReferences.fromEntityType(modelTestEntityIds[0], ConcreteEntityTypes.Element),
EntityReferences.fromEntityType(aspectWithNavPropId, ConcreteEntityTypes.ElementAspect),
// ignoring this one, because there seems to be a bug when specifying a relationship instance as a nav prop
// EntityReferences.fromEntityType(relWithNavPropId, ConcreteEntityTypes.Relationship),
].filter((x) => x !== undefined));
const relTestEntityIds = new Array(3).fill(undefined).map((_, index) => imodel.elements.insertElement({
classFullName: "TestGeneratedClasses:TestEntity",
prop: `rel-value-${index}`,
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
}));
// eslint-disable-next-line @typescript-eslint/naming-convention
const GeneratedLinkTableRelWithNavProp = imodel.getJsClass("TestGeneratedClasses:LinkTableRelWithNavProp");
const relWithNavProp = new GeneratedLinkTableRelWithNavProp({
classFullName: GeneratedLinkTableRelWithNavProp.classFullName,
sourceId: relTestEntityIds[0],
targetId: relTestEntityIds[1],
elemNavProp: {
id: relTestEntityIds[2],
relClassName: "TestGeneratedClasses:LinkTableRelToElemNavRel",
},
modelNavProp: {
id: modelWithNavPropId,
relClassName: "TestGeneratedClasses:LinkTableRelToModelNavRel",
},
aspectNavProp: {
id: aspectWithNavPropId,
relClassName: "TestGeneratedClasses:LinkTableRelToAspectNavRel",
},
}, imodel);
const _relWithNavPropId = relWithNavProp.insert();
expect([...relWithNavProp.getReferenceIds()]).to.have.members([
...relTestEntityIds.map((id) => EntityReferences.fromEntityType(id, ConcreteEntityTypes.Element)),
EntityReferences.fromEntityType(modelWithNavPropId, ConcreteEntityTypes.Model),
EntityReferences.fromEntityType(aspectWithNavPropId, ConcreteEntityTypes.ElementAspect),
]);
});
it("should not override custom registered schema class implementations of collectReferenceIds", async () => {
const testImplReferenceId = "TEST-INVALID-ID";
class MyTestElementWithNavProp extends TestElementWithNavProp {
collectReferenceIds(referenceIds) {
super.collectReferenceIds(referenceIds);
referenceIds.addElement(testImplReferenceId);
}
}
class MyTestGeneratedClasses extends TestGeneratedClasses {
static get classes() {
return [MyTestElementWithNavProp, Derived2, Derived3, Derived4, Derived5, Derived6];
}
}
imodel.jsClassMap.clear();
MyTestGeneratedClasses.registerSchema();
// eslint-disable-next-line @typescript-eslint/naming-convention
const ActualTestElementWithNavProp = imodel.getJsClass(TestElementWithNavProp.classFullName);
const testElementWithNavPropCollectReferencesSpy = sinon.spy(ActualTestElementWithNavProp.prototype, "collectReferenceIds");
class ActualDerivedWithNavProp extends imodel.getJsClass(DerivedWithNavProp.classFullName) {
constructor(props) {
super(props, imodel);
}
}
const testEntity1Id = imodel.elements.insertElement({
classFullName: "TestGeneratedClasses:TestEntity",
prop: "sample-value-1",
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
});
const testEntity2Id = imodel.elements.insertElement({
classFullName: "TestGeneratedClasses:TestEntity",
prop: "sample-value-2",
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
});
const elemWithNavProp = new ActualTestElementWithNavProp({
classFullName: TestElementWithNavProp.classFullName,
navProp: {
id: testEntity1Id,
relClassName: "TestGeneratedClasses:ElemRel",
},
}, imodel);
// eslint-disable-next-line @typescript-eslint/unbound-method
assert.isDefined(ActualTestElementWithNavProp.prototype.collectReferenceIds);
expect([...elemWithNavProp.getReferenceIds()]).to.have.members([
EntityReferences.fromEntityType(elemWithNavProp.model, ConcreteEntityTypes.Model),
EntityReferences.fromEntityType(elemWithNavProp.code.scope, ConcreteEntityTypes.Element),
elemWithNavProp.parent && EntityReferences.fromEntityType(elemWithNavProp.parent?.id, ConcreteEntityTypes.Element),
EntityReferences.fromEntityType(testImplReferenceId, ConcreteEntityTypes.Element),
].filter((x) => x !== undefined));
expect(testElementWithNavPropCollectReferencesSpy.called).to.be.true;
testElementWithNavPropCollectReferencesSpy.resetHistory();
const derivedElemWithNavProp = new ActualDerivedWithNavProp({
classFullName: DerivedWithNavProp.classFullName,
navProp: {
id: testEntity1Id,
relClassName: "TestGeneratedClasses:ElemRel",
},
derivedNavProp: {
id: testEntity2Id,
relClassName: "TestGeneratedClasses:DerivedElemRel",
},
});
// eslint-disable-next-line @typescript-eslint/dot-notation
assert.isDefined(ActualDerivedWithNavProp.prototype["collectReferenceIds"]);
// This demonstrates that if a non-generated class has a registered non-biscore base, it will not get a generated impl,
expect([...derivedElemWithNavProp.getReferenceIds()]).to.have.members([
EntityReferences.fromEntityType(elemWithNavProp.model, ConcreteEntityTypes.Model),
EntityReferences.fromEntityType(elemWithNavProp.code.scope, ConcreteEntityTypes.Element),
elemWithNavProp.parent && EntityReferences.fromEntityType(elemWithNavProp.parent?.id, ConcreteEntityTypes.Element),
EntityReferences.fromEntityType(testImplReferenceId, ConcreteEntityTypes.Element),
].filter((x) => x !== undefined));
// explicitly check we called the super function
// (we already know its implementation was called, because testImplReferenceId is in the derived call's result)
expect(testElementWithNavPropCollectReferencesSpy.called).to.be.true;
sinon.restore();
MyTestGeneratedClasses.unregisterSchema();
});
it("should work along a complex chain of overrides", async () => {
class MyDerived2 extends Derived2 {
collectReferenceIds(referenceIds) {
super.collectReferenceIds(referenceIds);
referenceIds.addElement("derived-2");
}
}
class MyDerived4 extends Derived4 {
collectReferenceIds(referenceIds) {
super.collectReferenceIds(referenceIds);
referenceIds.addElement("derived-4");
}
}
class MyTestGeneratedClasses extends TestGeneratedClasses {
static get classes() {
// leaving Derived3,5,6 generated
return [MyDerived2, MyDerived4];
}
}
imodel.jsClassMap.clear();
MyTestGeneratedClasses.registerSchema();
/* eslint-disable @typescript-eslint/naming-convention */
const ActualTestElementWithNavProp = imodel.getJsClass("TestGeneratedClasses:TestElementWithNavProp");
const ActualDerivedWithNavProp = imodel.getJsClass("TestGeneratedClasses:DerivedWithNavProp");
const ActualDerived2 = imodel.getJsClass("TestGeneratedClasses:Derived2");
const ActualDerived3 = imodel.getJsClass("TestGeneratedClasses:Derived3");
const ActualDerived4 = imodel.getJsClass("TestGeneratedClasses:Derived4");
const ActualDerived5 = imodel.getJsClass("TestGeneratedClasses:Derived5");
const ActualDerived6 = imodel.getJsClass("TestGeneratedClasses:Derived6");
expect(ActualTestElementWithNavProp.isGeneratedClass).to.be.true;
expect(ActualDerivedWithNavProp.isGeneratedClass).to.be.true;
expect(ActualDerived2.isGeneratedClass).to.be.false;
expect(ActualDerived3.isGeneratedClass).to.be.true;
expect(ActualDerived4.isGeneratedClass).to.be.false;
expect(ActualDerived5.isGeneratedClass).to.be.true;
expect(ActualDerived6.isGeneratedClass).to.be.true;
assert.isTrue(ActualTestElementWithNavProp.prototype.hasOwnProperty("collectReferenceIds")); // should have automatic impl
assert.isTrue(ActualDerivedWithNavProp.prototype.hasOwnProperty("collectReferenceIds"));
assert.isTrue(ActualDerived2.prototype.hasOwnProperty("collectReferenceIds")); // non-generated; manually implements so has method
assert.isFalse(ActualDerived3.prototype.hasOwnProperty("collectReferenceIds")); // base is non-generated so it shouldn't get the automatic impl
assert.isTrue(ActualDerived4.prototype.hasOwnProperty("collectReferenceIds")); // manually implements so it should have the method
assert.isFalse(ActualDerived5.prototype.hasOwnProperty("collectReferenceIds")); // ancestor is non-generated so it shouldn't get the automatic impl
assert.isFalse(ActualDerived6.prototype.hasOwnProperty("collectReferenceIds")); // ancestor is non-generated so it shouldn't get the automatic impl
const testEntity1Id = imodel.elements.insertElement({
classFullName: "TestGeneratedClasses:Derived6",
prop: "sample-value-1",
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
});
const testEntity2Id = imodel.elements.insertElement({
classFullName: "TestGeneratedClasses:TestEntity",
prop: "sample-value-2",
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
});
const derived6Id = imodel.elements.insertElement({
classFullName: Derived6.classFullName,
model: IModelDb.dictionaryId,
code: Code.createEmpty(),
navProp: {
id: testEntity1Id,
relClassName: "TestGeneratedClasses:ElemRel",
},
derivedNavProp: {
id: testEntity2Id,
relClassName: "TestGeneratedClasses:DerivedElemRel",
},
});
const derived6 = imodel.elements.getElement(derived6Id);
/** it is not possible to make a spy of an already existing spy, so lazy try making one
* this is necessary since due to prototypes, some "methods" we listen to are actually the same
*/
function spyCollectReferenceIds(cls) {
if (cls.prototype.collectReferenceIds.isSinonProxy) {
return cls.prototype.collectReferenceIds;
}
return sinon.spy(cls.prototype, "collectReferenceIds");
}
const elementMethodSpy = spyCollectReferenceIds(Element);
const testElementWithNavPropSpy = spyCollectReferenceIds(ActualTestElementWithNavProp);
const derivedWithNavPropSpy = spyCollectReferenceIds(ActualDerivedWithNavProp);
const derived2Spy = spyCollectReferenceIds(ActualDerived2);
const derived3Spy = spyCollectReferenceIds(ActualDerived3);
const derived4Spy = spyCollectReferenceIds(ActualDerived4);
const derived5Spy = spyCollectReferenceIds(ActualDerived5);
const derived6Spy = spyCollectReferenceIds(ActualDerived6);
// This demonstrates that if a generated class (Derived6) has a non-generated ancestor, it will not get a generated impl
// instead it will just call the closest non-generated ancestor (Derived4)
expect([...derived6.getReferenceIds()]).to.have.members([
EntityReferences.fromEntityType(derived6.model, ConcreteEntityTypes.Model),
EntityReferences.fromEntityType(derived6.code.scope, ConcreteEntityTypes.Element),
derived6.parent?.id && EntityReferences.fromEntityType(derived6.parent.id, ConcreteEntityTypes.Element),
// "TestGeneratedClasses:Derived4" is MyDerived4 above, which extends the Derived4 class, which extends up
// without any custom ancestor implementing collectReferenceIds, so Element.collectReferenceIds is called as the
// super, and no navigation properties or other custom implementations are called so we only get "derived-4"
EntityReferences.fromEntityType("derived-4", ConcreteEntityTypes.Element),
].filter((x) => x !== undefined));
expect(elementMethodSpy.called).to.be.true; // this is the `super.collectReferenceIds` call in MyDerived4
expect(testElementWithNavPropSpy.called).to.be.false;
expect(derivedWithNavPropSpy.called).to.be.false;
// these are the same (tested below)
expect(derived2Spy.called).to.be.false;
expect(derived3Spy.called).to.be.false;
// these are all the same (tested below)
expect(derived4Spy.called).to.be.true;
expect(derived5Spy.called).to.be.true;
expect(derived6Spy.called).to.be.true;
expect(new Set([
Element,
ActualTestElementWithNavProp,
ActualDerivedWithNavProp,
Derived2,
Derived3, // same as above (so will be removed from set)
Derived4,
Derived5, // save as above (so will be removed from set)
Derived6, // save as above (so will be removed from set)
].map((e) => e.prototype["collectReferenceIds"]))).to.deep.equal(new Set([
Element,
ActualTestElementWithNavProp,
ActualDerivedWithNavProp,
Derived2,
Derived4,
].map((e) => e.prototype["collectReferenceIds"])));
MyTestGeneratedClasses.unregisterSchema();
sinon.restore();
});
});
class Base {
static staticProperty = "base";
static get sqlName() { return `s.${this.staticProperty}`; }
}
class Derived extends Base {
}
describe("Static Properties", () => {
it("should be inherited, and the subclass should get its own copy", async () => {
assert.equal(Base.staticProperty, "base");
assert.equal(Derived.staticProperty, "base"); // Derived inherits Base's staticProperty (via its prototype)
Derived.staticProperty = "derived"; // Derived now gets its own copy of staticProperty
assert.equal(Base.staticProperty, "base"); // Base's staticProperty remains as it was
assert.equal(Derived.staticProperty, "derived"); // Derived's staticProperty is now different
assert.equal(Base.sqlName, "s.base");
const d = new Derived();
assert.equal(d.constructor.staticProperty, "derived"); // Instances of Derived see Derived.staticProperty
const b = new Base();
assert.equal(b.constructor.staticProperty, "base"); // Instances of Base see Base.staticProperty
});
});
describe("Global state of ClassRegistry", () => {
let imodel1;
let imodel2;
before(() => {
const seedFileName = IModelTestUtils.resolveAssetFile("test.bim");
const testFileName1 = IModelTestUtils.prepareOutputFile("ClassRegistry", "GlobalState1.bim");
const testFileName2 = IModelTestUtils.prepareOutputFile("ClassRegistry", "GlobalState2.bim");
imodel1 = IModelTestUtils.createSnapshotFromSeed(testFileName1, seedFileName);
imodel2 = IModelTestUtils.createSnapshotFromSeed(testFileName2, seedFileName);
assert.exists(imodel1);
assert.exists(imodel2);
});
after(() => {
imodel1?.close();
imodel2?.close();
});
it("registering a class in different imodels should not affect each other", async () => {
await imodel1.importSchemaStrings([
`<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestSchema" 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="TestClass">
<BaseClass>bis:PhysicalElement</BaseClass>
<ECProperty propertyName="foo" typeName="string"/>
<ECNavigationProperty propertyName="RelatedTestClass" relationshipName="MyRelationship" direction="backward"/>
</ECEntityClass>
<ECRelationshipClass typeName="MyRelationship" modifier="None" strength="embedding">
<Source multiplicity="(0..1)" roleLabel="has" polymorphic="false">
<Class class="TestClass"/>
</Source>
<Target multiplicity="(0..*)" roleLabel="has" polymorphic="false">
<Class class="TestClass"/>
</Target>
</ECRelationshipClass>
</ECSchema>
`,
]);
await imodel2.importSchemaStrings([
`<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="TestSchema" 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="TestClass">
<BaseClass>bis:PhysicalElement</BaseClass>
<ECProperty propertyName="bar" typeName="string"/>
</ECEntityClass>
</ECSchema>
`,
]);
const testClass1 = imodel1.getJsClass("TestSchema:TestClass");
assert.isFalse(testClass1.hasOwnProperty("testPropertyGuard"));
testClass1.testPropertyGuard = true;
assert.isTrue(testClass1.hasOwnProperty("testPropertyGuard"));
const testClass2 = imodel2.getJsClass("TestSchema:TestClass");
assert.isFalse(testClass2.hasOwnProperty("testPropertyGuard"));
});
});
// TODO: add tests on the new model/aspect prefixes
//# sourceMappingURL=ClassRegistry.test.js.map