UNPKG

@itwin/core-backend

Version:
289 lines • 18.5 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 { Id64 } from "@itwin/core-bentley"; import { Code, IModel, QueryBinder, QueryRowFormat } from "@itwin/core-common"; import { withEditTxn } from "../../EditTxn"; import { getRuntimeClass, reshapeInstanceRow, reshapePropertyValue } from "../../internal/ECSqlInstanceReshaper"; import { IModelJsFs, SnapshotDb } from "../../core-backend"; import { IModelTestUtils } from "../IModelTestUtils"; /* eslint-disable @typescript-eslint/naming-convention */ // Direct unit tests for the ECSqlInstanceReshaper helper, which reshapes rows/values queried with the // non-deprecated QueryRowFormat.UseECSqlPropertyNames row format back into the legacy shape historically // produced by the deprecated QueryRowFormat.UseJsPropertyNames row format. These call the helper's exported // functions directly against real ECSQL rows, rather than only exercising them indirectly through // ElementAspect/Relationship/annotation field call sites (which are covered elsewhere). describe("ECSqlInstanceReshaper", () => { const schemaName = "ReshaperTest"; const schemaAlias = "rst"; const subDirName = "ECSqlInstanceReshaper"; let iModel; let element1Id; let element2Id; let aspectId; let relationshipId; before(async () => { const schema = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="${schemaName}" alias="${schemaAlias}" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.04" alias="bis"/> <ECStructClass typeName="PointStruct" modifier="None"> <ECProperty propertyName="Label" typeName="string"/> <ECProperty propertyName="P3d" typeName="point3d"/> </ECStructClass> <ECEntityClass typeName="ReshaperElement" modifier="None"> <BaseClass>bis:DefinitionElement</BaseClass> </ECEntityClass> <ECEntityClass typeName="ReshaperAspect" modifier="None"> <BaseClass>bis:ElementUniqueAspect</BaseClass> <ECProperty propertyName="P2d" typeName="point2d"/> <ECProperty propertyName="P3d" typeName="point3d"/> <ECStructProperty propertyName="St" typeName="PointStruct"/> <ECArrayProperty propertyName="ArrayI" typeName="int" minOccurs="0" maxOccurs="unbounded"/> <ECArrayProperty propertyName="ArrayP3d" typeName="point3d" minOccurs="0" maxOccurs="unbounded"/> <ECStructArrayProperty propertyName="ArraySt" typeName="PointStruct" minOccurs="0" maxOccurs="unbounded"/> </ECEntityClass> <ECRelationshipClass typeName="ReshaperRelatesToElement" strength="referencing" modifier="Sealed"> <BaseClass>bis:ElementRefersToElements</BaseClass> <Source multiplicity="(0..*)" roleLabel="refers to" polymorphic="true"> <Class class="ReshaperElement"/> </Source> <Target multiplicity="(0..*)" roleLabel="is referenced by" polymorphic="true"> <Class class="ReshaperElement"/> </Target> <ECProperty propertyName="Label" typeName="string"/> </ECRelationshipClass> <ECEnumeration typeName="ReshaperNotAClass" backingTypeName="int" isStrict="true"> <ECEnumerator name="Value0" value="0"/> </ECEnumeration> </ECSchema>`; const testFileName = IModelTestUtils.prepareOutputFile(subDirName, "ECSqlInstanceReshaper.bim"); const schemaPath = IModelTestUtils.prepareOutputFile(subDirName, `${schemaName}.01.00.00.xml`); IModelJsFs.writeFileSync(schemaPath, schema); iModel = SnapshotDb.createEmpty(testFileName, { rootSubject: { name: "ECSqlInstanceReshaperTest" } }); await iModel.importSchemas([schemaPath]); withEditTxn(iModel, (txn) => { element1Id = txn.insertElement({ classFullName: `${schemaName}:ReshaperElement`, model: IModel.dictionaryId, code: Code.createEmpty(), }); element2Id = txn.insertElement({ classFullName: `${schemaName}:ReshaperElement`, model: IModel.dictionaryId, code: Code.createEmpty(), }); assert.isTrue(Id64.isValidId64(element1Id)); assert.isTrue(Id64.isValidId64(element2Id)); aspectId = txn.insertAspect({ classFullName: `${schemaName}:ReshaperAspect`, element: { id: element1Id }, p2d: { x: 100, y: 200 }, p3d: { x: 1, y: 2, z: 3 }, st: { label: "struct-label", p3d: { x: 4, y: 5, z: 6 } }, arrayI: [10, 20, 30], arrayP3d: [{ x: 13, y: 14, z: 15 }, { x: 16, y: 17, z: 18 }], arraySt: [{ label: "arr-0", p3d: { x: 7, y: 8, z: 9 } }, { label: "arr-1", p3d: { x: 10, y: 11, z: 12 } }], }); assert.isTrue(Id64.isValidId64(aspectId)); relationshipId = txn.insertRelationship({ classFullName: `${schemaName}:ReshaperRelatesToElement`, sourceId: element1Id, targetId: element2Id, label: "rel-label", }); assert.isTrue(Id64.isValidId64(relationshipId)); }); }); after(() => { iModel.close(); }); it("getRuntimeClass resolves a runtime class by full class name and throws for unknown classes", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); assert.equal(ecClass.name, "ReshaperAspect"); assert.equal(ecClass.schema.name, schemaName); assert.throws(() => getRuntimeClass(iModel, `${schemaName}:DoesNotExist`), /Class not found/); }); it("getRuntimeClass throws when the resolved schema item exists but isn't a class", () => { assert.throws(() => getRuntimeClass(iModel, `${schemaName}:ReshaperNotAClass`), /Class not found/); }); it("reshapeInstanceRow reshapes an ElementAspect row: system properties, points, struct, primitive array, point array, and struct array", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const row = iModel.withQueryReader(`SELECT * FROM ${schemaAlias}.ReshaperAspect WHERE ECInstanceId=:aspectId`, (reader) => { assert.isTrue(reader.step()); return reader.current.toRow(); }, new QueryBinder().bindId("aspectId", aspectId), { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); const reshaped = reshapeInstanceRow(row, ecClass, iModel); // system properties: ECInstanceId -> id, ECClassId -> className (dot-separated) assert.equal(reshaped.id, aspectId); assert.equal(reshaped.className, `${schemaName}.ReshaperAspect`); // inherited navigation property from ElementUniqueAspect assert.deepEqual(reshaped.element, { id: element1Id, relClassName: "BisCore.ElementOwnsUniqueAspect" }); // point2d property: first letter lowercased, X/Y -> x/y assert.deepEqual(reshaped.p2d, { x: 100, y: 200 }); // point3d property: first letter lowercased, X/Y/Z -> x/y/z assert.deepEqual(reshaped.p3d, { x: 1, y: 2, z: 3 }); // struct property: first letter lowercased, and its members recursively reshaped (including nested point) assert.deepEqual(reshaped.st, { label: "struct-label", p3d: { x: 4, y: 5, z: 6 } }); // primitive (non-point) array: values pass through unchanged, only the key is lowercased assert.deepEqual(reshaped.arrayI, [10, 20, 30]); // point3d array: each scalar element reshaped like a standalone point, not just key-renamed assert.deepEqual(reshaped.arrayP3d, [{ x: 13, y: 14, z: 15 }, { x: 16, y: 17, z: 18 }]); // struct array: each element recursively reshaped like a standalone struct assert.deepEqual(reshaped.arraySt, [ { label: "arr-0", p3d: { x: 7, y: 8, z: 9 } }, { label: "arr-1", p3d: { x: 10, y: 11, z: 12 } }, ]); }); it("reshapeInstanceRow reshapes a Relationship row: source/target system properties and a primitive property", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperRelatesToElement`); const row = iModel.withQueryReader(`SELECT * FROM ${schemaAlias}.ReshaperRelatesToElement WHERE ECInstanceId=:relationshipId`, (reader) => { assert.isTrue(reader.step()); return reader.current.toRow(); }, new QueryBinder().bindId("relationshipId", relationshipId), { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); const reshaped = reshapeInstanceRow(row, ecClass, iModel); assert.equal(reshaped.id, relationshipId); assert.equal(reshaped.className, `${schemaName}.ReshaperRelatesToElement`); assert.equal(reshaped.sourceId, element1Id); assert.equal(reshaped.sourceClassName, `${schemaName}.ReshaperElement`); assert.equal(reshaped.targetId, element2Id); assert.equal(reshaped.targetClassName, `${schemaName}.ReshaperElement`); assert.equal(reshaped.label, "rel-label"); }); it("reshapeInstanceRow omits null/undefined members instead of propagating them", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const row = { ECInstanceId: aspectId, P3d: null, St: undefined, ArrayI: [10, 20, 30] }; const reshaped = reshapeInstanceRow(row, ecClass, iModel); assert.equal(reshaped.id, aspectId); assert.isUndefined(reshaped.p3d); assert.isUndefined(reshaped.st); assert.deepEqual(reshaped.arrayI, [10, 20, 30]); }); it("reshapePropertyValue reshapes a single Point2d, Point3d, and struct property value in isolation", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const p2dProp = ecClass.getPropertySync("P2d"); const p3dProp = ecClass.getPropertySync("P3d"); const stProp = ecClass.getPropertySync("St"); assert.exists(p2dProp); assert.exists(p3dProp); assert.exists(stProp); assert.deepEqual(reshapePropertyValue({ X: 100, Y: 200 }, p2dProp, iModel), { x: 100, y: 200 }); assert.deepEqual(reshapePropertyValue({ X: 1, Y: 2, Z: 3 }, p3dProp, iModel), { x: 1, y: 2, z: 3 }); assert.deepEqual(reshapePropertyValue({ Label: "hello", P3d: { X: 1, Y: 2, Z: 3 } }, stProp, iModel), { label: "hello", p3d: { x: 1, y: 2, z: 3 } }); }); it("reshapePropertyValue reshapes an array of Point3d scalars, not just a key rename", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const arrayP3dProp = ecClass.getPropertySync("ArrayP3d"); assert.exists(arrayP3dProp); assert.isTrue(arrayP3dProp.isArray()); const reshaped = reshapePropertyValue([{ X: 13, Y: 14, Z: 15 }, { X: 16, Y: 17, Z: 18 }], arrayP3dProp, iModel); assert.deepEqual(reshaped, [{ x: 13, y: 14, z: 15 }, { x: 16, y: 17, z: 18 }]); }); it("reshapePropertyValue reshapes a navigation property value into {id, relClassName}", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const elementProp = ecClass.getPropertySync("Element"); assert.exists(elementProp); assert.isTrue(elementProp.isNavigation()); const row = iModel.withQueryReader(`SELECT Element FROM ${schemaAlias}.ReshaperAspect WHERE ECInstanceId=:aspectId`, (reader) => { assert.isTrue(reader.step()); return reader.current.toRow(); }, new QueryBinder().bindId("aspectId", aspectId), { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); const reshaped = reshapePropertyValue(row.Element, elementProp, iModel); expect(reshaped).to.deep.equal({ id: element1Id, relClassName: "BisCore.ElementOwnsUniqueAspect" }); }); it("reshapePropertyValue falls back to the raw classId instead of throwing when a navigation property's relationship class can't be resolved", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const elementProp = ecClass.getPropertySync("Element"); assert.exists(elementProp); const unresolvableClassId = "0xdeadbeef"; const reshaped = reshapePropertyValue({ Id: element1Id, RelECClassId: unresolvableClassId }, elementProp, iModel); assert.deepEqual(reshaped, { id: element1Id, relClassName: unresolvableClassId }); }); it("reshapePropertyValue omits relClassName entirely when a navigation property's relationship class Id is unset", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const elementProp = ecClass.getPropertySync("Element"); assert.exists(elementProp); const reshaped = reshapePropertyValue({ Id: element1Id, RelECClassId: undefined }, elementProp, iModel); assert.deepEqual(reshaped, { id: element1Id }); assert.isFalse("relClassName" in reshaped); }); it("reshapeInstanceRow falls back to the raw classId instead of throwing when a system ECClassId column can't be resolved", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const unresolvableClassId = "0xdeadbeef"; const reshaped = reshapeInstanceRow({ ECInstanceId: aspectId, ECClassId: unresolvableClassId }, ecClass, iModel); assert.equal(reshaped.id, aspectId); assert.equal(reshaped.className, unresolvableClassId); }); it("reshapePropertyValue preserves a null/undefined struct-array element as an empty object, matching the native row adaptor", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const arrayStProp = ecClass.getPropertySync("ArraySt"); assert.exists(arrayStProp); // A null struct-array element is rendered by the native row adaptor as an empty object placeholder // (unlike primitive arrays, where null elements are dropped entirely), preserving the array's length // and the positions of surrounding elements. The underlying query reader already normalizes a null // struct-array element to `{}` before it reaches the reshaper, but the reshaper is also exercised // directly here against raw `null`/`undefined` inputs to keep it robust regardless of caller. const reshaped = reshapePropertyValue([{ Label: "arr-0", P3d: { X: 1, Y: 2, Z: 3 } }, null, undefined, { Label: "arr-3", P3d: { X: 4, Y: 5, Z: 6 } }], arrayStProp, iModel); assert.deepEqual(reshaped, [ { label: "arr-0", p3d: { x: 1, y: 2, z: 3 } }, {}, {}, { label: "arr-3", p3d: { x: 4, y: 5, z: 6 } }, ]); }); it("reshapePropertyValue drops null/undefined elements from a primitive array, matching the native row adaptor", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const arrayIProp = ecClass.getPropertySync("ArrayI"); assert.exists(arrayIProp); const reshaped = reshapePropertyValue([10, null, undefined, 30], arrayIProp, iModel); assert.deepEqual(reshaped, [10, 30]); }); it("reshapeInstanceRow reshapes a row containing null array elements without throwing, matching the native row adaptor", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); let probeAspectId; withEditTxn(iModel, (txn) => { probeAspectId = txn.insertAspect({ classFullName: `${schemaName}:ReshaperAspect`, element: { id: element1Id }, arrayI: [10, null, 30], arraySt: [{ label: "a" }, null, { label: "c" }], }); }); const row = iModel.withQueryReader(`SELECT * FROM ${schemaAlias}.ReshaperAspect WHERE ECInstanceId=:id`, (reader) => { assert.isTrue(reader.step()); return reader.current.toRow(); }, new QueryBinder().bindId("id", probeAspectId), { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); const reshaped = reshapeInstanceRow(row, ecClass, iModel); assert.deepEqual(reshaped.arrayI, [10, 30]); assert.deepEqual(reshaped.arraySt, [{ label: "a" }, {}, { label: "c" }]); }); it("reshapeInstanceRow omits a null struct member from a real queried row, matching the native row adaptor", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); let probeAspectId; withEditTxn(iModel, (txn) => { probeAspectId = txn.insertAspect({ classFullName: `${schemaName}:ReshaperAspect`, element: { id: element1Id }, st: { label: null, p3d: { x: 1, y: 2, z: 3 } }, }); }); const row = iModel.withQueryReader(`SELECT * FROM ${schemaAlias}.ReshaperAspect WHERE ECInstanceId=:id`, (reader) => { assert.isTrue(reader.step()); return reader.current.toRow(); }, new QueryBinder().bindId("id", probeAspectId), { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); const reshaped = reshapeInstanceRow(row, ecClass, iModel); assert.isFalse("label" in reshaped.st); assert.deepEqual(reshaped.st.p3d, { x: 1, y: 2, z: 3 }); }); it("reshapeInstanceRow passes through an aliased/computed column with only its key lowercased", () => { const ecClass = getRuntimeClass(iModel, `${schemaName}:ReshaperAspect`); const row = iModel.withQueryReader(`SELECT *, 1+1 AS Foo FROM ${schemaAlias}.ReshaperAspect WHERE ECInstanceId=:aspectId`, (reader) => { assert.isTrue(reader.step()); return reader.current.toRow(); }, new QueryBinder().bindId("aspectId", aspectId), { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); const reshaped = reshapeInstanceRow(row, ecClass, iModel); assert.equal(reshaped.foo, 2); }); }); //# sourceMappingURL=ECSqlInstanceReshaper.test.js.map