UNPKG

@itwin/core-backend

Version:
332 lines • 17.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Schema */ import { DbResult, Id64, IModelStatus, Logger } from "@itwin/core-bentley"; import { IModelError } from "@itwin/core-common"; import { Entity } from "./Entity"; import { Schema, Schemas } from "./Schema"; import { EntityReferences } from "./EntityReferences"; import * as assert from "assert"; import { _nativeDb } from "./internal/Symbols"; const isGeneratedClassTag = Symbol("isGeneratedClassTag"); /** Maintains the mapping between the name of a BIS [ECClass]($ecschema-metadata) (in "schema:class" format) and the JavaScript [[Entity]] class that implements it. * @public */ export class EntityJsClassMap { _classMap = new Map(); /** @internal */ has(classFullName) { return this._classMap.has(classFullName.toLowerCase()); } /** @internal */ get(classFullName) { return this._classMap.get(classFullName.toLowerCase()); } /** @internal */ set(classFullName, entityClass) { this._classMap.set(classFullName.toLowerCase(), entityClass); } /** @internal */ delete(classFullName) { return this._classMap.delete(classFullName.toLowerCase()); } /** @internal */ clear() { this._classMap.clear(); } /** @internal */ [Symbol.iterator]() { return this._classMap[Symbol.iterator](); } /** * Registers a single `entityClass` defined in the specified `schema`. * This method registers the class globally. To register a class for a specific iModel, use [[IModelDb.jsClassMap]]. * * @param entityClass - The JavaScript class that implements the BIS [ECClass](@itwin/core-common) to be registered. * @param schema - The schema that contains the `entityClass`. * * @throws Error if the class is already registered. * * @public */ register(entityClass, schema) { const key = (`${schema.schemaName}:${entityClass.className}`).toLowerCase(); if (this.has(key)) { const errMsg = `Class ${key} is already registered. Make sure static className member is correct on JavaScript class ${entityClass.name}`; Logger.logError("core-frontend.classRegistry", errMsg); throw new Error(errMsg); } entityClass.schema = schema; this.set(key, entityClass); } } /** Maintains the mapping between the name of a BIS [ECClass]($ecschema-metadata) (in "schema:class" format) and the JavaScript [[Entity]] class that implements it. * Applications or modules that supply their own Entity subclasses should use [[registerModule]] or [[register]] at startup * to establish their mappings. * * When creating custom Entity subclasses for registration, you should: * - Override the `className` property to match your ECClass name: * ```typescript * public static override get className() { return "TestElement"; } * ``` * - Do NOT override `schemaName` or `schema` - these will be wired up automatically during registration * * @public */ export class ClassRegistry { static _globalClassMap = new EntityJsClassMap(); /** @internal */ static isNotFoundError(err) { return (err instanceof IModelError) && (err.errorNumber === IModelStatus.NotFound); } /** @internal */ static makeMetaDataNotFoundError(className) { return new IModelError(IModelStatus.NotFound, `metadata not found for ${className}`); } /** Register a single `entityClass` defined in the specified `schema`. * @see [[registerModule]] to register multiple classes. * @public */ static register(entityClass, schema) { this._globalClassMap.register(entityClass, schema); } /** Generate a proxy Schema for a domain that has not been registered. */ static generateProxySchema(domain, iModel) { const hasBehavior = iModel.withPreparedSqliteStatement(` SELECT NULL FROM [ec_CustomAttribute] [c] JOIN [ec_schema] [s] ON [s].[Id] = [c].[ContainerId] JOIN [ec_class] [e] ON [e].[Id] = [c].[ClassId] JOIN [ec_schema] [b] ON [e].[SchemaId] = [b].[Id] WHERE [c].[ContainerType] = 1 AND [s].[Name] = ? AND [b].[Name] || '.' || [e].[name] = ?`, (stmt) => { stmt.bindString(1, domain); stmt.bindString(2, "BisCore.SchemaHasBehavior"); return stmt.step() === DbResult.BE_SQLITE_ROW; }); const schemaClass = class extends Schema { static get schemaName() { return domain; } static get missingRequiredBehavior() { return hasBehavior; } }; iModel.schemaMap.registerSchema(schemaClass); // register the class before we return it. return schemaClass; } /** First, finds the root BisCore entity class for an entity, by traversing base classes and mixin targets (AppliesTo). * Then, gets its metadata and returns that. * @param iModel - iModel containing the metadata for this type * @param ecTypeQualifier - a full name of an ECEntityClass to find the root of * @returns the qualified full name of an ECEntityClass * @internal public for testing only */ static getRootEntity(iModel, ecTypeQualifier) { const [classSchema, className] = ecTypeQualifier.split("."); const schemaItemJson = iModel[_nativeDb].getSchemaItem(classSchema, className); if (schemaItemJson.error) throw new IModelError(schemaItemJson.error.status, `failed to get schema item '${ecTypeQualifier}'`); assert(undefined !== schemaItemJson.result); const schemaItem = JSON.parse(schemaItemJson.result); if (!("appliesTo" in schemaItem) && schemaItem.baseClass === undefined) { return ecTypeQualifier; } // typescript doesn't understand that the inverse of the above condition is // ("appliesTo" in rootclassMetaData || rootClassMetaData.baseClass !== undefined) const parentItemQualifier = schemaItem.appliesTo ?? schemaItem.baseClass; return this.getRootEntity(iModel, parentItemQualifier); } /** Generate a JavaScript class from Entity metadata. * @param entityMetaData The Entity metadata that defines the class */ // eslint-disable-next-line @typescript-eslint/no-deprecated static generateClassForEntity(entityMetaData, iModel) { const name = entityMetaData.ecclass.split(":"); const domainName = name[0]; const className = name[1]; if (0 === entityMetaData.baseClasses.length) // metadata must contain a superclass throw new IModelError(IModelStatus.BadArg, `class ${name} has no superclass`); // make sure schema exists let schema = iModel.schemaMap.get(domainName) ?? Schemas.getRegisteredSchema(domainName); if (undefined === schema) schema = this.generateProxySchema(domainName, iModel); // no schema found, create it too const superClassFullName = entityMetaData.baseClasses[0].toLowerCase(); const superclass = iModel.jsClassMap.get(superClassFullName) ?? this._globalClassMap.get(superClassFullName); if (undefined === superclass) throw new IModelError(IModelStatus.NotFound, `cannot find superclass for class ${name}`); // 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 let generatedClassHasNonGeneratedNonCoreAncestor = false; let currentSuperclass = superclass; const MAX_ITERS = 1000; for (let i = 0; i < MAX_ITERS; ++i) { if (currentSuperclass.schema.schemaName === "BisCore") break; if (!currentSuperclass.isGeneratedClass) { generatedClassHasNonGeneratedNonCoreAncestor = true; break; } // eslint-disable-next-line @typescript-eslint/no-deprecated const superclassMetaData = iModel.classMetaDataRegistry.find(currentSuperclass.classFullName); if (superclassMetaData === undefined) throw new IModelError(IModelStatus.BadSchema, `could not find the metadata for class '${currentSuperclass.name}', class metadata should be loaded by now`); const maybeNextSuperclass = this.getClass(superclassMetaData.baseClasses[0], iModel); if (maybeNextSuperclass === undefined) throw new IModelError(IModelStatus.BadSchema, `could not find the base class of '${currentSuperclass.name}', all generated classes must have a base class`); currentSuperclass = maybeNextSuperclass; } const generatedClass = class extends superclass { static get className() { return className; } static [isGeneratedClassTag] = true; static get isGeneratedClass() { return this.hasOwnProperty(isGeneratedClassTag); } }; // the above creates an anonymous class. For help debugging, set the "constructor.name" property to be the same as the bisClassName. Object.defineProperty(generatedClass, "name", { get: () => className }); // this is the (only) way to change that readonly property. // a class only gets an automatic `collectReferenceIds` implementation if: // - 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) // eslint-disable-next-line @typescript-eslint/no-shadow .map(([name, prop]) => { assert(prop.relationshipClass); const maybeMetaData = iModel[_nativeDb].getSchemaItem(...prop.relationshipClass.split(":")); assert(maybeMetaData.result !== undefined, "The nav props relationship metadata was not found"); const relMetaData = JSON.parse(maybeMetaData.result); const rootClassMetaData = ClassRegistry.getRootEntity(iModel, relMetaData.target.constraintClasses[0]); // root class must be in BisCore so should be loaded since biscore classes will never get this // generated implementation const normalizeClassName = (clsName) => 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) }; }); Object.defineProperty(generatedClass.prototype, "collectReferenceIds", { value(referenceIds) { // eslint-disable-next-line @typescript-eslint/dot-notation const superImpl = superclass.prototype["collectReferenceIds"]; superImpl.call(this, referenceIds); for (const navProp of navigationProps) { const relatedElem = this[navProp.name]; // cast to any since subclass can have any extensions if (!relatedElem || !Id64.isValid(relatedElem.id)) continue; const referenceId = EntityReferences.fromEntityType(relatedElem.id, navProp.concreteEntityType); referenceIds.add(referenceId); } }, // defaults for methods on a prototype (required for sinon to stub out methods on tests) writable: true, configurable: true, }); } // if the schema is a proxy for a domain with behavior, throw exceptions for all protected operations if (schema.missingRequiredBehavior) { const throwError = () => { throw new IModelError(IModelStatus.WrongHandler, `Schema [${domainName}] not registered, but is marked with SchemaHasBehavior`); }; superclass.protectedOperations.forEach((operation) => generatedClass[operation] = throwError); } iModel.jsClassMap.register(generatedClass, schema); // register it before returning return generatedClass; } /** Register all of the classes found in the given module that derive from [[Entity]]. * [[register]] will be invoked for each subclass of `Entity` exported by `moduleObj`. * @param moduleObj The module to search for subclasses of Entity * @param schema The schema that contains all of the [ECClass]($ecschema-metadata)es exported by `moduleObj`. */ static registerModule(moduleObj, schema) { for (const thisMember in moduleObj) { // eslint-disable-line guard-for-in const thisClass = moduleObj[thisMember]; if (thisClass.prototype instanceof Entity) this.register(thisClass, schema); } } /** * This function fetches the specified Entity from the imodel, generates a JavaScript class for it, and registers the generated * class. This function also ensures that all of the base classes of the Entity exist and are registered. */ static generateClass(classFullName, iModel) { // eslint-disable-next-line @typescript-eslint/no-deprecated const metadata = iModel.classMetaDataRegistry.find(classFullName); if (metadata === undefined || metadata.ecclass === undefined) throw this.makeMetaDataNotFoundError(classFullName); // Make sure we have all base classes registered. if (metadata.baseClasses && (0 !== metadata.baseClasses.length)) this.getClass(metadata.baseClasses[0], iModel); // Now we can generate the class from the classDef. return this.generateClassForEntity(metadata, iModel); } /** Find a registered class by classFullName. * @param classFullName class to find * @param iModel The IModel that contains the class definitions * @returns The Entity class or undefined */ static findRegisteredClass(classFullName) { return this._globalClassMap.get(classFullName.toLowerCase()); } /** Get the Entity class for the specified Entity className. * @param classFullName The full BIS class name of the Entity * @param iModel The IModel that contains the class definitions * @returns The Entity class */ static getClass(classFullName, iModel) { const key = classFullName.toLowerCase(); return iModel.jsClassMap.get(key) ?? this._globalClassMap.get(key) ?? this.generateClass(key, iModel); } /** Unregister a class, by name, if one is already registered. * This function is not normally needed, but is useful for cases where a generated *proxy* class needs to be replaced by the *real* class. * @param classFullName Name of the class to unregister * @return true if the class was unregistered * @internal */ static unregisterClass(classFullName) { return this._globalClassMap.delete(classFullName.toLowerCase()); } /** Unregister all classes from a schema. * This function is not normally needed, but is useful for cases where a generated *proxy* schema needs to be replaced by the *real* schema. * @param schema Name of the schema to unregister * @internal */ static unregisterClassesFrom(schema) { for (const entry of Array.from(this._globalClassMap)) { if (entry[1].schema === schema) this.unregisterClass(entry[0]); } } } /** * A cache that records the mapping between class names and class metadata. * @see [[IModelDb.classMetaDataRegistry]] to access the registry for a specific iModel. * @internal * @deprecated in 5.0 - will not be removed until after 2026-06-13. Please use `schemaContext` from the `iModel` instead. * * @example * @ * Current Usage: * ```ts * const metaData: EntityMetaData | undefined = iModel.classMetaDataRegistry.find("SchemaName:ClassName"); * ``` * * Replacement: * ```ts * const entityMetaData: EntityClass | undefined = iModel.schemaContext.getSchemaItemSync("SchemaName.ClassName", EntityClass); * const relationshipMetaData: RelationshipClass | undefined = iModel.schemaContext.getSchemaItemSync("SchemaName", "ClassName", RelationshipClass); * ``` */ export class MetaDataRegistry { // eslint-disable-next-line @typescript-eslint/no-deprecated _registry = new Map(); _classIdToName = new Map(); /** Get the specified Entity metadata */ // eslint-disable-next-line @typescript-eslint/no-deprecated find(classFullName) { return this._registry.get(classFullName.toLowerCase()); } // eslint-disable-next-line @typescript-eslint/no-deprecated findByClassId(classId) { const name = this._classIdToName.get(classId); return undefined !== name ? this.find(name) : undefined; } /** Add metadata to the cache */ // eslint-disable-next-line @typescript-eslint/no-deprecated add(classFullName, metaData) { const name = classFullName.toLowerCase(); this._registry.set(name, metaData); this._classIdToName.set(metaData.classId, name); } } //# sourceMappingURL=ClassRegistry.js.map