Skip to content

Instantly share code, notes, and snippets.

@AliYusuf95
Created September 8, 2019 14:02
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save AliYusuf95/e7faa722d82426008e90b44206a50000 to your computer and use it in GitHub Desktop.
Save AliYusuf95/e7faa722d82426008e90b44206a50000 to your computer and use it in GitHub Desktop.
MongoDB driver module for NestJS with dynamic mongo connection creation per request and handling open connections
import { ModuleMetadata, Type } from '@nestjs/common/interfaces'
/**
* Options that ultimately need to be provided to create a MongoDB connection
*/
export interface MongoModuleOptions {
connectionName?: string
uri: string
dbName: string
clientOptions?: any
}
export interface MongoOptionsFactory {
createMongoOptions(): Promise<MongoModuleOptions> | MongoModuleOptions
}
/**
* Options available when creating the module asynchrously. You should use only one of the
* useExisting, useClass, or useFactory options for creation.
*/
export interface MongoModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
/** A unique name for the container. If not specified, a default one will be used. */
containerName?: string
/** Reuse an injectable factory class created in another module. */
useExisting?: Type<MongoOptionsFactory>
/**
* Use an injectable factory class to populate the module options, such as URI and database name.
*/
useClass?: Type<MongoOptionsFactory>
/**
* A factory function that will populate the module options, such as URI and database name.
*/
useFactory?: (...args: any[]) => Promise<MongoModuleOptions> | MongoModuleOptions
/**
* Inject any dependencies required by the Mongo module, such as a configuration service
* that supplies the URI and database name
*/
inject?: any[]
}
@Injectable()
export class AppService {
constructor(
@InjectDb() private readonly db: Db,
) {}
async getHello(): Promise<string> {
const count = await this.db.collection("User").estimatedDocumentCount();
return 'users count:' + count;
}
}
@Injectable()
export class AppService2 {
constructor(
@InjectDb("Connection2") private readonly db2: Db,
) {}
async getHello(): Promise<string> {
const count = await this.db2.collection("User").estimatedDocumentCount();
return 'users count:' + count;
}
}
@Controller('api')
export class AppController {
constructor(
private readonly service: AppService
) {}
@Get()
async test() {
return this.service.getHello();
}
}
@Controller('api2')
export class AppController2 {
constructor(
private readonly service2: AppService2,
) {}
@Get()
async test() {
return this.service2.getHello();
}
}
@Injectable({ scope: Scope.REQUEST })
export class MongoConfigService implements MongoOptionsFactory {
constructor(@Inject(REQUEST) private readonly request: Request) {}
async createMongoOptions(): Promise<MongoModuleOptions> {
const uri = this.request.query.uri ||
'mongodb://localhost/test';
const db = this.request.query.db || 'test';
return {
uri: uri,
dbName: db,
clientOptions: {
useNewUrlParser: true,
useUnifiedTopology: true
}
};
}
}
@Module({
imports: [],
providers: [MongoConfigService],
exports: [MongoConfigService],
})
export class DBModule {}
@Module({
imports: [
MongoModule.forRootAsync({
imports: [DBModule],
useExisting: MongoConfigService
}),
MongoModule.forRootAsync({
containerName: "Connection2",
imports: [DBModule],
useExisting: MongoConfigService
}),
],
controllers: [AppController, AppController2],
providers: [AppService, AppService2],
})
export class AppModule {}
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Starts listening to shutdown hooks
app.enableShutdownHooks();
await app.listen(4040);
}
bootstrap();
import {
Module,
Inject,
Global,
DynamicModule,
Provider,
OnModuleDestroy,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { MongoClient, MongoClientOptions } from 'mongodb';
import {
DEFAULT_MONGO_CLIENT_OPTIONS,
MONGO_MODULE_OPTIONS,
DEFAULT_MONGO_CONTAINER_NAME,
MONGO_CONTAINER_NAME,
} from './mongo.constants';
import {
MongoModuleAsyncOptions,
MongoOptionsFactory,
MongoModuleOptions,
} from './interfaces';
import { getClientToken, getContainerToken, getDbToken } from './mongo.util';
import * as hash from 'object-hash';
@Global()
@Module({})
export class MongoCoreModule implements OnModuleDestroy {
constructor(
@Inject(MONGO_CONTAINER_NAME) private readonly containerName: string,
private readonly moduleRef: ModuleRef,
) {}
static forRoot(
uri: string,
dbName: string,
clientOptions: MongoClientOptions = DEFAULT_MONGO_CLIENT_OPTIONS,
containerName: string = DEFAULT_MONGO_CONTAINER_NAME,
): DynamicModule {
const containerNameProvider = {
provide: MONGO_CONTAINER_NAME,
useValue: containerName,
};
const connectionContainerProvider = {
provide: getContainerToken(containerName),
useFactory: () => new Map<any, MongoClient>(),
};
const clientProvider = {
provide: getClientToken(containerName),
useFactory: async (connections: Map<any, MongoClient>) => {
const key = hash.sha1({
uri: uri,
clientOptions: clientOptions,
});
if (connections.has(key)) {
return connections.get(key);
}
const client = new MongoClient(uri, clientOptions);
connections.set(key, client);
return await client.connect();
},
inject: [getContainerToken(containerName)],
};
const dbProvider = {
provide: getDbToken(containerName),
useFactory: (client: MongoClient) => client.db(dbName),
inject: [getClientToken(containerName)],
};
return {
module: MongoCoreModule,
providers: [
containerNameProvider,
connectionContainerProvider,
clientProvider,
dbProvider,
],
exports: [clientProvider, dbProvider],
};
}
static forRootAsync(options: MongoModuleAsyncOptions): DynamicModule {
const mongoContainerName =
options.containerName || DEFAULT_MONGO_CONTAINER_NAME;
const containerNameProvider = {
provide: MONGO_CONTAINER_NAME,
useValue: mongoContainerName,
};
const connectionContainerProvider = {
provide: getContainerToken(mongoContainerName),
useFactory: () => new Map<any, MongoClient>(),
};
const clientProvider = {
provide: getClientToken(mongoContainerName),
useFactory: async (
connections: Map<any, MongoClient>,
mongoModuleOptions: MongoModuleOptions,
) => {
const { uri, clientOptions } = mongoModuleOptions;
const key = hash.sha1({
uri: uri,
clientOptions: clientOptions,
});
if (connections.has(key)) {
return connections.get(key);
}
const client = new MongoClient(
uri,
clientOptions || DEFAULT_MONGO_CLIENT_OPTIONS,
);
connections.set(key, client);
return await client.connect();
},
inject: [getContainerToken(mongoContainerName), MONGO_MODULE_OPTIONS],
};
const dbProvider = {
provide: getDbToken(mongoContainerName),
useFactory: (
mongoModuleOptions: MongoModuleOptions,
client: MongoClient,
) => client.db(mongoModuleOptions.dbName),
inject: [MONGO_MODULE_OPTIONS, getClientToken(mongoContainerName)],
};
const asyncProviders = this.createAsyncProviders(options);
return {
module: MongoCoreModule,
imports: options.imports,
providers: [
...asyncProviders,
clientProvider,
dbProvider,
containerNameProvider,
connectionContainerProvider,
],
exports: [clientProvider, dbProvider],
};
}
async onModuleDestroy() {
const clientsMap: Map<any, MongoClient> = this.moduleRef.get<
Map<any, MongoClient>
>(getContainerToken(this.containerName));
if (clientsMap) {
await Promise.all(
[...clientsMap.values()].map(connection => connection.close()),
);
}
}
private static createAsyncProviders(
options: MongoModuleAsyncOptions,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
} else if (options.useClass) {
return [
this.createAsyncOptionsProvider(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
} else {
return [];
}
}
private static createAsyncOptionsProvider(
options: MongoModuleAsyncOptions,
): Provider {
if (options.useFactory) {
return {
provide: MONGO_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
} else if (options.useExisting) {
return {
provide: MONGO_MODULE_OPTIONS,
useFactory: async (optionsFactory: MongoOptionsFactory) =>
await optionsFactory.createMongoOptions(),
inject: [options.useExisting],
};
} else if (options.useClass) {
return {
provide: MONGO_MODULE_OPTIONS,
useFactory: async (optionsFactory: MongoOptionsFactory) =>
await optionsFactory.createMongoOptions(),
inject: [options.useClass],
};
} else {
throw new Error('Invalid MongoModule options');
}
}
}
import { MongoClientOptions } from 'mongodb';
export const MONGO_CONNECTIONS_CONTAINER = 'MongoConnectionsContainer';
export const MONGO_CONTAINER_NAME = 'MongoContainerName';
export const MONGO_CONNECTION_NAME = 'MongoConnectionName';
export const MONGO_MODULE_OPTIONS = 'MongoModuleOptions';
export const DEFAULT_MONGO_CONTAINER_NAME = 'DefaultMongo';
export const DEFAULT_MONGO_CONNECTION_NAME = 'DefaultMongo';
export const DEFAULT_MONGO_CLIENT_OPTIONS: MongoClientOptions = {
useNewUrlParser: true,
useUnifiedTopology: true
};
import { Inject } from '@nestjs/common'
import { getClientToken, getDbToken, getCollectionToken } from './mongo.util'
/**
* Inject the MongoClient object associated with a connection
* @param connectionName The unique name associated with the connection
*/
export const InjectClient = (connectionName?: string) => Inject(getClientToken(connectionName));
/**
* Inject the Mongo Db object associated with a connection
* @param connectionName The unique name associated with the connection
*/
export const InjectDb = (connectionName?: string) => Inject(getDbToken(connectionName));
/**
* Inject the Mongo Collection object associated with a Db
* @param collectionName The unique name associated with the collection
*/
export const InjectCollection = (collectionName: string) =>
Inject(getCollectionToken(collectionName));
import { Module, DynamicModule } from '@nestjs/common';
import { createMongoProviders } from './mongo.providers';
import { MongoCoreModule } from './mongo-core.module';
import { MongoClientOptions } from 'mongodb';
import { MongoModuleAsyncOptions } from './interfaces';
/**
* Module for the MongoDB driver
*/
@Module({})
export class MongoModule {
/**
* Inject the MongoDB driver synchronously.
* @param uri The database URI
* @param dbName The database name
* @param options Options for the MongoClient that will be created
* @param connectionName A unique name for the connection. If not specified, a default name
* will be used.
*/
static forRoot(
uri: string,
dbName: string,
options?: MongoClientOptions,
connectionName?: string,
): DynamicModule {
return {
module: MongoModule,
imports: [
MongoCoreModule.forRoot(uri, dbName, options, connectionName),
],
};
}
/**
* Inject the MongoDB driver asynchronously, allowing any dependencies such as a configuration
* service to be injected first.
* @param options Options for asynchrous injection
*/
static forRootAsync(options: MongoModuleAsyncOptions): DynamicModule {
return {
module: MongoModule,
imports: [MongoCoreModule.forRootAsync(options)],
};
}
/**
* Inject collections.
* @param collections An array of the names of the collections to be injected.
* @param connectionName A unique name for the connection. If not specified, a default name
* will be used.
*/
static forFeature(
collections: string[] = [],
connectionName?: string,
): DynamicModule {
const providers = createMongoProviders(connectionName, collections);
return {
module: MongoModule,
providers: providers,
exports: providers,
};
}
}
import { Db } from 'mongodb'
import { getCollectionToken, getDbToken } from './mongo.util'
export function createMongoProviders(connectionName?: string, collections: string[] = []) {
return (collections || []).map(collectionName => ({
provide: getCollectionToken(collectionName),
useFactory: (db: Db) => db.collection(collectionName),
inject: [getDbToken(connectionName)]
}))
}
import { DEFAULT_MONGO_CONTAINER_NAME } from './mongo.constants';
/**
* Get a token for the Map object for the given container name
* @param containerName The unique name for the container
*/
export function getContainerToken(containerName: string = DEFAULT_MONGO_CONTAINER_NAME) {
return `${containerName}Container`
}
/**
* Get a token for the MongoClient object for the given connection name
* @param containerName The unique name for the container
*/
export function getClientToken(containerName: string = DEFAULT_MONGO_CONTAINER_NAME) {
return `${containerName}Client`
}
/**
* Get a token for the Mongo Db object for the given connection name
* @param containerName The unique name for the container
*/
export function getDbToken(containerName: string = DEFAULT_MONGO_CONTAINER_NAME) {
return `${containerName}Db`
}
/**
* Get a token for the Mongo Db object for the given connection name
* @param containerName The unique name for the container
*/
export function getCollectionToken(containerName: string) {
return `${containerName}Collection`
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment