Created
September 8, 2019 14:02
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)] | |
})) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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