UNPKG

@itwin/core-backend

Version:
781 lines (772 loc) • 42 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { assert, 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