diff --git a/common/api/core-backend.api.md b/common/api/core-backend.api.md index db6c53d13267..8beeefe93791 100644 --- a/common/api/core-backend.api.md +++ b/common/api/core-backend.api.md @@ -173,6 +173,7 @@ import { Polyface } from '@itwin/core-geometry'; import { PolyfaceData } from '@itwin/core-geometry'; import { PolyfaceVisitor } from '@itwin/core-geometry'; import { PropertyCallback } from '@itwin/core-common'; +import { PropertyMetaDataCallback } from '@itwin/core-common'; import { QueryBinder } from '@itwin/core-common'; import { QueryOptions } from '@itwin/core-common'; import { Range2d } from '@itwin/core-geometry'; @@ -2253,7 +2254,7 @@ export class Entity { protected collectReferenceConcreteIds: (_referenceIds: EntityReferenceSet) => void; // @beta protected collectReferenceIds(_referenceIds: EntityReferenceSet): void; - forEachProperty(func: PropertyCallback, includeCustom?: boolean): void; + forEachProperty(func: PropertyCallback | PropertyMetaDataCallback, includeCustom?: boolean): void; // @internal @deprecated getReferenceConcreteIds: () => EntityReferenceSet; // @beta @@ -3068,8 +3069,8 @@ export abstract class IModelDb extends IModel { get fontMap(): FontMap; // (undocumented) protected _fontMap?: FontMap; - static forEachMetaData(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom?: boolean): void; - forEachMetaData(classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom?: boolean): void; + static forEachMetaData(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyMetaDataCallback | PropertyCallback, includeCustom?: boolean): void; + forEachMetaData(classFullName: string, wantSuper: boolean, func: PropertyMetaDataCallback | PropertyCallback, includeCustom?: boolean): void; generateElementGraphics(request: ElementGraphicsRequestProps): Promise; getBriefcaseId(): BriefcaseId; getGeoCoordinatesFromIModelCoordinates(props: GeoCoordinatesRequestProps): Promise; diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md index 49af16513965..44be4fd89759 100644 --- a/common/api/core-common.api.md +++ b/common/api/core-common.api.md @@ -1815,7 +1815,7 @@ export enum CurrentImdlVersion { Minor = 0 } -// @beta +// @public export interface CustomAttribute { ecclass: string; properties: { @@ -2910,37 +2910,38 @@ export interface EntityIdAndClassId { // @public export type EntityIdAndClassIdIterable = Iterable>; -// @beta -export class EntityMetaData implements EntityMetaDataProps { +// @public +export class EntityMetaData { constructor(jsonObj: EntityMetaDataProps); readonly baseClasses: string[]; readonly classId: Id64String; readonly customAttributes?: CustomAttribute[]; - // (undocumented) readonly description?: string; - // (undocumented) readonly displayLabel?: string; readonly ecclass: string; - // (undocumented) + // @internal + getMutableProperties(): Iterable; + getProperties(): Iterable>; + getProperty(name: string): Readonly | undefined; readonly modifier?: string; - readonly properties: { + // @deprecated + get properties(): { [propName: string]: PropertyMetaData; }; } -// @beta (undocumented) +// @public export interface EntityMetaDataProps { baseClasses: string[]; - // (undocumented) classId: Id64String; customAttributes?: CustomAttribute[]; - // (undocumented) description?: string; - // (undocumented) displayLabel?: string; - // (undocumented) ecclass: string; - // (undocumented) modifier?: string; properties: { [propName: string]: PropertyMetaData; @@ -6818,7 +6819,7 @@ export interface PrimaryTileTreeId { type: BatchType.Primary; } -// @beta +// @public export enum PrimitiveTypeCode { // (undocumented) Binary = 257, @@ -6922,52 +6923,39 @@ export interface ProjectionProps { zoneNumber?: number; } -// @beta +// @public @deprecated export type PropertyCallback = (name: string, meta: PropertyMetaData) => void; -// @beta +// @public export class PropertyMetaData implements PropertyMetaDataProps { constructor(jsonObj: PropertyMetaDataProps); + // @deprecated createProperty(jsonObj: any): any; - // (undocumented) customAttributes?: CustomAttribute[]; - // (undocumented) description?: string; - // (undocumented) direction?: string; - // (undocumented) displayLabel?: string; - // (undocumented) extendedType?: string; - // (undocumented) isCustomHandled?: boolean; - // (undocumented) + // @deprecated (undocumented) isCustomHandledOrphan?: boolean; get isNavigation(): boolean; - // (undocumented) kindOfQuantity?: string; - // (undocumented) maximumLength?: number; - // (undocumented) maximumValue?: any; - // (undocumented) maxOccurs?: number; - // (undocumented) minimumLength?: number; - // (undocumented) minimumValue?: any; - // (undocumented) minOccurs?: number; - // (undocumented) primitiveType?: PrimitiveTypeCode; - // (undocumented) readOnly?: boolean; - // (undocumented) relationshipClass?: string; - // (undocumented) structName?: string; } +// @public +export type PropertyMetaDataCallback = (name: string, meta: Readonly) => void; + // @public (undocumented) export class PropertyMetaDataMap implements Iterable { // (undocumented) @@ -6985,43 +6973,26 @@ export class PropertyMetaDataMap implements Iterable { readonly properties: QueryPropertyMetaData[]; } -// @beta (undocumented) +// @public export interface PropertyMetaDataProps { - // (undocumented) customAttributes?: CustomAttribute[]; - // (undocumented) description?: string; - // (undocumented) direction?: string; - // (undocumented) displayLabel?: string; - // (undocumented) extendedType?: string; - // (undocumented) isCustomHandled?: boolean; - // (undocumented) + // @deprecated (undocumented) isCustomHandledOrphan?: boolean; - // (undocumented) kindOfQuantity?: string; - // (undocumented) maximumLength?: number; - // (undocumented) maximumValue?: any; - // (undocumented) maxOccurs?: number; - // (undocumented) minimumLength?: number; - // (undocumented) minimumValue?: any; - // (undocumented) minOccurs?: number; - // (undocumented) primitiveType?: number; - // (undocumented) readOnly?: boolean; - // (undocumented) relationshipClass?: string; - // (undocumented) structName?: string; } diff --git a/common/api/summary/core-common.exports.csv b/common/api/summary/core-common.exports.csv index 7b638b3ed5a5..81996eb95311 100644 --- a/common/api/summary/core-common.exports.csv +++ b/common/api/summary/core-common.exports.csv @@ -156,7 +156,7 @@ internal;interface;CreateStandaloneIModelProps internal;const;CURRENT_INVOCATION internal;const;CURRENT_REQUEST internal;enum;CurrentImdlVersion -beta;interface;CustomAttribute +public;interface;CustomAttribute internal;interface;CustomViewState3dCreatorOptions internal;interface;CustomViewState3dProps public;class;CutStyle @@ -242,8 +242,8 @@ public;interface;EmphasizeElementsProps public;class;EmptyLocalization public;interface;EntityIdAndClassId public;type;EntityIdAndClassIdIterable -beta;class;EntityMetaData -beta;interface;EntityMetaDataProps +public;class;EntityMetaData +public;interface;EntityMetaDataProps public;interface;EntityProps public;interface;EntityQueryParams alpha;type;EntityReference @@ -591,16 +591,18 @@ public;enum;PolylineTypeFlags public;class;PositionalVectorTransform public;interface;PositionalVectorTransformProps internal;interface;PrimaryTileTreeId -beta;enum;PrimitiveTypeCode +public;enum;PrimitiveTypeCode public;interface;PriorityPlanarClipMaskArgs beta;enum;ProfileOptions public;class;Projection public;type;ProjectionMethod public;interface;ProjectionProps -beta;type;PropertyCallback -beta;class;PropertyMetaData +public;type;PropertyCallback +deprecated;type;PropertyCallback +public;class;PropertyMetaData +public;type;PropertyMetaDataCallback public;class;PropertyMetaDataMap -beta;interface;PropertyMetaDataProps +public;interface;PropertyMetaDataProps internal;interface;PullChangesOptions public;class;QParams2d alpha;interface;QParams2dProps diff --git a/common/changes/@itwin/core-backend/pmc-entity-metadata_2024-08-02-12-40.json b/common/changes/@itwin/core-backend/pmc-entity-metadata_2024-08-02-12-40.json new file mode 100644 index 000000000000..99b35bb89b62 --- /dev/null +++ b/common/changes/@itwin/core-backend/pmc-entity-metadata_2024-08-02-12-40.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-backend", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/core-backend" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-common/pmc-entity-metadata_2024-08-02-12-40.json b/common/changes/@itwin/core-common/pmc-entity-metadata_2024-08-02-12-40.json new file mode 100644 index 000000000000..8357542c645f --- /dev/null +++ b/common/changes/@itwin/core-common/pmc-entity-metadata_2024-08-02-12-40.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-common", + "comment": "Clean up EntityMetaData APIs and promote to public.", + "type": "none" + } + ], + "packageName": "@itwin/core-common" +} \ No newline at end of file diff --git a/core/backend/src/ClassRegistry.ts b/core/backend/src/ClassRegistry.ts index b85ef54df17b..1870b4044044 100644 --- a/core/backend/src/ClassRegistry.ts +++ b/core/backend/src/ClassRegistry.ts @@ -95,12 +95,10 @@ export class ClassRegistry { * @param entityMetaData The Entity metadata that defines the class */ private static generateClassForEntity(entityMetaData: EntityMetaData, iModel: IModelDb): typeof Entity { - const name = entityMetaData.ecclass.split(":"); - const domainName = name[0]; - const className = name[1]; + const [domainName, className] = entityMetaData.ecclass.split(":"); if (0 === entityMetaData.baseClasses.length) // metadata must contain a superclass - throw new IModelError(IModelStatus.BadArg, `class ${name} has no superclass`); + throw new IModelError(IModelStatus.BadArg, `class ${entityMetaData.ecclass} has no superclass`); // make sure schema exists let schema = Schemas.getRegisteredSchema(domainName); @@ -109,7 +107,7 @@ export class ClassRegistry { const superclass = this._classMap.get(entityMetaData.baseClasses[0].toLowerCase()); if (undefined === superclass) - throw new IModelError(IModelStatus.NotFound, `cannot find superclass for class ${name}`); + throw new IModelError(IModelStatus.NotFound, `cannot find superclass for class ${entityMetaData.ecclass}`); // user defined class hierarchies may skip a class in the hierarchy, and therefore their JS base class cannot // be used to tell if there are any generated classes in the hierarchy @@ -146,10 +144,10 @@ export class ClassRegistry { // - it is not in the `BisCore` schema // - there are no ancestors with manually registered JS implementations, (excluding BisCore base classes) if (!generatedClassHasNonGeneratedNonCoreAncestor) { - const navigationProps = Object.entries(entityMetaData.properties) - .filter(([_name, prop]) => prop.isNavigation) + const navigationProps = Array.from(entityMetaData.getProperties()) + .filter((prop) => prop.isNavigation) // eslint-disable-next-line @typescript-eslint/no-shadow - .map(([name, prop]) => { + .map((prop) => { assert(prop.relationshipClass); const maybeMetaData = iModel[_nativeDb].getSchemaItem(...prop.relationshipClass.split(":") as [string, string]); assert(maybeMetaData.result !== undefined, "The nav props relationship metadata was not found"); @@ -160,7 +158,7 @@ export class ClassRegistry { const normalizeClassName = (clsName: string) => clsName.replace(".", ":"); const rootClass = ClassRegistry.findRegisteredClass(normalizeClassName(rootClassMetaData)); assert(rootClass, `The root class for ${prop.relationshipClass} was not in BisCore.`); - return { name, concreteEntityType: EntityReferences.typeFromClass(rootClass) }; + return { name: prop.name, concreteEntityType: EntityReferences.typeFromClass(rootClass) }; }); Object.defineProperty( diff --git a/core/backend/src/Entity.ts b/core/backend/src/Entity.ts index b613b968b656..ddfe72205a06 100644 --- a/core/backend/src/Entity.ts +++ b/core/backend/src/Entity.ts @@ -7,7 +7,7 @@ */ import { Id64, Id64String } from "@itwin/core-bentley"; -import { EntityProps, EntityReferenceSet, PropertyCallback, PropertyMetaData } from "@itwin/core-common"; +import { EntityProps, EntityReferenceSet, PropertyCallback, PropertyMetaData, PropertyMetaDataCallback } from "@itwin/core-common"; import type { IModelDb } from "./IModelDb"; import { Schema } from "./Schema"; @@ -55,6 +55,7 @@ export class Entity { this.iModel = iModel; this.id = Id64.fromJSON(props.id); // copy all auto-handled properties from input to the object being constructed + // eslint-disable-next-line deprecation/deprecation this.forEachProperty((propName: string, meta: PropertyMetaData) => (this as any)[propName] = meta.createProperty((props as any)[propName]), false); } @@ -82,7 +83,7 @@ export class Entity { * @param includeCustom If true (default), include custom-handled properties in the iteration. Otherwise, skip custom-handled properties. * @note Custom-handled properties are core properties that have behavior enforced by C++ handlers. */ - public forEachProperty(func: PropertyCallback, includeCustom: boolean = true) { + public forEachProperty(func: PropertyCallback | PropertyMetaDataCallback, includeCustom: boolean = true) { // eslint-disable-line deprecation/deprecation this.iModel.forEachMetaData(this.classFullName, true, func, includeCustom); } diff --git a/core/backend/src/IModelDb.ts b/core/backend/src/IModelDb.ts index 28edd9167eb4..ae932ecf578d 100644 --- a/core/backend/src/IModelDb.ts +++ b/core/backend/src/IModelDb.ts @@ -22,7 +22,7 @@ import { GeoCoordinatesRequestProps, GeoCoordinatesResponseProps, GeometryContainmentRequestProps, GeometryContainmentResponseProps, IModel, IModelCoordinatesRequestProps, IModelCoordinatesResponseProps, IModelError, IModelNotFoundResponse, IModelTileTreeProps, LocalFileName, MassPropertiesRequestProps, MassPropertiesResponseProps, ModelExtentsProps, ModelLoadProps, ModelProps, ModelSelectorProps, OpenBriefcaseProps, - OpenCheckpointArgs, OpenSqliteArgs, ProfileOptions, PropertyCallback, QueryBinder, QueryOptions, QueryOptionsBuilder, QueryRowFormat, SchemaState, + OpenCheckpointArgs, OpenSqliteArgs, ProfileOptions, PropertyCallback, PropertyMetaDataCallback, QueryBinder, QueryOptions, QueryOptionsBuilder, QueryRowFormat, SchemaState, SheetProps, SnapRequestProps, SnapResponseProps, SnapshotOpenOptions, SpatialViewDefinitionProps, SubCategoryResultRow, TextureData, TextureLoadProps, ThumbnailProps, UpgradeOptions, ViewDefinition2dProps, ViewDefinitionProps, ViewIdString, ViewQueryParams, ViewStateLoadProps, ViewStateProps, ViewStoreRpc, @@ -1111,7 +1111,7 @@ export abstract class IModelDb extends IModel { * @param includeCustom If true (default), include custom-handled properties in the iteration. Otherwise, skip custom-handled properties. * @note Custom-handled properties are core properties that have behavior enforced by C++ handlers. */ - public static forEachMetaData(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom: boolean = true) { + public static forEachMetaData(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyMetaDataCallback | PropertyCallback, includeCustom: boolean = true) { // eslint-disable-line deprecation/deprecation iModel.forEachMetaData(classFullName, wantSuper, func, includeCustom); } @@ -1122,12 +1122,11 @@ export abstract class IModelDb extends IModel { * @param includeCustom If true (default), include custom-handled properties in the iteration. Otherwise, skip custom-handled properties. * @note Custom-handled properties are core properties that have behavior enforced by C++ handlers. */ - public forEachMetaData(classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom: boolean = true) { + public forEachMetaData(classFullName: string, wantSuper: boolean, func: PropertyMetaDataCallback | PropertyCallback, includeCustom: boolean = true) { // eslint-disable-line deprecation/deprecation const meta = this.getMetaData(classFullName); // will load if necessary - for (const propName in meta.properties) { // eslint-disable-line guard-for-in - const propMeta = meta.properties[propName]; - if (includeCustom || !propMeta.isCustomHandled || propMeta.isCustomHandledOrphan) - func(propName, propMeta); + for (const propMeta of meta.getMutableProperties()) { + if (includeCustom || !propMeta.isCustomHandled) + func(propMeta.name, propMeta); } if (wantSuper && meta.baseClasses && meta.baseClasses.length > 0) diff --git a/core/backend/src/test/imodel/IModel.test.ts b/core/backend/src/test/imodel/IModel.test.ts index 7304ae93487d..755546c2592c 100644 --- a/core/backend/src/test/imodel/IModel.test.ts +++ b/core/backend/src/test/imodel/IModel.test.ts @@ -1030,9 +1030,9 @@ describe("iModel", () => { } assert.isTrue(foundClassHasHandler); assert.isTrue(foundClassHasCurrentTimeStampProperty); - assert.isDefined(obj.properties.federationGuid); - assert.equal(obj.properties.federationGuid.primitiveType, 257); - assert.equal(obj.properties.federationGuid.extendedType, "BeGuid"); + assert.isDefined(obj.getProperty("federationGuid")); + assert.equal(obj.getProperty("federationGuid")!.primitiveType, 257); + assert.equal(obj.getProperty("federationGuid")!.extendedType, "BeGuid"); } it("should get metadata for class", () => { @@ -1118,9 +1118,9 @@ describe("iModel", () => { }); function checkClassHasHandlerMetaData(obj: EntityMetaData) { - assert.isDefined(obj.properties.restrictions); - assert.equal(obj.properties.restrictions.primitiveType, 2305); - assert.equal(obj.properties.restrictions.minOccurs, 0); + assert.isDefined(obj.getProperty("restrictions")); + assert.equal(obj.getProperty("restrictions")!.primitiveType, 2305); + assert.equal(obj.getProperty("restrictions")!.minOccurs, 0); } it("should get metadata for CA class just as well (and we'll see a array-typed property)", () => { @@ -1336,8 +1336,8 @@ describe("iModel", () => { it("should import schemas", async () => { const classMetaData = imodel1.getMetaData("TestBim:TestDocument"); // will throw on failure - assert.isDefined(classMetaData.properties.testDocumentProperty); - assert.isTrue(classMetaData.properties.testDocumentProperty.primitiveType === PrimitiveTypeCode.Integer); + assert.isDefined(classMetaData.getProperty("testDocumentProperty")); + assert.isTrue(classMetaData.getProperty("testDocumentProperty")!.primitiveType === PrimitiveTypeCode.Integer); }); it("should do CRUD on models", () => { diff --git a/core/backend/src/test/schema/ClassRegistry.test.ts b/core/backend/src/test/schema/ClassRegistry.test.ts index f9be8898d6d4..c0fd52e13eff 100644 --- a/core/backend/src/test/schema/ClassRegistry.test.ts +++ b/core/backend/src/test/schema/ClassRegistry.test.ts @@ -51,9 +51,8 @@ describe("Class Registry", () => { assert.equal(metaData.baseClasses[0], UrlLink.classFullName); assert.equal(metaData.customAttributes![0].ecclass, "BisCore:ClassHasHandler"); // Check the metadata on the one property that RepositoryLink defines, RepositoryGuid - assert.exists(metaData.properties); - assert.isDefined(metaData.properties.repositoryGuid); - const p = metaData.properties.repositoryGuid; + const p = metaData.getProperty("repositoryGuid")!; + assert.isDefined(p); assert.equal(p.extendedType, "BeGuid"); assert.equal(p.customAttributes![1].ecclass, "CoreCustomAttributes:HiddenProperty"); } @@ -69,9 +68,8 @@ describe("Class Registry", () => { assert.equal(metaData.ecclass, SpatialViewDefinition.classFullName); assert.isTrue(metaData.baseClasses.length > 0); assert.equal(metaData.baseClasses[0], ViewDefinition3d.classFullName); - assert.exists(metaData.properties); - assert.isDefined(metaData.properties.modelSelector); - const n = metaData.properties.modelSelector; + const n = metaData.getProperty("modelSelector")!; + assert.isDefined(n); assert.equal(n.relationshipClass, "BisCore:SpatialViewDefinitionUsesModelSelector"); } }); @@ -92,7 +90,7 @@ describe("Class Registry", () => { // Verify that the forEach method which is called when constructing an entity // is picking up all expected properties. const testData: string[] = []; - IModelDb.forEachMetaData(imodel, "TestDomain:TestDomainClass", true, (propName) => { + IModelDb.forEachMetaData(imodel, "TestDomain:TestDomainClass", true, (propName: string) => { testData.push(propName); }, false); diff --git a/core/common/src/EntityProps.ts b/core/common/src/EntityProps.ts index 10039f62a24e..579610e5e940 100644 --- a/core/common/src/EntityProps.ts +++ b/core/common/src/EntityProps.ts @@ -69,8 +69,9 @@ export interface EntityQueryParams { bindings?: any[] | object; } -/** The primitive types of an Entity property. - * @beta +/** The set of [fundamental types]($docs/bis/ec/primitive-types.md) for an [EC property]($docs/bis/ec/ec-property.md) + * that defines a simple (non-struct) value or an array of such values. + * @public */ export enum PrimitiveTypeCode { Uninitialized = 0x00, @@ -83,19 +84,27 @@ export enum PrimitiveTypeCode { Point2d = 0x701, // eslint-disable-line @typescript-eslint/no-shadow Point3d = 0x801, // eslint-disable-line @typescript-eslint/no-shadow String = 0x901, - IGeometry = 0xa01, // Used for Bentley.Geometry.Common.IGeometry types + IGeometry = 0xa01, } -/** A callback function to process properties of an Entity - * @beta +/** A callback function used when iterating over the properties of an [Entity]($backend) class using methods like + * [Entity.forEachProperty]($backend) and [IModelDb.forEachMetaData]($backend). + * @deprecated in 4.8. Use PropertyMetaDataCallback. + * @public */ export type PropertyCallback = (name: string, meta: PropertyMetaData) => void; -/** A custom attribute instance - * @beta +/** A callback function used when iterating over the properties of an [Entity]($backend) class using methods like + * [Entity.forEachProperty]($backend) and [IModelDb.forEachMetaData]($backend). + * @public + */ +export type PropertyMetaDataCallback = (name: string, meta: Readonly) => void; + +/** Represents a [custom attribute]($docs/bis/ec/ec-custom-attributes.md) attached to a [[PropertyMetaData]] or [[EntityMetaData]]. + * @public */ export interface CustomAttribute { - /** The class of the CustomAttribute */ + /** The fully-qualified name of the [custom attribute class]($docs/bis/ec/ec-custom-attribute-class.md).*/ ecclass: string; /** An object whose properties correspond by name to the properties of this custom attribute instance. */ properties: { [propName: string]: any }; @@ -103,49 +112,92 @@ export interface CustomAttribute { type FactoryFunc = (jsonObj: any) => any; -/** @beta */ +/** JSON representation of a [[PropertyMetaData]]. + * @public + */ export interface PropertyMetaDataProps { + /** See [[PropertyMetaData.primitiveType]]. */ primitiveType?: number; + /** See [[PropertyMetaData.structName]]. */ structName?: string; + /** See [[PropertyMetaData.extendedType]]. */ extendedType?: string; + /** See [[PropertyMetaData.description]]. */ description?: string; + /** See [[PropertyMetaData.displayLabel]]. */ displayLabel?: string; + /** See [[PropertyMetaData.minimumValue]]. */ minimumValue?: any; + /** See [[PropertyMetaData.maximumValue]]. */ maximumValue?: any; + /** See [[PropertyMetaData.minimumLength]]. */ minimumLength?: number; + /** See [[PropertyMetaData.maximumLength]]. */ maximumLength?: number; + /** See [[PropertyMetaData.readOnly]]. */ readOnly?: boolean; + /** See [[PropertyMetaData.kindOfQuantity]]. */ kindOfQuantity?: string; + /** See [[PropertyMetaData.isCustomHandled]]. */ isCustomHandled?: boolean; + /** @deprecated in 4.8. This property doesn't do anything useful. */ isCustomHandledOrphan?: boolean; + /** See [[PropertyMetaData.minOccurs]]. */ minOccurs?: number; + /** See [[PropertyMetaData.maxOccurs]]. */ maxOccurs?: number; + /** See [[PropertyMetaData.direction]]. */ direction?: string; + /** See [[PropertyMetaData.relationshipClass]]. */ relationshipClass?: string; + /** See [[PropertyMetaData.customAttributes]]. */ customAttributes?: CustomAttribute[]; } -/** Metadata for a property. - * @beta +/** Describes one [property]($docs/bis/ec/ec-property.md) of an [[EntityMetaData]]. + * @public */ export class PropertyMetaData implements PropertyMetaDataProps { + /** For a primitive property, or an array of primitive values, the underlying type. */ public primitiveType?: PrimitiveTypeCode; + /** For a complex property, or an array of complex values, the fully-qualified name of the class that defines the property's type. */ public structName?: string; + /** The optional name of a more specific type than [[primitiveType]] that provides additional semantics. + * For example, a property of type [[PrimitiveTypeCode.String]] may have an extended type of "Json" if it stores a stringified JSON value, or "URI" if + * it stores a universal resource identifier. + */ public extendedType?: string; + /** An optional extended description of the property. */ public description?: string; + /** An optional user-facing label. */ public displayLabel?: string; + /** For primitive properties, an optional constraint on the minimum value permitted to be assigned to it. */ public minimumValue?: any; + /** For primitive properties, an optional constraint on the maximum value permitted to be assigned to it. */ public maximumValue?: any; + /** For a string or binary property, an optional constraint on the minimum number of characters or bytes, respectively. */ public minimumLength?: number; + /** For a string or binary property, an optional constraint on the maximum number of characters or bytes, respectively. */ public maximumLength?: number; + /** If true, the value of the property cannot be changed. */ public readOnly?: boolean; + /** The optional name denoating the ["kind of quantity"]($docs/bis/ec/kindofquantity.md) this property represents. */ public kindOfQuantity?: string; + /** If true, the property has some custom logic that controls its value. Custom-handled properties are limited to a handful of fundamental + * properties in the BisCore ECSchema, like [Element.federationGuid]($backend) and [GeometricElement.category]($backend). + */ public isCustomHandled?: boolean; + /** @deprecated in 4.8. This property doesn't do anything useful. */ public isCustomHandledOrphan?: boolean; + /** For an array property, an optional constraint on the minimum number of entries in the array. */ public minOccurs?: number; + /** For an array property, an optional constraint on the maximum number of entries in the array. */ public maxOccurs?: number; + /** For a navigation property, the direction in which to traverse the [relationship]($docs/bis/ec/ec-relationships.md). */ public direction?: string; + /** For a navigation property, the fully-qualified name of the [EC relationship class]($docs/bis/ec/ec-relationship-class.md) defining the relationship. */ public relationshipClass?: string; + /** The set of [custom attributes]($docs/bis/ec/ec-custom-attributes.md) attached to the property. */ public customAttributes?: CustomAttribute[]; public constructor(jsonObj: PropertyMetaDataProps) { @@ -185,7 +237,9 @@ export class PropertyMetaData implements PropertyMetaDataProps { return val; } - /** construct a single property from an input object according to this metadata */ + /** Construct a single property from an input object according to this metadata + * @deprecated in 4.8. If you are using this for some reason, please [tell us why](https://github.com/orgs/iTwin/discussions). + */ public createProperty(jsonObj: any): any { if (jsonObj === undefined) return undefined; @@ -211,44 +265,66 @@ export class PropertyMetaData implements PropertyMetaDataProps { return jsonObj; } - /** Return `true` if this property is a NavigationProperty. */ + /** Return `true` if this property is a "navigation property" - i.e., it points to another entity via an [EC relationship]($docs/bis/ec/ec-relationships.md). */ public get isNavigation(): boolean { return (this.direction !== undefined); // the presence of `direction` means it is a navigation property } } -/** @beta */ +/** The JSON representation of an [[EntityMetaData]]. + * @public + */ export interface EntityMetaDataProps { + /** See [[EntityMetaData.classId]]. */ classId: Id64String; + /** See [[EntityMetaData.ecclass]]. */ ecclass: string; + /** See [[EntityMetaData.description]]. */ description?: string; + /** See [[EntityMetaData.modifier]]. */ modifier?: string; + /** See [[EntityMetaData.displayLabel]]. */ displayLabel?: string; - /** The base classes from which this class derives. If more than one, the first is the super class and the others are [mixins]($docs/bis/ec/ec-mixin-class). */ + /** See [[EntityMetaData.baseClasses]]. */ baseClasses: string[]; - /** The Custom Attributes for this class */ + /** See [[EntityMetaData.customAttributes]]. */ customAttributes?: CustomAttribute[]; - /** An object whose properties correspond by name to the properties of this class. */ + /** See [[EntityMetaData.properties]]. */ properties: { [propName: string]: PropertyMetaData }; } -/** Metadata for an Entity. - * @beta +/** Describes the [ECClass]($docs/bis/ec/ec-class.md) for an [Entity]($backend). + * @public */ -export class EntityMetaData implements EntityMetaDataProps { - /** The Id of the class in the [[IModelDb]] from which the metadata was obtained. */ +export class EntityMetaData { + private readonly _properties: { [propName: string]: PropertyMetaData & { name: string } }; + /** The Id of the class in the [IModelDb]($backend) from which the metadata was obtained. */ public readonly classId: Id64String; - /** The Entity name */ + /** The fully-qualified class name. */ public readonly ecclass: string; + /** An optional extended description of the class. */ public readonly description?: string; + /** An optional constraint applied to the class, one of the following: + * - "Abstract", indicating that the class cannot be instantiated, but may have instantiable subclasses; + * - "Sealed", indicating that the class cannot have subclasses; or + * - "None" (the default) + */ public readonly modifier?: string; + /** An optional user-facing label. */ public readonly displayLabel?: string; - /** The base class that this class is derives from. If more than one, the first is the actual base class and the others are mixins. */ + /** The list of classes from which this class derives. The first entry in the array is the direct Entity base class; any subsequent + * entries are [mix-ins]($docs/bis/ec/ec-mixin-class.md). + */ public readonly baseClasses: string[]; - /** The Custom Attributes for this class */ + /** The set of [custom attributes]($docs/bis/ec/ec-custom-attributes.md) attached to the class. */ public readonly customAttributes?: CustomAttribute[]; - /** An object whose properties correspond by name to the properties of this class. */ - public readonly properties: { [propName: string]: PropertyMetaData }; + /** An object whose properties correspond by name to the properties of this class. + * @note The return type of the indexer is incorrect - it will return `undefined` if no property named `propName` exists. + * @deprecated in 4.8. Use getProperty to look up a property, or getProperties to iterate all properties. + */ + public get properties(): { [propName: string]: PropertyMetaData } { + return this._properties; + } public constructor(jsonObj: EntityMetaDataProps) { this.classId = jsonObj.classId; @@ -258,10 +334,29 @@ export class EntityMetaData implements EntityMetaDataProps { this.displayLabel = jsonObj.displayLabel; this.baseClasses = jsonObj.baseClasses; this.customAttributes = jsonObj.customAttributes; - this.properties = {}; + this._properties = {}; for (const propName in jsonObj.properties) { // eslint-disable-line guard-for-in - this.properties[propName] = new PropertyMetaData(jsonObj.properties[propName]); + const prop = new PropertyMetaData(jsonObj.properties[propName]) as PropertyMetaData & { name: string }; + prop.name = propName; + this._properties[propName] = prop; } } + + /** Look up a property by its name, if it exists. */ + public getProperty(name: string): Readonly | undefined { + return this._properties[name]; + } + + /** Iterate over all of the properties of the entity class. */ + public getProperties(): Iterable> { + return this.getMutableProperties(); + } + + /** Retained to avoid breaking PropertyCallback which exposes mutable PropertyMetaData. + * @internal + */ + public getMutableProperties(): Iterable { + return Object.values(this._properties); + } } diff --git a/docs/bis/ec/ec-class.md b/docs/bis/ec/ec-class.md index 6f473b34e470..9bc80543200a 100644 --- a/docs/bis/ec/ec-class.md +++ b/docs/bis/ec/ec-class.md @@ -69,4 +69,4 @@ The traversal order will be: Foo, B1, Root, B2, and Root’s definition of prope ``` -The traversal order will be: Foo, B2, B1, Root and B2’s definition of property "A" will "win". If we introduce the "diamond pattern" via multiple inheritance by adding "Root" as a BaseClass of B2, the traversal order (using our second definition of Foo) would be: Foo, B2, Root, B1, Root. If a polymorphic algorithm were looking for subclasses of "Root", it would stop when it hit "Root" the first time. \ No newline at end of file +The traversal order will be: Foo, B2, B1, Root and B2’s definition of property "A" will "win". If we introduce the "diamond pattern" via multiple inheritance by adding "Root" as a BaseClass of B2, the traversal order (using our second definition of Foo) would be: Foo, B2, Root, B1, Root. If a polymorphic algorithm were looking for subclasses of "Root", it would stop when it hit "Root" the first time. diff --git a/full-stack-tests/backend/src/integration/SchemaSync.test.ts b/full-stack-tests/backend/src/integration/SchemaSync.test.ts index 9bc20e4310ac..2f20e4746196 100644 --- a/full-stack-tests/backend/src/integration/SchemaSync.test.ts +++ b/full-stack-tests/backend/src/integration/SchemaSync.test.ts @@ -71,7 +71,7 @@ const tinySchemaToXml = (s: TinySchema) => { }; const queryPropNames = (b: BriefcaseDb, className: string) => { try { - return Object.getOwnPropertyNames(b.getMetaData(className).properties); + return Array.from(b.getMetaData(className).getProperties()).map((prop) => prop.name); } catch { return []; } }; const assertChangesetTypeAndDescr = async (b: BriefcaseDb, changesetType: ChangesetType, description: string) => { @@ -185,14 +185,14 @@ describe("Schema synchronization", function (this: Suite) { b1.saveChanges(); // ensure b1 have class and its properties - assert.sameOrderedMembers(["p1", "p2"], Object.getOwnPropertyNames(b1.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b1, "TestSchema1:Pipe1"), ["p1", "p2"]); // pull schema change into b2 from shared schema channel await synchronizeSchemas(b2); b2.saveChanges(); // ensure b2 have class and its properties - assert.sameOrderedMembers(["p1", "p2"], Object.getOwnPropertyNames(b2.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b2, "TestSchema1:Pipe1"), ["p1", "p2"]); // add new properties in b2 const schema2 = ` @@ -210,14 +210,14 @@ describe("Schema synchronization", function (this: Suite) { b2.saveChanges(); // ensure b2 have class and its properties - assert.sameOrderedMembers(["p1", "p2", "p3", "p4"], Object.getOwnPropertyNames(b2.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b2, "TestSchema1:Pipe1"), ["p1", "p2", "p3", "p4"]); // pull schema change into b1 from shared schema channel await synchronizeSchemas(b1); b1.saveChanges(); // ensure b1 have class and its properties - assert.sameOrderedMembers(["p1", "p2", "p3", "p4"], Object.getOwnPropertyNames(b1.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b1, "TestSchema1:Pipe1"), ["p1", "p2", "p3", "p4"]); // push changes await b1.pushChanges({ accessToken: user1AccessToken, description: "push schema changes" }); @@ -227,7 +227,7 @@ describe("Schema synchronization", function (this: Suite) { await b3.pullChanges({ accessToken: user3AccessToken }); // ensure b3 have class and its properties - assert.sameOrderedMembers(["p1", "p2", "p3", "p4"], Object.getOwnPropertyNames(b3.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b3, "TestSchema1:Pipe1"), ["p1", "p2", "p3", "p4"]); b1.close(); b2.close(); @@ -283,28 +283,28 @@ describe("Schema synchronization", function (this: Suite) { b1.saveChanges(); // ensure b1 have class and its properties - assert.sameOrderedMembers(["p1", "p2"], Object.getOwnPropertyNames(b1.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b1, "TestSchema1:Pipe1"), ["p1", "p2"]); // pull schema change into b2 from shared schema channel await synchronizeSchemas(b2); b2.saveChanges(); // ensure b2 have class and its properties - assert.sameOrderedMembers(["p1", "p2"], Object.getOwnPropertyNames(b2.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b2, "TestSchema1:Pipe1"), ["p1", "p2"]); // import same schema from another briefcase await b2.importSchemaStrings([schema1]); b2.saveChanges(); // ensure b2 have class and its properties - assert.sameOrderedMembers(["p1", "p2"], Object.getOwnPropertyNames(b2.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b2, "TestSchema1:Pipe1"), ["p1", "p2"]); // pull schema change into b1 from shared schema channel await synchronizeSchemas(b1); b1.saveChanges(); // ensure b1 have class and its properties - assert.sameOrderedMembers(["p1", "p2"], Object.getOwnPropertyNames(b1.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b1, "TestSchema1:Pipe1"), ["p1", "p2"]); // push changes await b1.pushChanges({ accessToken: user1AccessToken, description: "push schema changes" }); @@ -314,7 +314,7 @@ describe("Schema synchronization", function (this: Suite) { await b3.pullChanges({ accessToken: user3AccessToken }); // ensure b3 have class and its properties - assert.sameOrderedMembers(["p1", "p2"], Object.getOwnPropertyNames(b3.getMetaData("TestSchema1:Pipe1").properties)); + assert.sameOrderedMembers(queryPropNames(b3, "TestSchema1:Pipe1"), ["p1", "p2"]); b1.close(); b2.close();