/
typegoose.ts
497 lines (439 loc) · 17.5 KB
/
typegoose.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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
/* imports */
import * as mongoose from 'mongoose';
import 'reflect-metadata';
import * as semver from 'semver';
import {
assertion,
assertionIsClass,
getMergedModelOptions,
getName,
isCachingEnabled,
isGlobalCachingEnabled,
isNullOrUndefined,
mapModelOptionsToNaming,
warnNotMatchingExisting,
} from './internal/utils';
// using "typeof process", because somehow js gives a ReferenceError when using "process === undefined" in browser
/* istanbul ignore next */
if (typeof process !== 'undefined' && !isNullOrUndefined(process?.version) && !isNullOrUndefined(mongoose?.version)) {
// for usage on client side
/* istanbul ignore next */
if (semver.lt(mongoose?.version, '7.1.0')) {
throw new Error(`Please use mongoose 7.1.0 or higher (Current mongoose: ${mongoose.version}) [E001]`);
}
/* istanbul ignore next */
if (semver.lt(process.version.slice(1), '14.17.0')) {
throw new Error('You are using a NodeJS Version below 14.17.0, Please Upgrade! [E002]');
}
}
import { parseENV, setGlobalOptions } from './globalOptions';
import { DecoratorKeys } from './internal/constants';
import { constructors, models } from './internal/data';
import { _buildSchema } from './internal/schema';
import { logger } from './logSettings';
import { isModel } from './typeguards';
import type {
AnyParamConstructor,
BeAnObject,
DocumentType,
IModelOptions,
Ref,
ReturnModelType,
SubDocumentType,
ArraySubDocumentType,
IBuildSchemaOptions,
} from './types';
import { CacheDisabledError, ExpectedTypeError, FunctionCalledMoreThanSupportedError, NotValidModelError } from './internal/errors';
/* exports */
// export the internally used "mongoose", to not need to always import it
export { mongoose, setGlobalOptions };
export { setLogLevel, LogLevels } from './logSettings';
export * from './prop';
export * from './hooks';
export * from './plugin';
export * from './indexes';
export * from './modelOptions';
export * from './queryMethod';
export * from './typeguards';
export * as defaultClasses from './defaultClasses';
export * as errors from './internal/errors';
export * as types from './types';
// the following types are re-exported (instead of just in "types") because they are often used types
export { DocumentType, Ref, ReturnModelType, SubDocumentType, ArraySubDocumentType };
export { getClass, getName } from './internal/utils';
export { Severity, PropType } from './internal/constants';
parseENV(); // call this before anything to ensure they are applied
/**
* Build a Model From a Class
* @param cl The Class to build a Model from
* @param options Overwrite Options, like for naming or general SchemaOptions the class gets compiled with
* @returns The finished Model
* @public
* @example
* ```ts
* class ClassName {}
*
* const NameModel = getModelForClass(ClassName);
* ```
*/
export function getModelForClass<U extends AnyParamConstructor<any>, QueryHelpers = BeAnObject>(cl: U, options?: IModelOptions) {
assertionIsClass(cl);
const rawOptions = typeof options === 'object' ? options : {};
const overwriteNaming = mapModelOptionsToNaming(rawOptions); // use "rawOptions" instead of "mergedOptions" to consistently differentiate between classes & models
const mergedOptions = getMergedModelOptions(rawOptions, cl);
const name = getName(cl, overwriteNaming);
if (isCachingEnabled(mergedOptions.options?.disableCaching) && models.has(name)) {
return models.get(name) as ReturnModelType<U, QueryHelpers>;
}
const modelFn =
mergedOptions?.existingConnection?.model.bind(mergedOptions.existingConnection) ??
mergedOptions?.existingMongoose?.model.bind(mergedOptions.existingMongoose) ??
mongoose.model.bind(mongoose);
const compiledModel: mongoose.Model<any> = modelFn(name, buildSchema(cl, mergedOptions));
return addModelToTypegoose<U, QueryHelpers>(compiledModel, cl, {
existingMongoose: mergedOptions?.existingMongoose,
existingConnection: mergedOptions?.existingConnection,
disableCaching: mergedOptions.options?.disableCaching,
});
}
/**
* Get Model from internal cache
* @param key Model's name key
* @example
* ```ts
* class ClassName {}
* getModelForClass(ClassName); // build the model
* const NameModel = getModelWithString<typeof ClassName>("ClassName");
* ```
*/
export function getModelWithString<U extends AnyParamConstructor<any>, QueryHelpers = BeAnObject>(
key: string
): undefined | ReturnModelType<U, QueryHelpers> {
assertion(typeof key === 'string', () => new ExpectedTypeError('key', 'string', key));
assertion(isGlobalCachingEnabled(), () => new CacheDisabledError('getModelWithString'));
return models.get(key) as any;
}
/**
* Generates a Mongoose schema out of class props, iterating through all parents
* @param cl The Class to build a Schema from
* @param options Overwrite Options, like for naming or general SchemaOptions the class gets compiled with
* @returns Returns the Build Schema
* @example
* ```ts
* class ClassName {}
* const NameSchema = buildSchema(ClassName);
* const NameModel = mongoose.model("Name", NameSchema);
* ```
*/
export function buildSchema<U extends AnyParamConstructor<any>>(
cl: U,
options?: IModelOptions
): mongoose.Schema<DocumentType<InstanceType<U>>> {
assertionIsClass(cl);
const overwriteNaming = mapModelOptionsToNaming(options);
logger.debug('buildSchema called for "%s"', getName(cl, overwriteNaming));
// dont re-run the merging if already done so before (like in getModelForClass)
const mergedOptions = getMergedModelOptions(options, cl);
let sch: mongoose.Schema<DocumentType<InstanceType<U>>> | undefined = undefined;
/** Parent Constructor */
let parentCtor = Object.getPrototypeOf(cl.prototype).constructor;
/* This array is to execute from lowest class to highest (when extending) */
const parentClasses: [AnyParamConstructor<any>, IBuildSchemaOptions][] = [];
let upperOptions: IBuildSchemaOptions = {};
// iterate trough all parents to the lowest class
while (parentCtor?.name !== 'Object') {
// add lower classes (when extending) to the front of the array to be processed first
parentClasses.unshift([parentCtor, upperOptions]);
// clone object, because otherwise it will affect the upper classes too because the same reference is used
upperOptions = { ...upperOptions };
const ropt: IModelOptions = Reflect.getMetadata(DecoratorKeys.ModelOptions, parentCtor) ?? {};
// only affect options of lower classes, not the class the options are from
if (ropt.options?.disableLowerIndexes) {
upperOptions.buildIndexes = false;
}
// set next parent
parentCtor = Object.getPrototypeOf(parentCtor.prototype).constructor;
}
// iterate and build class schemas from lowest to highest (when extending classes, the lower class will get build first) see https://github.com/typegoose/typegoose/pull/243
for (const [parentClass, extraOptions] of parentClasses) {
// extend schema
sch = _buildSchema(parentClass, sch!, mergedOptions, false, undefined, extraOptions);
}
// get schema of current model
sch = _buildSchema(cl, sch, mergedOptions, true, overwriteNaming);
return sch;
}
/**
* Add a Class-Model Pair to the Typegoose Cache
* This can be used to add custom Models to Typegoose, with the type information of "cl"
* Note: no guarrantee that the type information is fully correct when used manually
* @param model The Model to store
* @param cl The Class to store
* @param options Overwrite existingMongoose or existingConnection
* @example
* ```ts
* class ClassName {}
*
* const schema = buildSchema(ClassName);
* // modifications to the schema can be done
* const model = addModelToTypegoose(mongoose.model("Name", schema), ClassName);
* ```
*/
export function addModelToTypegoose<U extends AnyParamConstructor<any>, QueryHelpers = BeAnObject>(
model: mongoose.Model<any>,
cl: U,
options?: { existingMongoose?: mongoose.Mongoose; existingConnection?: any; disableCaching?: boolean }
) {
const mongooseModel = options?.existingMongoose?.Model || options?.existingConnection?.base?.Model || mongoose.Model;
assertion(model.prototype instanceof mongooseModel, new NotValidModelError(model, 'addModelToTypegoose.model'));
assertionIsClass(cl);
// only check cache after the above checks, just to make sure they run
if (!isCachingEnabled(options?.disableCaching)) {
logger.info('Caching is not enabled, skipping adding');
return model as ReturnModelType<U, QueryHelpers>;
}
const name = model.modelName;
assertion(
!models.has(name),
new FunctionCalledMoreThanSupportedError(
'addModelToTypegoose',
1,
`This was caused because the model name "${name}" already exists in the typegoose-internal "models" cache`
)
);
if (constructors.get(name)) {
logger.info('Class "%s" already existed in the constructors Map', name);
}
models.set(name, model);
constructors.set(name, cl);
return models.get(name) as ReturnModelType<U, QueryHelpers>;
}
/**
* Deletes a existing model so that it can be overwritten with another model
* (deletes from mongoose.connection and typegoose models cache and typegoose constructors cache)
* @param name The Model's mongoose name
* @example
* ```ts
* class ClassName {}
* const NameModel = getModelForClass(ClassName);
* deleteModel("ClassName");
* ```
*/
export function deleteModel(name: string) {
assertion(typeof name === 'string', () => new ExpectedTypeError('name', 'string', name));
assertion(isGlobalCachingEnabled(), () => new CacheDisabledError('deleteModelWithClass'));
logger.debug('Deleting Model "%s"', name);
const model = models.get(name);
if (!isNullOrUndefined(model)) {
model.db.deleteModel(name);
}
models.delete(name);
constructors.delete(name);
}
/**
* Delete a model, with the given class
* Same as "deleteModel", only that it can be done with the class instead of the name
* @param cl The Class to delete the model from
* @example
* ```ts
* class ClassName {}
* const NameModel = getModelForClass(ClassName);
* deleteModelWithClass(ClassName);
* ```
*/
export function deleteModelWithClass<U extends AnyParamConstructor<any>>(cl: U) {
assertionIsClass(cl);
assertion(isGlobalCachingEnabled(), () => new CacheDisabledError('deleteModelWithClass'));
let name = getName(cl);
if (!models.has(name)) {
logger.debug(`Class "${name}" is not in "models", trying to find in "constructors"`);
let found = false;
// type "Map" does not have a "find" function, and using "get" would maybe result in the incorrect values
for (const [cname, constructor] of constructors) {
if (constructor === cl) {
logger.debug(`Found Class in "constructors" with class name "${name}" and entered name "${cname}""`);
name = cname;
found = true;
}
}
if (!found) {
logger.debug(`Could not find class "${name}" in constructors`);
return;
}
}
return deleteModel(name);
}
/**
* Build a Model from the given Class and add it as a discriminator onto "from"
* @param from The Model to add the new discriminator model to
* @param cl The Class to make a discriminator model from
* @param options Overwrite ModelOptions (Merged with ModelOptions from class)
* @example
* ```ts
* class Main {
* @prop({ ref: () => BaseDiscriminator })
* public discriminators?: Ref<BaseDiscriminator>;
* }
*
* class BaseDiscriminator {
* @prop()
* public propertyOnAllDiscriminators?: string;
* }
*
* class AnotherDiscriminator {
* @prop()
* public someValue?: string;
* }
*
* const MainModel = getModelForClass(Main);
*
* const BaseDiscriminatorModel = getModelFroClass(BaseDiscriminator);
* const AnotherDiscriminatorModel = getDiscriminatorModelForClass(BaseDiscriminatorModel, AnotherDiscriminator);
* // add other discriminator models the same way as "AnotherDiscriminatorModel"
* ```
*/
export function getDiscriminatorModelForClass<U extends AnyParamConstructor<any>, QueryHelpers = BeAnObject>(
from: mongoose.Model<any, any, any, any>,
cl: U,
options?: IModelOptions
): ReturnModelType<U, QueryHelpers>;
/**
* Build a Model from the given Class and add it as a discriminator onto "from"
* @param from The Model to add the new discriminator model to
* @param cl The Class to make a discriminator model from
* @param value The Identifier to use to differentiate documents (default: cl.name)
* @example
* ```ts
* class Main {
* @prop({ ref: () => BaseDiscriminator })
* public discriminators?: Ref<BaseDiscriminator>;
* }
*
* class BaseDiscriminator {
* @prop()
* public propertyOnAllDiscriminators?: string;
* }
*
* class AnotherDiscriminator {
* @prop()
* public someValue?: string;
* }
*
* const MainModel = getModelForClass(Main);
*
* const BaseDiscriminatorModel = getModelFroClass(BaseDiscriminator);
* const AnotherDiscriminatorModel = getDiscriminatorModelForClass(BaseDiscriminatorModel, AnotherDiscriminator);
* // add other discriminator models the same way as "AnotherDiscriminatorModel"
* ```
*/
export function getDiscriminatorModelForClass<U extends AnyParamConstructor<any>, QueryHelpers = BeAnObject>(
from: mongoose.Model<any, any, any, any>,
cl: U,
value?: string
): ReturnModelType<U, QueryHelpers>;
/**
* Build a Model from the given Class and add it as a discriminator onto "from"
* @param from The Model to add the new discriminator model to
* @param cl The Class to make a discriminator model from
* @param value The Identifier to use to differentiate documents (default: cl.name)
* @param options Overwrite ModelOptions (Merged with ModelOptions from class)
* @example
* ```ts
* class Main {
* @prop({ ref: () => BaseDiscriminator })
* public discriminators?: Ref<BaseDiscriminator>;
* }
*
* class BaseDiscriminator {
* @prop()
* public propertyOnAllDiscriminators?: string;
* }
*
* class AnotherDiscriminator {
* @prop()
* public someValue?: string;
* }
*
* const MainModel = getModelForClass(Main);
*
* const BaseDiscriminatorModel = getModelFroClass(BaseDiscriminator);
* const AnotherDiscriminatorModel = getDiscriminatorModelForClass(BaseDiscriminatorModel, AnotherDiscriminator);
* // add other discriminator models the same way as "AnotherDiscriminatorModel"
* ```
*/
export function getDiscriminatorModelForClass<U extends AnyParamConstructor<any>, QueryHelpers = BeAnObject>(
from: mongoose.Model<any, any, any, any>,
cl: U,
value?: string,
options?: IModelOptions
): ReturnModelType<U, QueryHelpers>;
export function getDiscriminatorModelForClass<U extends AnyParamConstructor<any>, QueryHelpers = BeAnObject>(
from: mongoose.Model<any, any, any, any>,
cl: U,
value_or_options?: string | IModelOptions,
options?: IModelOptions
) {
assertion(isModel(from), new NotValidModelError(from, 'getDiscriminatorModelForClass.from'));
assertionIsClass(cl);
const value = typeof value_or_options === 'string' ? value_or_options : undefined;
const rawOptions = typeof value_or_options !== 'string' ? value_or_options : typeof options === 'object' ? options : {};
const overwriteNaming = mapModelOptionsToNaming(rawOptions); // use "rawOptions" instead of "mergedOptions" to consistently differentiate between classes & models
const mergedOptions = getMergedModelOptions(rawOptions, cl);
const name = getName(cl, overwriteNaming);
if (isCachingEnabled(mergedOptions.options?.disableCaching) && models.has(name)) {
return models.get(name) as ReturnModelType<U, QueryHelpers>;
}
if (mergedOptions.existingConnection && mergedOptions.existingConnection !== from.db) {
warnNotMatchingExisting(from.modelName, getName(cl), 'existingConnection');
}
if (mergedOptions.existingMongoose && mergedOptions.existingMongoose !== from.base) {
warnNotMatchingExisting(from.modelName, getName(cl), 'existingMongoose');
}
const sch: mongoose.Schema<any> = buildSchema(cl, mergedOptions);
const mergeHooks = mergedOptions.options?.enableMergeHooks ?? false;
// Note: this option is not actually for "merging plugins", but if "true" it will *overwrite* all plugins with the base-schema's
const mergePlugins = mergedOptions.options?.enableMergePlugins ?? false;
const discriminatorKey = sch.get('discriminatorKey');
if (!!discriminatorKey && sch.path(discriminatorKey)) {
(sch.paths[discriminatorKey] as any).options.$skipDiscriminatorCheck = true;
}
const compiledModel = from.discriminator(name, sch, {
value: value ? value : name,
mergeHooks,
mergePlugins,
});
return addModelToTypegoose<U, QueryHelpers>(compiledModel, cl, {
disableCaching: mergedOptions.options?.disableCaching,
});
}
/**
* Use this class if raw mongoose for a path is wanted
* It is still recommended to use the typegoose classes directly
* @see Using `Passthrough`, the paths created will also result as an `Schema` (since mongoose 6.0), see {@link https://github.com/Automattic/mongoose/issues/7181 Mongoose#7181}
* @example
* ```ts
* class Dummy {
* @prop({ type: () => new Passthrough({ somePath: String }) })
* public somepath: { somePath: string };
* }
*
* class Dummy {
* @prop({ type: () => new Passthrough({ somePath: String }, true) })
* public somepath: { somePath: string };
* }
* ```
*/
export class Passthrough {
// this property has no types, because it can slightly differentiate than a normal mongoose schema (like being a direct array)
public raw: any;
public direct: boolean;
/**
* Use this like `new mongoose.Schema()`
* @param raw The Schema definition
* @param direct Directly insert "raw", instead of using "type" (this will not apply any other inner options)
*/
constructor(raw: any, direct?: boolean) {
this.raw = raw;
this.direct = direct ?? false;
}
}