Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
[travis-url]: https://travis-ci.org/nestjs/nest
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
[linux-url]: https://travis-ci.org/nestjs/nest
A progressive Node.js framework for building efficient and scalable server-side applications.
## Description
[Mongoose](http://mongoosejs.com/) module for [Nest](https://github.com/nestjs/nest).
## Installation
```bash
$ npm i --save @nestjs/mongoose mongoose
```
## Quick Start
[Overview & Tutorial](https://docs.nestjs.com/techniques/mongodb)
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
* Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
* Website - [https://nestjs.com](https://nestjs.com/)
* Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).
================================================
FILE: docker-compose.yml
================================================
version: "3"
services:
mongodb:
image: mongo:latest
environment:
- MONGODB_DATABASE="test"
ports:
- 27017:27017
================================================
FILE: eslint.config.mjs
================================================
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: [],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/require-await': 'warn',
'@typescript-eslint/no-misused-promises': 'warn',
'no-self-assign': 'warn',
'@typescript-eslint/restrict-template-expressions': 'warn',
'@typescript-eslint/no-redundant-type-constituents': 'warn'
},
},
);
================================================
FILE: lib/common/index.ts
================================================
export * from './mongoose.decorators';
export { getConnectionToken, getModelToken } from './mongoose.utils';
================================================
FILE: lib/common/mongoose.decorators.ts
================================================
import { Inject } from '@nestjs/common';
import { getConnectionToken, getModelToken } from './mongoose.utils';
/**
* @publicApi
*/
export const InjectModel = (model: string, connectionName?: string) =>
Inject(getModelToken(model, connectionName));
/**
* @publicApi
*/
export const InjectConnection = (name?: string) =>
Inject(getConnectionToken(name));
================================================
FILE: lib/common/mongoose.utils.ts
================================================
import { Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { delay, retryWhen, scan } from 'rxjs/operators';
import { DEFAULT_DB_CONNECTION } from '../mongoose.constants';
/**
* @publicApi
*/
export function getModelToken(model: string, connectionName?: string) {
if (connectionName === undefined) {
return `${model}Model`;
}
return `${getConnectionToken(connectionName)}/${model}Model`;
}
/**
* @publicApi
*/
export function getConnectionToken(name?: string) {
return name && name !== DEFAULT_DB_CONNECTION
? `${name}Connection`
: DEFAULT_DB_CONNECTION;
}
export function handleRetry(
retryAttempts = 9,
retryDelay = 3000,
verboseRetryLog = false,
): (source: Observable) => Observable {
const logger = new Logger('MongooseModule');
return (source: Observable) =>
source.pipe(
retryWhen((e) =>
e.pipe(
scan((errorCount, error) => {
const verboseMessage = verboseRetryLog
? ` Message: ${error.message}.`
: '';
const retryMessage =
retryAttempts > 0 ? ` Retrying (${errorCount + 1})...` : '';
logger.error(
[
'Unable to connect to the database.',
verboseMessage,
retryMessage,
].join(''),
error.stack,
);
if (errorCount + 1 >= retryAttempts) {
throw error;
}
return errorCount + 1;
}, 0),
delay(retryDelay),
),
),
);
}
================================================
FILE: lib/decorators/index.ts
================================================
export * from './prop.decorator';
export * from './schema.decorator';
export * from './virtual.decorator';
================================================
FILE: lib/decorators/prop.decorator.ts
================================================
import * as mongoose from 'mongoose';
import { CannotDetermineTypeError } from '../errors';
import { RAW_OBJECT_DEFINITION } from '../mongoose.constants';
import { TypeMetadataStorage } from '../storages/type-metadata.storage';
const TYPE_METADATA_KEY = 'design:type';
/**
* Interface defining property options that can be passed to `@Prop()` decorator.
*/
export type PropOptions =
| Partial>
| mongoose.SchemaType;
/**
* @Prop decorator is used to mark a specific class property as a Mongoose property.
* Only properties decorated with this decorator will be defined in the schema.
*
* @publicApi
*/
export function Prop(options?: PropOptions): PropertyDecorator {
return (target: object, propertyKey: string | symbol) => {
options = (options || {}) as mongoose.SchemaTypeOptions;
const isRawDefinition = options[RAW_OBJECT_DEFINITION];
if (!options.type && !Array.isArray(options) && !isRawDefinition) {
const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
if (type === Array) {
options.type = [];
} else if (type && type !== Object) {
options.type = type;
} else {
throw new CannotDetermineTypeError(
target.constructor?.name,
propertyKey as string,
);
}
}
TypeMetadataStorage.addPropertyMetadata({
target: target.constructor,
propertyKey: propertyKey as string,
options: options as PropOptions,
});
};
}
================================================
FILE: lib/decorators/schema.decorator.ts
================================================
import * as mongoose from 'mongoose';
import { TypeMetadataStorage } from '../storages/type-metadata.storage';
/**
* Interface defining schema options that can be passed to `@Schema()` decorator.
*/
export type SchemaOptions = mongoose.SchemaOptions;
/**
* @Schema decorator is used to mark a class as a Mongoose schema.
* Only properties decorated with this decorator will be defined in the schema.
*
* @publicApi
*/
export function Schema(options?: SchemaOptions): ClassDecorator {
return (target: Function) => {
TypeMetadataStorage.addSchemaMetadata({
target,
options,
});
};
}
================================================
FILE: lib/decorators/virtual.decorator.ts
================================================
import { VirtualTypeOptions } from 'mongoose';
import { TypeMetadataStorage } from '../storages/type-metadata.storage';
/**
* Interface defining the options that can be passed to the `@Virtual()` decorator.
*
* @publicApi
*/
export interface VirtualOptions {
/**
* The options to pass to the virtual type.
*/
options?: VirtualTypeOptions;
/**
* The sub path to use for the virtual.
* Defaults to the property key.
*/
subPath?: string;
/**
* The getter function to use for the virtual.
*/
get?: (...args: any[]) => any;
/**
* The setter function to use for the virtual.
*/
set?: (...args: any[]) => any;
}
/**
* The Virtual decorator marks a class property as a Mongoose virtual.
*
* @publicApi
*/
export function Virtual(options?: VirtualOptions): PropertyDecorator {
return (target: object, propertyKey: string | symbol) => {
TypeMetadataStorage.addVirtualMetadata({
target: target.constructor,
options: options?.options,
name:
propertyKey.toString() +
(options?.subPath ? `.${options.subPath}` : ''),
setter: options?.set,
getter: options?.get,
});
};
}
================================================
FILE: lib/errors/cannot-determine-type.error.ts
================================================
export class CannotDetermineTypeError extends Error {
constructor(hostClass: string, propertyKey: string) {
super(
`Cannot determine a type for the "${hostClass}.${propertyKey}" field (union/intersection/ambiguous type was used). Make sure your property is decorated with a "@Prop({ type: TYPE_HERE })" decorator.`,
);
}
}
================================================
FILE: lib/errors/index.ts
================================================
export * from './cannot-determine-type.error';
================================================
FILE: lib/factories/definitions.factory.ts
================================================
import { Type } from '@nestjs/common';
import { isUndefined } from '@nestjs/common/utils/shared.utils';
import * as mongoose from 'mongoose';
import { PropOptions } from '../decorators';
import { TypeMetadataStorage } from '../storages/type-metadata.storage';
const BUILT_IN_TYPES: Function[] = [
Boolean,
Number,
String,
Map,
Date,
Buffer,
BigInt,
];
export class DefinitionsFactory {
static createForClass(target: Type): mongoose.SchemaDefinition {
if (!target) {
throw new Error(
`Target class "${target}" passed in to the "DefinitionsFactory#createForClass()" method is "undefined".`,
);
}
let schemaDefinition: mongoose.SchemaDefinition = {};
let parent: Function = target;
while (!isUndefined(parent.prototype)) {
if (parent === Function.prototype) {
break;
}
const schemaMetadata = TypeMetadataStorage.getSchemaMetadataByTarget(
parent as Type,
);
if (!schemaMetadata) {
parent = Object.getPrototypeOf(parent);
continue;
}
schemaMetadata.properties?.forEach((item) => {
const options = this.inspectTypeDefinition(item.options as any);
this.inspectRef(item.options as any);
schemaDefinition = {
[item.propertyKey]: options as any,
...schemaDefinition,
};
});
parent = Object.getPrototypeOf(parent);
}
return schemaDefinition;
}
private static inspectTypeDefinition(
optionsOrType: mongoose.SchemaTypeOptions | Function,
): PropOptions | [PropOptions] | Function | mongoose.Schema {
if (typeof optionsOrType === 'function') {
if (this.isPrimitive(optionsOrType)) {
return optionsOrType;
} else if (this.isMongooseSchemaType(optionsOrType)) {
return optionsOrType;
}
const isClass = /^class\s/.test(
Function.prototype.toString.call(optionsOrType),
);
optionsOrType = isClass ? optionsOrType : optionsOrType();
const schemaDefinition = this.createForClass(
optionsOrType as Type,
);
const schemaMetadata = TypeMetadataStorage.getSchemaMetadataByTarget(
optionsOrType as Type,
);
if (schemaMetadata?.options) {
/**
* When options are provided (e.g., `@Schema({ timestamps: true })`)
* create a new nested schema for a subdocument
* @ref https://mongoosejs.com/docs/subdocs.html
**/
return new mongoose.Schema(
schemaDefinition,
schemaMetadata.options,
) as mongoose.Schema;
}
return schemaDefinition;
} else if (
typeof optionsOrType.type === 'function' ||
(Array.isArray(optionsOrType.type) &&
typeof optionsOrType.type[0] === 'function')
) {
optionsOrType.type = this.inspectTypeDefinition(optionsOrType.type);
return optionsOrType;
} else if (Array.isArray(optionsOrType)) {
return optionsOrType.length > 0
? [this.inspectTypeDefinition(optionsOrType[0])]
: (optionsOrType as any);
}
return optionsOrType;
}
private static inspectRef(
optionsOrType: mongoose.SchemaTypeOptions | Function,
) {
if (!optionsOrType || typeof optionsOrType !== 'object') {
return;
}
if (typeof optionsOrType?.ref === 'function') {
try {
const result = (optionsOrType.ref as Function)();
if (typeof result?.name === 'string') {
optionsOrType.ref = result.name;
}
optionsOrType.ref = optionsOrType.ref;
} catch (err) {
if (err instanceof TypeError) {
const refClassName = (optionsOrType.ref as Function)?.name;
throw new Error(
`Unsupported syntax: Class constructor "${refClassName}" cannot be invoked without 'new'. Make sure to wrap your class reference in an arrow function (for example, "ref: () => ${refClassName}").`,
);
}
throw err;
}
} else if (Array.isArray(optionsOrType.type)) {
if (optionsOrType.type.length > 0) {
this.inspectRef(optionsOrType.type[0]);
}
}
}
private static isPrimitive(type: Function) {
return BUILT_IN_TYPES.includes(type);
}
private static isMongooseSchemaType(type: Function) {
if (!type || !type.prototype) {
return false;
}
const prototype = Object.getPrototypeOf(type.prototype);
return prototype && prototype.constructor === mongoose.SchemaType;
}
}
================================================
FILE: lib/factories/index.ts
================================================
export * from './definitions.factory';
export * from './schema.factory';
export * from './virtuals.factory';
================================================
FILE: lib/factories/schema.factory.ts
================================================
import { Type } from '@nestjs/common';
import * as mongoose from 'mongoose';
import { SchemaDefinition, SchemaDefinitionType } from 'mongoose';
import { TypeMetadataStorage } from '../storages/type-metadata.storage';
import { DefinitionsFactory } from './definitions.factory';
import { VirtualsFactory } from './virtuals.factory';
/**
* @publicApi
*/
export class SchemaFactory {
static createForClass(
target: Type,
): mongoose.Schema {
const schemaDefinition = DefinitionsFactory.createForClass(target);
const schemaMetadata =
TypeMetadataStorage.getSchemaMetadataByTarget(target);
const schemaOpts = schemaMetadata?.options;
const schema = new mongoose.Schema(
schemaDefinition as SchemaDefinition>,
schemaOpts as mongoose.SchemaOptions,
);
VirtualsFactory.inspect(target, schema);
return schema;
}
}
================================================
FILE: lib/factories/virtuals.factory.ts
================================================
import { Type } from '@nestjs/common';
import { isUndefined } from '@nestjs/common/utils/shared.utils';
import * as mongoose from 'mongoose';
import { TypeMetadataStorage } from '../storages/type-metadata.storage';
/**
* @publicApi
*/
export class VirtualsFactory {
static inspect(
target: Type,
schema: mongoose.Schema,
): void {
let parent = target;
while (!isUndefined(parent.prototype)) {
if (parent === Function.prototype) {
break;
}
const virtuals = TypeMetadataStorage.getVirtualsMetadataByTarget(parent);
virtuals.forEach(({ options, name, getter, setter }) => {
const virtual = schema.virtual(name, options);
if (getter) {
virtual.get(getter);
}
if (setter) {
virtual.set(setter);
}
});
parent = Object.getPrototypeOf(parent);
}
}
}
================================================
FILE: lib/index.ts
================================================
export * from './common';
export * from './decorators';
export * from './errors';
export * from './factories';
export * from './interfaces';
export * from './mongoose.module';
export * from './pipes';
export * from './utils';
================================================
FILE: lib/interfaces/async-model-factory.interface.ts
================================================
import { ModuleMetadata } from '@nestjs/common';
import { ModelDefinition } from './model-definition.interface';
/**
* @publicApi
*/
export interface AsyncModelFactory
extends Pick,
Pick {
useFactory: (
...args: any[]
) => ModelDefinition['schema'] | Promise;
inject?: any[];
}
================================================
FILE: lib/interfaces/index.ts
================================================
export * from './async-model-factory.interface';
export * from './model-definition.interface';
export * from './mongoose-options.interface';
================================================
FILE: lib/interfaces/model-definition.interface.ts
================================================
import { Schema } from 'mongoose';
/**
* @publicApi
*/
export type DiscriminatorOptions = {
name: string;
schema: Schema;
value?: string;
};
/**
* @publicApi
*/
export type ModelDefinition = {
name: string;
schema: any;
collection?: string;
discriminators?: DiscriminatorOptions[];
};
================================================
FILE: lib/interfaces/mongoose-options.interface.ts
================================================
import { ModuleMetadata, Type } from '@nestjs/common';
import { ConnectOptions, Connection, MongooseError } from 'mongoose';
/**
* @publicApi
*/
export interface MongooseModuleOptions extends ConnectOptions {
uri?: string;
retryAttempts?: number;
retryDelay?: number;
connectionName?: string;
connectionFactory?: (connection: any, name: string) => any;
connectionErrorFactory?: (error: MongooseError) => MongooseError;
lazyConnection?: boolean;
onConnectionCreate?: (connection: Connection) => void;
/**
* If `true`, will show verbose error messages on each connection retry.
*/
verboseRetryLog?: boolean;
}
/**
* @publicApi
*/
export interface MongooseOptionsFactory {
createMongooseOptions():
| Promise
| MongooseModuleOptions;
}
/**
* @publicApi
*/
export type MongooseModuleFactoryOptions = Omit<
MongooseModuleOptions,
'connectionName'
>;
/**
* @publicApi
*/
export interface MongooseModuleAsyncOptions
extends Pick {
connectionName?: string;
useExisting?: Type;
useClass?: Type;
useFactory?: (
...args: any[]
) => Promise | MongooseModuleFactoryOptions;
inject?: any[];
}
================================================
FILE: lib/metadata/property-metadata.interface.ts
================================================
import { PropOptions } from '../decorators/prop.decorator';
export interface PropertyMetadata {
target: Function;
propertyKey: string;
options: PropOptions;
}
================================================
FILE: lib/metadata/schema-metadata.interface.ts
================================================
import * as mongoose from 'mongoose';
import { PropertyMetadata } from './property-metadata.interface';
export interface SchemaMetadata {
target: Function;
options?: mongoose.SchemaOptions;
properties?: PropertyMetadata[];
}
================================================
FILE: lib/metadata/virtual-metadata.interface.ts
================================================
import { VirtualTypeOptions } from 'mongoose';
export interface VirtualMetadataInterface {
target: Function;
name: string;
options?: VirtualTypeOptions;
getter?: (...args: any[]) => any;
setter?: (...args: any[]) => any;
}
================================================
FILE: lib/mongoose-core.module.ts
================================================
import {
DynamicModule,
Global,
Inject,
Module,
OnApplicationShutdown,
Provider,
Type,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import * as mongoose from 'mongoose';
import { ConnectOptions, Connection } from 'mongoose';
import { defer, lastValueFrom } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { getConnectionToken, handleRetry } from './common/mongoose.utils';
import {
MongooseModuleAsyncOptions,
MongooseModuleFactoryOptions,
MongooseModuleOptions,
MongooseOptionsFactory,
} from './interfaces/mongoose-options.interface';
import {
MONGOOSE_CONNECTION_NAME,
MONGOOSE_MODULE_OPTIONS,
} from './mongoose.constants';
@Global()
@Module({})
export class MongooseCoreModule implements OnApplicationShutdown {
constructor(
@Inject(MONGOOSE_CONNECTION_NAME) private readonly connectionName: string,
private readonly moduleRef: ModuleRef,
) {}
static forRoot(
uri: string,
options: MongooseModuleOptions = {},
): DynamicModule {
const {
retryAttempts,
retryDelay,
connectionName,
connectionFactory,
connectionErrorFactory,
lazyConnection,
onConnectionCreate,
verboseRetryLog,
...mongooseOptions
} = options;
const mongooseConnectionFactory =
connectionFactory || ((connection) => connection);
const mongooseConnectionError =
connectionErrorFactory || ((error) => error);
const mongooseConnectionName = getConnectionToken(connectionName);
const mongooseConnectionNameProvider = {
provide: MONGOOSE_CONNECTION_NAME,
useValue: mongooseConnectionName,
};
const connectionProvider = {
provide: mongooseConnectionName,
useFactory: async (): Promise =>
await lastValueFrom(
defer(async () =>
mongooseConnectionFactory(
await this.createMongooseConnection(uri, mongooseOptions, {
lazyConnection,
onConnectionCreate,
}),
mongooseConnectionName,
),
).pipe(
handleRetry(retryAttempts, retryDelay, verboseRetryLog),
catchError((error) => {
throw mongooseConnectionError(error);
}),
),
),
};
return {
module: MongooseCoreModule,
providers: [connectionProvider, mongooseConnectionNameProvider],
exports: [connectionProvider],
};
}
static forRootAsync(options: MongooseModuleAsyncOptions): DynamicModule {
const mongooseConnectionName = getConnectionToken(options.connectionName);
const mongooseConnectionNameProvider = {
provide: MONGOOSE_CONNECTION_NAME,
useValue: mongooseConnectionName,
};
const connectionProvider = {
provide: mongooseConnectionName,
useFactory: async (
mongooseModuleOptions: MongooseModuleFactoryOptions,
): Promise => {
const {
retryAttempts,
retryDelay,
uri,
connectionFactory,
connectionErrorFactory,
lazyConnection,
onConnectionCreate,
verboseRetryLog,
...mongooseOptions
} = mongooseModuleOptions;
const mongooseConnectionFactory =
connectionFactory || ((connection) => connection);
const mongooseConnectionError =
connectionErrorFactory || ((error) => error);
return await lastValueFrom(
defer(async () =>
mongooseConnectionFactory(
await this.createMongooseConnection(
uri as string,
mongooseOptions,
{ lazyConnection, onConnectionCreate },
),
mongooseConnectionName,
),
).pipe(
handleRetry(retryAttempts, retryDelay, verboseRetryLog),
catchError((error) => {
throw mongooseConnectionError(error);
}),
),
);
},
inject: [MONGOOSE_MODULE_OPTIONS],
};
const asyncProviders = this.createAsyncProviders(options);
return {
module: MongooseCoreModule,
imports: options.imports,
providers: [
...asyncProviders,
connectionProvider,
mongooseConnectionNameProvider,
],
exports: [connectionProvider],
};
}
private static createAsyncProviders(
options: MongooseModuleAsyncOptions,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
const useClass = options.useClass as Type;
return [
this.createAsyncOptionsProvider(options),
{
provide: useClass,
useClass,
},
];
}
private static createAsyncOptionsProvider(
options: MongooseModuleAsyncOptions,
): Provider {
if (options.useFactory) {
return {
provide: MONGOOSE_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
// `as Type` is a workaround for microsoft/TypeScript#31603
const inject = [
(options.useClass || options.useExisting) as Type,
];
return {
provide: MONGOOSE_MODULE_OPTIONS,
useFactory: async (optionsFactory: MongooseOptionsFactory) =>
await optionsFactory.createMongooseOptions(),
inject,
};
}
private static async createMongooseConnection(
uri: string,
mongooseOptions: ConnectOptions,
factoryOptions: {
lazyConnection?: boolean;
onConnectionCreate?: MongooseModuleOptions['onConnectionCreate'];
},
): Promise {
const connection = mongoose.createConnection(uri, mongooseOptions);
if (factoryOptions?.lazyConnection) {
return connection;
}
factoryOptions?.onConnectionCreate?.(connection);
return connection.asPromise();
}
async onApplicationShutdown() {
const connection = this.moduleRef.get(this.connectionName);
if (connection) {
await connection.close();
}
}
}
================================================
FILE: lib/mongoose.constants.ts
================================================
export const DEFAULT_DB_CONNECTION = 'DatabaseConnection';
export const MONGOOSE_MODULE_OPTIONS = 'MongooseModuleOptions';
export const MONGOOSE_CONNECTION_NAME = 'MongooseConnectionName';
export const RAW_OBJECT_DEFINITION = 'RAW_OBJECT_DEFINITION';
================================================
FILE: lib/mongoose.module.ts
================================================
import { DynamicModule, flatten, Module } from '@nestjs/common';
import { AsyncModelFactory, ModelDefinition } from './interfaces';
import {
MongooseModuleAsyncOptions,
MongooseModuleOptions,
} from './interfaces/mongoose-options.interface';
import { MongooseCoreModule } from './mongoose-core.module';
import {
createMongooseAsyncProviders,
createMongooseProviders,
} from './mongoose.providers';
/**
* @publicApi
*/
@Module({})
export class MongooseModule {
static forRoot(
uri: string,
options: MongooseModuleOptions = {},
): DynamicModule {
return {
module: MongooseModule,
imports: [MongooseCoreModule.forRoot(uri, options)],
};
}
static forRootAsync(options: MongooseModuleAsyncOptions): DynamicModule {
return {
module: MongooseModule,
imports: [MongooseCoreModule.forRootAsync(options)],
};
}
static forFeature(
models: ModelDefinition[] = [],
connectionName?: string,
): DynamicModule {
const providers = createMongooseProviders(connectionName, models);
return {
module: MongooseModule,
providers: providers,
exports: providers,
};
}
static forFeatureAsync(
factories: AsyncModelFactory[] = [],
connectionName?: string,
): DynamicModule {
const providers = createMongooseAsyncProviders(connectionName, factories);
const imports = factories.map((factory) => factory.imports || []);
const uniqImports = new Set(flatten(imports));
return {
module: MongooseModule,
imports: [...uniqImports],
providers: providers,
exports: providers,
};
}
}
================================================
FILE: lib/mongoose.providers.ts
================================================
import { Provider } from '@nestjs/common';
import { Connection, Document, Model } from 'mongoose';
import { getConnectionToken, getModelToken } from './common/mongoose.utils';
import { AsyncModelFactory, ModelDefinition } from './interfaces';
export function createMongooseProviders(
connectionName?: string,
options: ModelDefinition[] = [],
): Provider[] {
return options.reduce(
(providers, option) => [
...providers,
...(option.discriminators || []).map((d) => ({
provide: getModelToken(d.name, connectionName),
useFactory: (model: Model) =>
model.discriminator(d.name, d.schema, d.value),
inject: [getModelToken(option.name, connectionName)],
})),
{
provide: getModelToken(option.name, connectionName),
useFactory: (connection: Connection) => {
const model = connection.models[option.name]
? connection.models[option.name]
: connection.model(option.name, option.schema, option.collection);
return model;
},
inject: [getConnectionToken(connectionName)],
},
],
[] as Provider[],
);
}
export function createMongooseAsyncProviders(
connectionName?: string,
modelFactories: AsyncModelFactory[] = [],
): Provider[] {
return modelFactories.reduce((providers, option) => {
return [
...providers,
{
provide: getModelToken(option.name, connectionName),
useFactory: async (connection: Connection, ...args: unknown[]) => {
const schema = await option.useFactory(...args);
const model = connection.model(
option.name,
schema,
option.collection,
);
return model;
},
inject: [getConnectionToken(connectionName), ...(option.inject || [])],
},
...(option.discriminators || []).map((d) => ({
provide: getModelToken(d.name, connectionName),
useFactory: (model: Model) =>
model.discriminator(d.name, d.schema, d.value),
inject: [getModelToken(option.name, connectionName)],
})),
];
}, [] as Provider[]);
}
================================================
FILE: lib/pipes/index.ts
================================================
export * from './is-object-id.pipe';
export * from './parse-object-id.pipe';
================================================
FILE: lib/pipes/is-object-id.pipe.ts
================================================
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import { Types } from 'mongoose';
@Injectable()
export class IsObjectIdPipe implements PipeTransform {
transform(value: string): string {
const isValidObjectId = Types.ObjectId.isValid(value);
if (!isValidObjectId) {
throw new BadRequestException(
`Invalid ObjectId: '${value}' is not a valid MongoDB ObjectId`,
);
}
return value;
}
}
================================================
FILE: lib/pipes/parse-object-id.pipe.ts
================================================
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import { Types } from 'mongoose';
@Injectable()
export class ParseObjectIdPipe implements PipeTransform {
transform(value: string): Types.ObjectId {
const isValidObjectId = Types.ObjectId.isValid(value);
if (!isValidObjectId) {
throw new BadRequestException(
`Invalid ObjectId: '${value}' is not a valid MongoDB ObjectId`,
);
}
return new Types.ObjectId(value);
}
}
================================================
FILE: lib/storages/type-metadata.storage.ts
================================================
import { Type } from '@nestjs/common';
import { PropertyMetadata } from '../metadata/property-metadata.interface';
import { SchemaMetadata } from '../metadata/schema-metadata.interface';
import { VirtualMetadataInterface } from '../metadata/virtual-metadata.interface';
import { isTargetEqual } from '../utils/is-target-equal-util';
export class TypeMetadataStorageHost {
private schemas = new Array();
private properties = new Array();
private virtuals = new Array();
addPropertyMetadata(metadata: PropertyMetadata) {
this.properties.unshift(metadata);
}
addSchemaMetadata(metadata: SchemaMetadata) {
this.compileClassMetadata(metadata);
this.schemas.push(metadata);
}
addVirtualMetadata(metadata: VirtualMetadataInterface) {
this.virtuals.push(metadata);
}
getSchemaMetadataByTarget(target: Type): SchemaMetadata | undefined {
return this.schemas.find((item) => item.target === target);
}
getVirtualsMetadataByTarget(targetFilter: Type) {
return this.virtuals.filter(({ target }) => target === targetFilter);
}
private compileClassMetadata(metadata: SchemaMetadata) {
const belongsToClass = isTargetEqual.bind(undefined, metadata);
if (!metadata.properties) {
metadata.properties = this.getClassFieldsByPredicate(belongsToClass);
}
}
private getClassFieldsByPredicate(
belongsToClass: (item: PropertyMetadata) => boolean,
) {
return this.properties.filter(belongsToClass);
}
}
const globalRef = global as any;
export const TypeMetadataStorage: TypeMetadataStorageHost =
globalRef.MongoTypeMetadataStorage ||
(globalRef.MongoTypeMetadataStorage = new TypeMetadataStorageHost());
================================================
FILE: lib/utils/index.ts
================================================
export * from './raw.util';
================================================
FILE: lib/utils/is-target-equal-util.ts
================================================
export type TargetHost = Record<'target', Function>;
export function isTargetEqual(
a: T,
b: U,
) {
return (
a.target === b.target ||
(a.target.prototype
? isTargetEqual({ target: (a.target as any).__proto__ }, b)
: false)
);
}
================================================
FILE: lib/utils/raw.util.ts
================================================
import { RAW_OBJECT_DEFINITION } from '../mongoose.constants';
export function raw(definition: Record) {
Object.defineProperty(definition, RAW_OBJECT_DEFINITION, {
value: true,
enumerable: false,
configurable: false,
});
return definition;
}
================================================
FILE: package.json
================================================
{
"name": "@nestjs/mongoose",
"version": "11.0.4",
"description": "Nest - modern, fast, powerful node.js web framework (@mongoose)",
"author": "Kamil Mysliwiec",
"repository": "https://github.com/nestjs/mongoose.git",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint \"lib/**/*.ts\" --fix",
"format": "prettier \"lib/**/*.ts\" --write",
"build": "rm -rf dist && tsc -p tsconfig.build.json",
"precommit": "lint-staged",
"prepublish:npm": "npm run build",
"publish:npm": "npm publish --access public",
"prepublish:next": "npm run build",
"publish:next": "npm publish --access public --tag next",
"prerelease": "npm run build",
"release": "release-it",
"test:e2e": "jest --config ./tests/jest-e2e.json --runInBand",
"test:e2e:dev": "jest --config ./tests/jest-e2e.json --runInBand --watch",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/cli": "20.5.0",
"@commitlint/config-angular": "20.5.0",
"@eslint/eslintrc": "3.3.5",
"@eslint/js": "10.0.1",
"@nestjs/common": "11.1.18",
"@nestjs/core": "11.1.18",
"@nestjs/platform-express": "11.1.18",
"@nestjs/testing": "11.1.18",
"@types/jest": "30.0.0",
"@types/node": "24.12.2",
"eslint": "10.2.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-prettier": "5.5.5",
"globals": "17.4.0",
"husky": "9.1.7",
"jest": "30.3.0",
"lint-staged": "16.4.0",
"mongoose": "9.4.1",
"prettier": "3.8.2",
"reflect-metadata": "0.2.2",
"release-it": "19.2.4",
"rxjs": "7.8.2",
"supertest": "7.2.2",
"ts-jest": "29.4.9",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"typescript-eslint": "8.58.1"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0",
"mongoose": "^7.0.0 || ^8.0.0 || ^9.0.0",
"rxjs": "^7.0.0"
},
"lint-staged": {
"**/*.{ts,json}": []
}
}
================================================
FILE: renovate.json
================================================
{
"semanticCommits": true,
"packageRules": [{
"depTypeList": ["devDependencies"],
"automerge": true
}],
"extends": [
"config:base"
]
}
================================================
FILE: tests/e2e/discriminator.spec.ts
================================================
import { DynamicModule, HttpStatus, INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { Server } from 'http';
import * as request from 'supertest';
import { MongooseModule } from '../../lib';
import { EventModule } from '../src/event/event.module';
import {
ClickLinkEvent,
ClieckLinkEventSchema,
} from '../src/event/schemas/click-link-event.schema';
import { Event, EventSchema } from '../src/event/schemas/event.schema';
import {
SignUpEvent,
SignUpEventSchema,
} from '../src/event/schemas/sign-up-event.schema';
const testCase: [string, DynamicModule][] = [
[
'forFeature',
MongooseModule.forFeature([
{
name: Event.name,
schema: EventSchema,
discriminators: [
{ name: ClickLinkEvent.name, schema: ClieckLinkEventSchema },
{ name: SignUpEvent.name, schema: SignUpEventSchema },
],
},
]),
],
[
'forFeatureAsync',
MongooseModule.forFeatureAsync([
{
name: Event.name,
useFactory: async () => EventSchema,
discriminators: [
{ name: ClickLinkEvent.name, schema: ClieckLinkEventSchema },
{ name: SignUpEvent.name, schema: SignUpEventSchema },
],
},
]),
],
];
describe.each(testCase)('Discriminator - %s', (_, features) => {
let server: Server;
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/test'),
EventModule.forFeature(features),
],
}).compile();
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
afterEach(async () => {
await app.close();
});
it(`should return click-link document`, async () => {
const createDto = { url: 'http://google.com' };
const response = await request(server)
.post('/event/click-link')
.send(createDto);
expect(response.status).toBe(HttpStatus.CREATED);
expect(response.body).toMatchObject({
...createDto,
kind: expect.any(String),
time: expect.any(String),
});
});
it(`should return sign-up document`, async () => {
const createDto = { user: 'testuser' };
const response = await request(server)
.post('/event/sign-up')
.send(createDto);
expect(response.status).toBe(HttpStatus.CREATED);
expect(response.body).toMatchObject({
...createDto,
kind: expect.any(String),
time: expect.any(String),
});
});
test.each`
path | payload
${'click-link'} | ${{ testing: 1 }}
${'sign-up'} | ${{ testing: 1 }}
`(`document ($path) should not be created`, async ({ path, payload }) => {
const response = await request(server).post(`/event/${path}`).send(payload);
expect(response.error).toBeInstanceOf(Error);
expect(response.status).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
});
});
================================================
FILE: tests/e2e/mongoose-lazy-connection.spec.ts
================================================
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { Server } from 'http';
import * as request from 'supertest';
import { LazyAppModule } from '../src/lazy-app.module';
describe('Mongoose lazy connection', () => {
let server: Server;
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [LazyAppModule],
}).compile();
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
it(`should return created document`, (done) => {
const createDto = { name: 'Nest', breed: 'Maine coon', age: 5 };
request(server)
.post('/cats')
.send(createDto)
.expect(201)
.end((err, { body }) => {
expect(body.name).toEqual(createDto.name);
expect(body.age).toEqual(createDto.age);
expect(body.breed).toEqual(createDto.breed);
done();
});
});
afterEach(async () => {
await app.close();
});
});
================================================
FILE: tests/e2e/mongoose.spec.ts
================================================
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { Server } from 'http';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { CreateCatDto } from '../src/cats/dto/create-cat.dto';
import { Cat } from '../src/cats/schemas/cat.schema';
describe('Mongoose', () => {
let server: Server;
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
it(`should return created document`, (done) => {
const createDto = { name: 'Nest', breed: 'Maine coon', age: 5 };
request(server)
.post('/cats')
.send(createDto)
.expect(201)
.end((err, { body }) => {
expect(body.name).toEqual(createDto.name);
expect(body.age).toEqual(createDto.age);
expect(body.breed).toEqual(createDto.breed);
done();
});
});
it('should populate array of kittens', async () => {
let createDto: CreateCatDto = {
name: 'Kitten',
breed: 'Maine coon',
age: 1,
};
const kitten: Cat = await new Promise((resolve) => {
request(server)
.post('/cats')
.send(createDto)
.expect(201)
.end((err, { body }) => {
expect(body.name).toEqual(createDto.name);
expect(body.age).toEqual(createDto.age);
expect(body.breed).toEqual(createDto.breed);
resolve(body);
});
});
createDto = {
...createDto,
name: 'Nest',
age: 5,
kitten: [kitten._id.toString()],
};
const parent = await new Promise((resolve) => {
request(server)
.post('/cats')
.send(createDto)
.expect(201)
.end((err, { body }) => {
expect(body.name).toEqual(createDto.name);
expect(body.age).toEqual(createDto.age);
expect(body.breed).toEqual(createDto.breed);
resolve(body._id as string);
});
});
await new Promise((resolve) => {
request(server)
.get(`/cat/${parent}`)
.expect(200)
.end((err, { body }) => {
expect(Array.isArray(body.kitten)).toBe(true);
expect(body.kitten[0]._id).toBe(kitten._id);
expect(body.kitten[0].name).toBe(kitten.name);
expect(body.kitten[0].breed).toBe(kitten.breed);
expect(body.kitten[0].age).toBe(kitten.age);
resolve();
});
});
});
afterEach(async () => {
await app.close();
});
});
================================================
FILE: tests/e2e/schema-definitions.factory.spec.ts
================================================
import * as mongoose from 'mongoose';
import { DefinitionsFactory, Prop, raw, Schema } from '../../lib';
import { CannotDetermineTypeError } from '../../lib/errors';
@Schema()
class RefClass {
@Prop()
title: string;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: () => ExampleClass })
host;
}
@Schema()
class ChildClass {
@Prop()
id: number;
@Prop()
name: string;
}
@Schema()
class ExampleClass {
@Prop()
objectId: mongoose.Schema.Types.ObjectId;
@Prop({ required: true })
name: string;
@Prop()
buffer: mongoose.Schema.Types.Buffer;
@Prop()
decimal: mongoose.Schema.Types.Decimal128;
@Prop()
mixed: mongoose.Schema.Types.Mixed;
@Prop(
raw({
expires: 0,
type: Date,
}),
)
expiresAt: Date;
@Prop()
map: Map;
@Prop()
isEnabled: boolean;
@Prop()
number: number;
@Prop({
type: [{ type: mongoose.Schema.Types.ObjectId, ref: () => RefClass }],
})
ref: RefClass[];
@Prop({ required: true })
child: ChildClass;
@Prop({ type: () => ChildClass })
child2: ChildClass;
@Prop([ChildClass])
nodes: ChildClass[];
@Prop([raw({ custom: 'literal', object: true })])
customArray: any;
@Prop(raw({ custom: 'literal', object: true }))
customObject: any;
@Prop({ type: mongoose.Schema.Types.Mixed })
any: any;
@Prop()
array: Array;
@Prop()
bigint: bigint;
}
describe('DefinitionsFactory', () => {
it('should generate a valid schema definition', () => {
const definition = DefinitionsFactory.createForClass(ExampleClass);
expect(Object.keys(definition)).toEqual([
'objectId',
'name',
'buffer',
'decimal',
'mixed',
'expiresAt',
'map',
'isEnabled',
'number',
'ref',
'child',
'child2',
'nodes',
'customArray',
'customObject',
'any',
'array',
'bigint',
]);
expect(definition).toEqual({
objectId: {
type: mongoose.Schema.Types.ObjectId,
},
ref: {
type: [
{
ref: 'RefClass',
type: mongoose.Schema.Types.ObjectId,
},
],
},
name: {
required: true,
type: String,
},
nodes: [
{
id: {
type: Number,
},
name: {
type: String,
},
},
],
bigint: { type: BigInt },
buffer: { type: mongoose.Schema.Types.Buffer },
decimal: { type: mongoose.Schema.Types.Decimal128 },
child: {
required: true,
type: {
id: {
type: Number,
},
name: {
type: String,
},
},
},
child2: {
type: {
id: {
type: Number,
},
name: {
type: String,
},
},
},
any: { type: mongoose.Schema.Types.Mixed },
array: { type: [] },
customArray: [{ custom: 'literal', object: true }],
customObject: { custom: 'literal', object: true },
expiresAt: {
expires: 0,
type: Date,
},
isEnabled: {
type: Boolean,
},
map: {
type: Map,
},
mixed: { type: mongoose.Schema.Types.Mixed },
number: { type: Number },
});
});
it('should generate a valid schema definition (class reference) for cyclic deps', () => {
const refClassDefinition = DefinitionsFactory.createForClass(RefClass);
expect(refClassDefinition).toEqual({
host: {
ref: 'ExampleClass',
type: mongoose.Schema.Types.ObjectId,
},
title: {
type: String,
},
});
});
it('should throw an error when type is ambiguous', () => {
try {
class AmbiguousField {
@Prop()
randomField: object | null;
}
DefinitionsFactory.createForClass(AmbiguousField);
} catch (err) {
expect(err).toBeInstanceOf(CannotDetermineTypeError);
expect((err as Error).message).toEqual(
'Cannot determine a type for the "AmbiguousField.randomField" field (union/intersection/ambiguous type was used). Make sure your property is decorated with a "@Prop({ type: TYPE_HERE })" decorator.',
);
}
});
});
================================================
FILE: tests/e2e/schema.factory.spec.ts
================================================
import { Prop, Schema, SchemaFactory, Virtual } from '../../lib';
@Schema({ validateBeforeSave: false, _id: true, autoIndex: true })
class ChildClass {
@Prop()
id: number;
@Prop()
name: string;
}
const getterFunctionMock = jest.fn();
const setterFunctionMock = jest.fn();
@Schema({
validateBeforeSave: false,
_id: true,
autoIndex: true,
timestamps: true,
})
class ExampleClass {
@Prop({ required: true })
children: ChildClass;
@Prop([ChildClass])
nodes: ChildClass[];
@Prop()
array: Array;
// see https://github.com/nestjs/mongoose/issues/839
@Prop({ type: [ChildClass], required: true })
anotherArray: ChildClass[];
@Virtual({
options: {
localField: 'array',
ref: 'ChildClass',
foreignField: 'id',
},
})
virtualPropsWithOptions: Array;
@Virtual({
get: getterFunctionMock,
set: setterFunctionMock,
})
virtualPropsWithGetterSetterFunctions: Array;
}
describe('SchemaFactory', () => {
it('should populate the schema options', () => {
const schema = SchemaFactory.createForClass(ExampleClass) as any;
expect(schema.$timestamps).toBeDefined();
expect(schema.options).toEqual(
expect.objectContaining({
validateBeforeSave: false,
_id: true,
autoIndex: true,
timestamps: true,
}),
);
expect(schema.childSchemas[0].schema).toEqual(
expect.objectContaining({
options: expect.objectContaining({
validateBeforeSave: false,
_id: true,
autoIndex: true,
}),
}),
);
});
it('should add virtuals with corresponding options', () => {
const {
virtuals: { virtualPropsWithOptions },
} = SchemaFactory.createForClass(ExampleClass) as any;
expect(virtualPropsWithOptions).toEqual(
expect.objectContaining({
path: 'virtualPropsWithOptions',
setters: [expect.any(Function)],
getters: [],
options: expect.objectContaining({
localField: 'array',
ref: 'ChildClass',
foreignField: 'id',
}),
}),
);
});
it('should add virtuals with corresponding getter and setter functions', () => {
const {
virtuals: { virtualPropsWithGetterSetterFunctions },
} = SchemaFactory.createForClass(ExampleClass) as any;
expect(virtualPropsWithGetterSetterFunctions).toEqual(
expect.objectContaining({
path: 'virtualPropsWithGetterSetterFunctions',
setters: [setterFunctionMock],
getters: [getterFunctionMock],
options: {},
}),
);
});
it('should inherit virtuals from parent classes', () => {
@Schema()
class ChildClass extends ExampleClass {}
const { virtuals } = SchemaFactory.createForClass(ChildClass) as any;
expect(virtuals.virtualPropsWithOptions).toBeDefined();
expect(virtuals.virtualPropsWithGetterSetterFunctions).toBeDefined();
});
});
================================================
FILE: tests/e2e/virtual.factory.spec.ts
================================================
import { VirtualsFactory } from '../../lib';
import { VirtualMetadataInterface } from '../../lib/metadata/virtual-metadata.interface';
import { TypeMetadataStorage } from '../../lib/storages/type-metadata.storage';
describe('VirtualsFactory', () => {
const setVirtualSetterFunctionMock = jest.fn();
const setVirtualGetterFunctionMock = jest.fn();
const schemaMock = {
virtual: jest.fn(() => ({
get: setVirtualGetterFunctionMock,
set: setVirtualSetterFunctionMock,
})),
} as any;
const targetConstructorMock = jest.fn();
const virtualOptionsMock = {
ref: 'collectionNameMock',
localField: 'localFieldMockValue',
foreignField: 'foreignFieldMockValue',
};
const virtualMetadataWithOnlyRequiredAttributesMock = {
target: targetConstructorMock,
name: 'attribute1Mock',
};
const virtualMetadataNotLikedToModelMock = {
target: jest.fn(),
name: 'attribute1Mock',
};
const virtualMetadataWithOptionsMock = {
target: targetConstructorMock,
name: 'virtualMetadataWithOptionsMock',
options: virtualOptionsMock,
};
const virtualMetadataWithGetterMock = {
target: targetConstructorMock,
name: 'virtualMetadataWithGetterMock',
options: virtualOptionsMock,
getter: jest.fn(),
};
const virtualMetadataWithSetterMock = {
target: targetConstructorMock,
name: 'virtualMetadataWithSetterMock',
options: virtualOptionsMock,
setter: jest.fn(),
};
const virtualMetadataWithGetterSetterMock = {
target: targetConstructorMock,
name: 'virtualMetadataWithGetterSetterMock',
options: virtualOptionsMock,
getter: jest.fn(),
setter: jest.fn(),
};
beforeEach(() => {
schemaMock.virtual = jest.fn(() => ({
get: setVirtualGetterFunctionMock,
set: setVirtualSetterFunctionMock,
}));
});
afterEach(() => {
jest.resetAllMocks();
});
describe('Schema virtual definition', () => {
it('should not define any virtuals if no virtual definitions are stored', () => {
TypeMetadataStorage['virtuals'] = [];
VirtualsFactory.inspect(targetConstructorMock, schemaMock);
expect(schemaMock.virtual).not.toHaveBeenCalled();
});
it('should not define virtuals if there are no stored virtual definitions linked to the schema model', () => {
TypeMetadataStorage['virtuals'] = [virtualMetadataNotLikedToModelMock];
VirtualsFactory.inspect(targetConstructorMock, schemaMock);
expect(schemaMock.virtual).not.toHaveBeenCalled();
});
it('should define virtuals for each stored virtual metadata linked to the schema model', () => {
TypeMetadataStorage['virtuals'] = [
virtualMetadataWithOnlyRequiredAttributesMock,
virtualMetadataNotLikedToModelMock,
virtualMetadataWithOptionsMock,
];
VirtualsFactory.inspect(targetConstructorMock, schemaMock);
expect(schemaMock.virtual['mock'].calls).toEqual([
[virtualMetadataWithOnlyRequiredAttributesMock.name, undefined],
[
virtualMetadataWithOptionsMock.name,
virtualMetadataWithOptionsMock.options,
],
]);
});
});
describe('Schema virtual getter/setter definitions', () => {
it('should not call the getter/setter methods if no getter/setter is defined in the stored virtual metadata linked to the schema model', () => {
TypeMetadataStorage['virtuals'] = [
virtualMetadataWithOptionsMock,
] as VirtualMetadataInterface[];
VirtualsFactory.inspect(targetConstructorMock, schemaMock);
expect(setVirtualGetterFunctionMock).not.toHaveBeenCalled();
expect(setVirtualSetterFunctionMock).not.toHaveBeenCalled();
});
it('should invoke the getter/setter methods for each stored virtual metadata with defined getter/setter linked to the schema model', () => {
TypeMetadataStorage['virtuals'] = [
virtualMetadataWithOptionsMock,
virtualMetadataWithGetterMock,
virtualMetadataWithSetterMock,
virtualMetadataWithGetterSetterMock,
] as VirtualMetadataInterface[];
VirtualsFactory.inspect(targetConstructorMock, schemaMock);
expect(setVirtualGetterFunctionMock.mock.calls).toEqual([
[virtualMetadataWithGetterMock.getter],
[virtualMetadataWithGetterSetterMock.getter],
]);
expect(setVirtualSetterFunctionMock.mock.calls).toEqual([
[virtualMetadataWithSetterMock.setter],
[virtualMetadataWithGetterSetterMock.setter],
]);
});
});
});
================================================
FILE: tests/jest-e2e.json
================================================
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
================================================
FILE: tests/src/app.module.ts
================================================
import { Module } from '@nestjs/common';
import { MongooseModule } from '../../lib';
import { CatsModule } from './cats/cats.module';
import { CatModule } from './cats/cat.module';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/test'),
CatsModule,
CatModule,
],
})
export class AppModule {}
================================================
FILE: tests/src/cats/cat.controller.ts
================================================
import { Controller, Get, Param } from '@nestjs/common';
import { CatService } from './cat.service';
import { Cat } from './schemas/cat.schema';
@Controller('cat')
export class CatController {
constructor(private readonly catService: CatService) {}
@Get(':id')
async findOne(@Param('id') id: string): Promise {
return this.catService.findOne(id);
}
}
================================================
FILE: tests/src/cats/cat.module.ts
================================================
import { Module } from '@nestjs/common';
import { MongooseModule } from '../../../lib';
import { Cat, CatSchema } from './schemas/cat.schema';
import { CatController } from './cat.controller';
import { CatService } from './cat.service';
@Module({
imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
controllers: [CatController],
providers: [CatService],
})
export class CatModule {}
================================================
FILE: tests/src/cats/cat.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '../../../lib';
import { Cat } from './schemas/cat.schema';
@Injectable()
export class CatService {
constructor(@InjectModel(Cat.name) private readonly catModel: Model) {}
async findOne(id: string): Promise {
return this.catModel.findById(id).populate('kitten').exec();
}
}
================================================
FILE: tests/src/cats/cats.controller.ts
================================================
import { Body, Controller, Get, Post } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './schemas/cat.schema';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise {
return this.catsService.findAll();
}
}
================================================
FILE: tests/src/cats/cats.module.ts
================================================
import { Module } from '@nestjs/common';
import { MongooseModule } from '../../../lib';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat, CatSchema } from './schemas/cat.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
================================================
FILE: tests/src/cats/cats.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { Model, Types } from 'mongoose';
import { InjectModel } from '../../../lib';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './schemas/cat.schema';
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name) private readonly catModel: Model) {}
async create(createCatDto: CreateCatDto): Promise {
const createdCat = new this.catModel({
...createCatDto,
kitten: createCatDto.kitten?.map((kitten) => new Types.ObjectId(kitten)),
});
return createdCat.save();
}
async findAll(): Promise {
return this.catModel.find().exec();
}
}
================================================
FILE: tests/src/cats/dto/create-cat.dto.ts
================================================
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
readonly kitten?: string[];
}
================================================
FILE: tests/src/cats/schemas/cat.schema.ts
================================================
import { Document, Types } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '../../../../lib';
@Schema()
export class Cat extends Document {
@Prop()
name: string;
@Prop()
age: number;
@Prop()
breed: string;
// see https://github.com/nestjs/mongoose/issues/2421
@Prop({
type: [{ type: Types.ObjectId, ref: Cat.name }],
default: [],
})
kitten: Types.ObjectId[];
}
export const CatSchema = SchemaFactory.createForClass(Cat);
================================================
FILE: tests/src/event/dto/create-click-link-event.dto.ts
================================================
export class CreateClickLinkEventDto {
kind: string;
time: Date;
url: string;
}
================================================
FILE: tests/src/event/dto/create-sign-up-event.dto.ts
================================================
export class CreateSignUpEventDto {
kind: string;
time: Date;
user: string;
}
================================================
FILE: tests/src/event/event.controller.ts
================================================
import { Body, Controller, Get, Post } from '@nestjs/common';
import { CreateClickLinkEventDto } from './dto/create-click-link-event.dto';
import { CreateSignUpEventDto } from './dto/create-sign-up-event.dto';
import { EventService } from './event.service';
import { ClickLinkEvent } from './schemas/click-link-event.schema';
import { Event } from './schemas/event.schema';
import { SignUpEvent } from './schemas/sign-up-event.schema';
@Controller('event')
export class EventController {
constructor(private readonly eventService: EventService) {}
@Post('click-link')
async createClickLinkEvent(
@Body() dto: CreateClickLinkEventDto,
): Promise {
return this.eventService.create({
...dto,
time: new Date(),
kind: ClickLinkEvent.name,
});
}
@Post('sign-up')
async create(@Body() dto: CreateSignUpEventDto): Promise {
return this.eventService.create({
...dto,
time: new Date(),
kind: SignUpEvent.name,
});
}
@Get()
async findAll(): Promise {
return this.eventService.findAll();
}
}
================================================
FILE: tests/src/event/event.module.ts
================================================
import { DynamicModule, Module } from '@nestjs/common';
import { EventService } from './event.service';
import { EventController } from './event.controller';
@Module({})
export class EventModule {
static forFeature(module: DynamicModule): DynamicModule {
return {
imports: [module],
module: EventModule,
controllers: [EventController],
providers: [EventService],
};
}
}
================================================
FILE: tests/src/event/event.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { Document, Model } from 'mongoose';
import { InjectModel } from '../../../lib';
import { CreateClickLinkEventDto } from './dto/create-click-link-event.dto';
import { CreateSignUpEventDto } from './dto/create-sign-up-event.dto';
import { ClickLinkEvent } from './schemas/click-link-event.schema';
import { Event } from './schemas/event.schema';
import { SignUpEvent } from './schemas/sign-up-event.schema';
@Injectable()
export class EventService {
constructor(
@InjectModel(Event.name)
private readonly eventModel: Model,
@InjectModel(ClickLinkEvent.name)
private readonly clickEventModel: Model,
@InjectModel(SignUpEvent.name)
private readonly signUpEventModel: Model,
) {}
async create(
createDto: CreateClickLinkEventDto | CreateSignUpEventDto,
): Promise {
const createdEvent = new this.eventModel(createDto);
return createdEvent.save();
}
async findAll(): Promise {
return this.eventModel.find().exec();
}
}
================================================
FILE: tests/src/event/schemas/click-link-event.schema.ts
================================================
import { Prop, Schema, SchemaFactory } from '../../../../lib';
import { Event } from './event.schema';
@Schema({})
export class ClickLinkEvent implements Event {
kind: string;
time: Date;
@Prop({ type: String, required: true })
url: string;
}
export const ClieckLinkEventSchema = SchemaFactory.createForClass(
ClickLinkEvent,
);
================================================
FILE: tests/src/event/schemas/event.schema.ts
================================================
import { Prop, Schema, SchemaFactory } from '../../../../lib';
import { ClickLinkEvent } from './click-link-event.schema';
import { SignUpEvent } from './sign-up-event.schema';
@Schema({ discriminatorKey: 'kind' })
export class Event {
@Prop({
type: String,
required: true,
enum: [ClickLinkEvent.name, SignUpEvent.name],
})
kind: string;
@Prop({ type: Date, required: true })
time: Date;
}
export const EventSchema = SchemaFactory.createForClass(Event);
================================================
FILE: tests/src/event/schemas/sign-up-event.schema.ts
================================================
import { Prop, Schema, SchemaFactory } from '../../../../lib';
import { Event } from './event.schema';
@Schema({})
export class SignUpEvent implements Event {
kind: string;
time: Date;
@Prop({ type: String, required: true })
user: string;
}
export const SignUpEventSchema = SchemaFactory.createForClass(SignUpEvent);
================================================
FILE: tests/src/lazy-app.module.ts
================================================
import { Module } from '@nestjs/common';
import { MongooseModule } from '../../lib';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/test', {
lazyConnection: true,
}),
CatsModule,
],
})
export class LazyAppModule {}
================================================
FILE: tests/src/main.ts
================================================
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
================================================
FILE: tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./lib"
},
"include": ["lib/**/*"],
"exclude": ["node_modules", "tests", "dist", "**/*spec.ts"]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"sourceMap": true,
"skipLibCheck": true,
"strict": true,
"strictPropertyInitialization": false
}
}