-
Notifications
You must be signed in to change notification settings - Fork 208
/
ClassRegistry.ts
296 lines (262 loc) · 14.3 KB
/
ClassRegistry.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
/*---------------------------------------------------------------------------------------------
* 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, Id64String, IModelStatus, Logger } from "@itwin/core-bentley";
import { EntityMetaData, EntityReferenceSet, IModelError, RelatedElement } from "@itwin/core-common";
import { Entity } from "./Entity";
import { IModelDb } from "./IModelDb";
import { Schema, Schemas } from "./Schema";
import { EntityReferences } from "./EntityReferences";
import * as assert from "assert";
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.
* Applications or modules that supply their own Entity subclasses should use [[registerModule]] or [[register]] at startup
* to establish their mappings.
* @public
*/
export class ClassRegistry {
private static readonly _classMap = new Map<string, typeof Entity>();
/** @internal */
public static isNotFoundError(err: any) { return (err instanceof IModelError) && (err.errorNumber === IModelStatus.NotFound); }
/** @internal */
public static makeMetaDataNotFoundError(className: string): IModelError { 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
*/
public static register(entityClass: typeof Entity, schema: typeof Schema) {
entityClass.schema = schema;
const key = (`${schema.schemaName}:${entityClass.className}`).toLowerCase();
if (this._classMap.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);
}
this._classMap.set(key, entityClass);
}
/** Generate a proxy Schema for a domain that has not been registered. */
private static generateProxySchema(domain: string, iModel: IModelDb): typeof Schema {
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 {
public static override get schemaName() { return domain; }
public static override get missingRequiredBehavior() { return hasBehavior; }
};
Schemas.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
*/
public static getRootEntity(iModel: IModelDb, ecTypeQualifier: string): string {
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 as string;
return this.getRootEntity(iModel, parentItemQualifier);
}
/** Generate a JavaScript class from Entity metadata.
* @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];
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 = Schemas.getRegisteredSchema(domainName);
if (undefined === schema)
schema = this.generateProxySchema(domainName, iModel); // no schema found, create it too
const superclass = this._classMap.get(entityMetaData.baseClasses[0].toLowerCase());
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;
}
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 {
public static override get className() { return className; }
private static [isGeneratedClassTag] = true;
public static override 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(":") as [string, string]);
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: 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) };
});
Object.defineProperty(
generatedClass.prototype,
"collectReferenceIds",
{
value(this: typeof generatedClass, referenceIds: EntityReferenceSet) {
// eslint-disable-next-line @typescript-eslint/dot-notation
const superImpl = superclass.prototype["collectReferenceIds"];
superImpl.call(this, referenceIds);
for (const navProp of navigationProps) {
const relatedElem: RelatedElement | undefined = (this as any)[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 as any)[operation] = throwError);
}
this.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`.
*/
public static registerModule(moduleObj: any, schema: typeof 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.
*/
private static generateClass(classFullName: string, iModel: IModelDb): typeof Entity {
const metadata: EntityMetaData | undefined = 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
*/
public static findRegisteredClass(classFullName: string): typeof Entity | undefined {
return this._classMap.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
*/
public static getClass(classFullName: string, iModel: IModelDb): typeof Entity {
const key = classFullName.toLowerCase();
const ctor = this._classMap.get(key);
return ctor ? ctor : 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
*/
public static unregisterCLass(classFullName: string) { return this._classMap.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
*/
public static unregisterClassesFrom(schema: typeof Schema) {
for (const entry of Array.from(this._classMap)) {
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
*/
export class MetaDataRegistry {
private _registry = new Map<string, EntityMetaData>();
private _classIdToName = new Map<Id64String, string>();
/** Get the specified Entity metadata */
public find(classFullName: string): EntityMetaData | undefined {
return this._registry.get(classFullName.toLowerCase());
}
public findByClassId(classId: Id64String): EntityMetaData | undefined {
const name = this._classIdToName.get(classId);
return undefined !== name ? this.find(name) : undefined;
}
/** Add metadata to the cache */
public add(classFullName: string, metaData: EntityMetaData): void {
const name = classFullName.toLowerCase();
this._registry.set(name, metaData);
this._classIdToName.set(metaData.classId, name);
}
}