6

Working on a project with Nestjs 6.x, Mongoose, Mongo, etc... Regarding to the Back End, in my use case, I must change the connection of one of my databases depending of some conditions/parameters coming from some requests.

Basically, I have this

mongoose.createConnection('mongodb://127.0.0.1/whatever-a', { useNewUrlParser: true })

and I want to change to, for example

mongoose.createConnection('mongodb://127.0.0.1/whatever-b', { useNewUrlParser: true })

Therefore, I have in Nestjs the first provider

export const databaseProviders = [
  {
    provide: 'DbConnectionToken',
    useFactory: async (): Promise<typeof mongoose> =>
    await mongoose.createConnection('mongodb://127.0.0.1/whatever', { useNewUrlParser: true })
  }

I was researching for a while and I found out that in release Nestjs 6.x there are provider requests allowing me to modify dynamically Per-request the injection of some providers.

Anyway, I don't know how to achieve my change neither if it is going to be working in case I'd achieve that

Can anyone help or guide me? Many thanks in advance.

ackuser
  • 5,681
  • 5
  • 40
  • 48
  • Why don't you create two connections and use the right one based on "some conditions/parameters coming from some requests"? – Mladen Apr 08 '19 at 12:26
  • I am already doing that in one service with two providers. However, I am working with a kind of system versioning so for the other cases, databases are created almost daily (with a name "whatever....")...Therefore I would need to be able to connect to one of them depending of what version user wants, save of whatever...Thx – ackuser Apr 08 '19 at 12:33
  • I have never worked with Nest.js, so I don't know how you should handle your providers, but it seems logical to me that you have to create a new provider by having a method that takes one parameter which should be a database name (or whatever is that that is different for every connection), returns this object `{ provide: string, useFactory: Function }`. Calling this function by providing variable parameter should give you the right provider. – Mladen Apr 08 '19 at 13:13
  • Yes, exactly but what you are saying is at compilation time and I would need it to change it dynamically at runtime so that's the point :) – ackuser Apr 08 '19 at 13:38

2 Answers2

6

You can do the following using Nest's built-in Mongoose package:

/*************************
* mognoose.service.ts
*************************/
import { Inject, Injectable, Scope } from '@nestjs/common';
import { MongooseOptionsFactory, MongooseModuleOptions } from '@nestjs/mongoose';
import { REQUEST } from '@nestjs/core';
import { Request } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class MongooseConfigService implements MongooseOptionsFactory {
    constructor(
        @Inject(REQUEST) private readonly request: Request,) {
    }

    createMongooseOptions(): MongooseModuleOptions {
        return {
            uri: request.params.uri, // Change this to whatever you want; you have full access to the request object.
        };
    }
}

/*************************
* mongoose.module.ts
*************************/
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { MongooseConfigService } from 'mognoose.service';

@Module({
    imports: [
        MongooseModule.forRootAsync({
            useClass: MongooseConfigService,
        }),
    ]
})
export class DbModule {}

Then, you can attach whatever you want to the request and change the database per request; hence the use of the Scope.REQUEST. You can read more about Injection Scopes on their docs.


Edit: If you run into issues with PassportJS (or any other package) or the request is empty, it seems to be an error that relates to PassportJS (or the other package) not supporting request scopes; you may read more about the issue on GitHub regarding PassportJS.

yaserso
  • 2,638
  • 5
  • 41
  • 73
  • 2
    I used this same approach, instead of request I added username from middleware into mongoose.service but createMongooseOptions() function is executed once only per application startup and by that time I didn't get the username... Is there any way to re-execute this function on-demand. – Ank Dec 05 '19 at 11:43
  • By default, the injection scope is set to SINGLETON, which means the provider is only injected once the project runs. Changing the scope to REQUEST insures it is injected per request, which should allow for the code to run properly. – yaserso Dec 05 '19 at 11:58
  • 1
    That worked! But I am not getting params from this.request.params – Ank Dec 05 '19 at 12:39
  • Why are you using `this`? In the `MongooseConfigService` `createMongooseOptions` this is not used. I'm using `request.params.uri` without this. – yaserso Dec 05 '19 at 13:43
  • Because it's dependency injected in the constructor and will be available in "this" scope, right? Also I don't get Request interface from "@nestjs/common" instead it's coming from "express". There is a decorator @Req in "@nestjs/common" - https://docs.nestjs.com/fundamentals/injection-scopes – Ank Dec 05 '19 at 15:55
  • The bad thing with this implementation is it will create a new DB connection with each request instead of utilizing the one created before for the same DB. – Ali Yusuf Dec 08 '19 at 09:14
  • 2
    How this problem can be solved? How can I use the same connection created before? @AliYusuf – Marluan Espiritusanto Guerrero Dec 12 '19 at 02:06
  • Hi! I am stuck in a similar situation. Is there a way to do exactly what you have done but on a message received from a stream service like RabbitMQ or Kafka? – Andy95 Nov 20 '22 at 00:41
3

I did a simple implementation for nest-mongodb,

The main changes are in mongo-core.module.ts where I store the connections in a map and used them if available instead of creating a new connection every time.

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');
        }
    }
}

Check out the full implementation

Ali Yusuf
  • 337
  • 4
  • 6
  • Hi! I tried your implementation and it's working nice when I am using the forRoot, but has some problems when I am trying to use forRootAsync({useClass}) because the defined decorators are not working. Except of the containerConnection. – Andy95 Nov 21 '22 at 17:19