I have this code:
import { Module, Injectable } from '#nestjs/common'
#Injectable()
export class PrismaService {
constructor() {
console.log('init original')
}
}
#Injectable()
export class PrismaServiceMock {
constructor() {
console.log('init mock')
}
}
#Module({
providers: [
{
provide: PrismaService,
useClass: PrismaService,
},
],
exports: [PrismaService],
})
export class PrismaModule {}
#Module({
imports: [PrismaModule],
providers: [
{
provide: PrismaService,
useClass: PrismaServiceMock,
},
],
})
export class AppModule {}
Because of
providers: [
{
provide: PrismaService,
useClass: PrismaServiceMock,
},
],
I expect PrismaServiceMock to be instantiated. However, both PrismaService are still created. What do I miss?
you can do something like this:
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PrismaService,
useValue: {
onModuleInit: jest.fn()
},
},
]
});
or mock your database driver:
jest.mock('mysql')
or:
jest.spyOn(mysql,'connect').mockImplemetation()
or in the case of MongoDB, do something like:
const module: TestingModule = await Test.createTestingModule({
providers: [
{ provide: 'DatabaseConnection', useValue: {} }
]
});
Related
There are plenty of articles showing how to inject providers into dynamic module but all of them only show examples using their exported services, none with middleware. Using the exact same method as used with services for middleware fails. How can I create reusable middleware that requires providers to be injected?
Example middleware, requiring injection of a UserService provider
#Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(#Inject(USER_SERVICE) private readonly userService: UserService) {
console.log('userService findAll: ', userService.findAll());
}
use(req: any, res: any, next: () => void) {
console.log('AuthMiddleware called!');
next();
}
}
Example module containing the middleware:
#Module({
providers: [AuthService, AuthMiddleware],
imports: [ExtModule],
})
export class AuthModule extends createConfigurableDynamicRootModule<
AuthModule,
AuthModuleOptions
>(AUTH_OPTIONS, {
providers: [
{
provide: AUTH_SECRET,
inject: [AUTH_OPTIONS],
useFactory: (options: AuthModuleOptions) => options.secret,
},
{
provide: USER_SERVICE,
inject: [AUTH_OPTIONS],
useFactory: (options: AuthModuleOptions) => options.userService,
},
],
controllers: [AuthController],
}) {}
Now importing the module and trying to use the middleware:
#Module({
imports: [
ConfigModule.forRoot(),
AuthModule.forRootAsync(AuthModule, {
imports: [ConfigModule, UserModule],
inject: [ConfigService, UserService],
useFactory: (config: ConfigService, userService: UserService) => {
return {
secret: config.get('AUTH_SECRET_VALUE'),
userService,
};
},
}) as DynamicModule,
UserModule,
],
controllers: [UserController],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthMiddleware).forRoutes('*');
}
}
Now when initializing the AuthModule dependencies the middleware class is clearly instantiated correctly, the userService.findAll being logged:
userService findAll: []
So far so good, and this works fine for services.
But the problem is that when then actually using the middleware in configure(), the injected context is not used
...\app\node_modules\#nestjs\core\injector\injector.js:193
throw new unknown_dependencies_exception_1.UnknownDependenciesException(wrapper.name, dependencyContext, moduleRef);
^
Error: Nest can't resolve dependencies of the class AuthMiddleware {
constructor(userService) {
this.userService = userService;
console.log('userService findAll: ', userService.findAll());
}
use(req, res, next) {
console.log('AuthMiddleware called!');
next();
}
} (?). Please make sure that the argument Symbol(USER_SERVICE) at index [0] is available in the AppModule context.
I've ended up getting it to work, mostly by re-exporting the injected providers. After trying different combinations, the working one is as follows
Static dependencies (e.g. external libraries) need to be imported and re-exported inside the module decorator
Dynamic dependencies need to be imported and re-exported inside createConfigurableDynamicRootModule
Exporting the Middleware class seems to have no effect in any way
#Module({
providers: [],
imports: [ExtModule],
exports: [ExtModule],
})
export class AuthModule extends createConfigurableDynamicRootModule<
AuthModule,
AuthModuleOptions
>(AUTH_OPTIONS, {
providers: [
{
provide: AUTH_SECRET,
inject: [AUTH_OPTIONS],
useFactory: (options: AuthModuleOptions) => options.secret,
},
{
provide: USER_SERVICE,
inject: [AUTH_OPTIONS],
useFactory: (options: AuthModuleOptions) => options.userService,
},
],
exports: [
{
provide: AUTH_SECRET,
inject: [AUTH_OPTIONS],
useFactory: (options: AuthModuleOptions) => options.secret,
},
{
provide: USER_SERVICE,
inject: [AUTH_OPTIONS],
useFactory: (options: AuthModuleOptions) => options.userService,
},
],
}) {}
#Module({
imports: [],
providers: [SupertokensService, AuthService],
exports: [],
controllers: [AuthController],
})
export class AuthModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthMiddleware).forRoutes('*');
}
static forRoot({
connectionURI,
apiKey,
appInfo,
}: AuthModuleConfig): DynamicModule {
return {
providers: [
{
useValue: {
appInfo,
connectionURI,
apiKey,
},
provide: ConfigInjectionToken,
},
],
exports: [],
imports: [],
module: AuthModule,
};
}
}
The problem with this implementaion I can't use env variables, so I need useFactory to pass ConfigService. Can somebody do that, and give some explanation.
I figure out how to make this work, unfortunately it only works with nest.js version 9 (current latest version). First you need to create a new file. For example auth.module-definition.ts. Now in this file we need to create ConfigurableModuleBuilder.
import { ConfigurableModuleBuilder } from '#nestjs/common';
import { AuthModuleConfig } from './config.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<AuthModuleConfig>()
.setClassMethodName('forRoot')
.build();
we need to set setClassMethodName('forRoot'), when we set forRoot it will create two methods, forRoot and forRootAsync. The next step is to extend our created ConfigurableModuleClass. it should look something like this
import { MiddlewareConsumer, Module } from '#nestjs/common';
import { AuthMiddleware } from './auth.middleware';
import { SupertokensService } from './supertokens/supertokens.service';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { ConfigurableModuleClass } from './auth.module-definition';;
#Module({
imports: [],
providers: [SupertokensService, AuthService],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule extends ConfigurableModuleClass {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthMiddleware).forRoutes('*');
}
}
And that's actually it, so from now on we have forRootAsync and we get reigister it in app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '#nestjs/common';
import { AuthModule } from './auth/auth.module';
import { ConfigModule, ConfigType } from '#nestjs/config';
import authConfig from './auth/auth.config';
#Module({
imports: [
AuthModule.forRootAsync({
inject: [authConfig.KEY],
imports: [ConfigModule.forFeature(authConfig)],
useFactory: (config: ConfigType<typeof authConfig>) => {
return {
connectionURI: config.CONNECTION_URI,
appInfo: {
appName: config.appInfo.APP_NAME,
apiDomain: config.appInfo.API_DOMAIN,
websiteDomain: config.appInfo.WEBSITE_DOMAIN,
apiBasePath: config.appInfo.API_BASE_PATH,
websiteBasePath: config.appInfo.WEBSITE_BASE_PATH,
},
};
},
})
],
controllers: [],
providers: [
],
})
export class AppModule implements NestModule {
}
here I am using Nest.js Config, but you don't need to, so use it how you want.
I know that my english is not the best, so if you still do not understand you can check these sources https://docs.nestjs.com/fundamentals/dynamic-modules#configurable-module-builder
https://trilon.io/blog/nestjs-9-is-now-available#Configurable-module-builder
I created my Dynamic configModule to extract environment variables from a different path. It extracts from an yml file. Everything works properly if a add in some module. Here is my ConfigModule:
import { DynamicModule } from '#nestjs/common';
import { ConfigModule } from '#nestjs/config';
import { EnvConfigService } from './env.config.service';
export class EnvConfigModule {
/**
* Create an static function to call directly from the class without instantiation
* #param options: Our config module attributes or properties
* #returns DynamicModule
*/
static register(options): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
EnvConfigService,
],
exports: [EnvConfigService],
};
}
}
Now when I want to add that configuration in the new custom JwtModule, CustomJwtModule:
...
import { EnvConfigModule } from 'src/utils/environment/env.config.module';
import { EnvConfigService } from 'src/utils/environment/env.config.service';
#Module({
imports: [
JwtModule.registerAsync({
imports: [EnvConfigModule],
inject: [EnvConfigService],
useFactory: (configService: EnvConfigService) => {
const base64_pubKey = configService.get(ACCESS_PUBLIC_KEY_NAME);
return {
publicKey: Buffer.from(base64_pubKey, KEY_ENCODING),
signOptions: {
algorithm: ENCRYPTION_ALGORITHM,
},
};
},
}),
],
providers: [
{
provide: JWT_ACCESS_SERVICE,
useExisting: JwtService,
},
],
exports: [JWT_ACCESS_SERVICE],
})
export class JWTAccessModule {}
And here is my error:
Error: Nest can't resolve dependencies of the JWT_MODULE_OPTIONS (?). Please make sure that the argument EnvConfigService at index [0] is available in the JwtModule context.
I guess, I do get that error because it does not inject properly in JWT module my service.
My app.module.ts is implemented in that way
...
import { EnvConfigModule } from './utils/environment/env.config.module';
#Module({
imports: [
IamModule,
EnvConfigModule.register({
folder: 'config/environment/',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Ok, it seems I was importing wrongly. It was missing some part of the configuration. Here is the working example of CustomJwtModule:
...
import { EnvConfigModule } from 'src/utils/environment/env.config.module';
import { EnvConfigService } from 'src/utils/environment/env.config.service';
#Module({
imports: [
JwtModule.registerAsync({
imports: [EnvConfigModule.register({
folder: 'config/environment/',
}),
],
inject: [EnvConfigService],
...
Is there a way to connect multiple MongoDB connections per module?
app.module.ts
#Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/masterDB'),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
Similarly, can we define another connection in another module which is a child of app.module?
child.module.ts
#Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/childDB'),
MongooseModule.forFeature([{ name: 'child', schema: ChildSchema }]),
],
controllers: [ChildController],
providers: [ChildService],
})
export class ChildModule { }
Or any other way to access different databases at once.
Thanks in advance!
[SOLVED March 2021]
Here you'll find the solution:
https://www.learmoreseekmore.com/2020/04/nestjs-multiple-mongodb-databases.html
import { Module } from '#nestjs/common';
import { MongooseModule } from '#nestjs/mongoose';
import { studentSchema } from './schemas/myworld/student.schema';
import { animalSchema } from './schemas/wildlife/animal.schema';
#Module({
imports: [
MongooseModule.forFeature([
{
name: 'Student',
schema: studentSchema,
collection: 'Student',
},
],'myWorldDb'),
MongooseModule.forFeature([
{
name: 'Animals',
schema: animalSchema,
collection: 'Animals'
}
],'wildLifeDb'),
MongooseModule.forRoot(
'mongodb+srv://<userName>:<password>#cluster0-igk.mongodb.net/MyWorld?retryWrites=true&w=majority',
{
connectionName: 'myWorldDb'
}
),
MongooseModule.forRoot(
'mongodb+srv://<username>:<password>#cluster0-igk.mongodb.net/WildLife?retryWrites=true&w=majority',
{
connectionName: 'wildLifeDb'
}
)
],
controllers: [],
providers: [],
})
export class AppModule {}
You have to do it manually you have to use a providers file:
mongoose.providers.ts
import * as mongoose from 'mongoose';
export const mongooseProviders = [
{
provide: 'MASTER_CONNECTION',
useFactory: (): Promise<typeof mongoose> =>
// This mongoose.connect never working for multples DB connection
// mongoose.connect('mongodb://localhost/masterDB'),
// Following is working fine and tested by me
mongoose.createConnection('mongodb://localhost/masterDB'),
},
{
provide: 'CHILD_CONNECTION',
useFactory: (): Promise<typeof mongoose> =>
// This mongoose.connect never working for multples DB connection
// mongoose.connect('mongodb://localhost/masterDB'),
// Following is working fine and tested by me
mongoose.createConnection('mongodb://localhost/ChildDB'),
},
];
mongoose.module.ts
import { Module } from '#nestjs/common';
import { mongooseProviders } from './mongoose.providers';
#Module({
providers: [...mongooseProviders],
exports: [...mongooseProviders],
})
export class MongooseModule {}
model.providers.ts
import { Connection } from 'mongoose';
import { ChildSchema } from './schemas/child/child.schema';
import { MasterSchema } from './schemas/master/master.schema';
export const modelProviders = [
{
provide: 'CHILD_MODEL',
useFactory: (connection: Connection) => connection.model('Child', ChildSchema),
inject: ['CHILD_CONNECTION'],
},
{
provide: 'MASTER_MODEL',
useFactory: (connection: Connection) => connection.model('Master', MasterSchema),
inject: ['MASTER_CONNECTION'],
},
];
And on the constructor instead of using #InjectModel you use #Inject:
#Injectable
export Class ModelService {
constructor(#Inject('MASTER_MODEL') private masterModel: Model<Master>) {}
...
Note: in the module you provide the service you should import the MongooseModule and put as provider modelProviders.
Post above from RalphJS really helped, but it only was using one connection for everything. Had to change 1 thing in model.providers.ts:
instead of mongoose.connect
you have to use mongoose.createConnection
model.providers.ts
import * as mongoose from 'mongoose';
export const mongooseProviders = [
{
provide: 'MASTER_CONNECTION',
useFactory: async (): Promise<unknown> =>
await mongoose.createConnection('mongodb://localhost/masterDB'),
},
{
provide: 'CHILD_CONNECTION',
useFactory: async (): Promise<unknown> =>
await mongoose.createConnection('mongodb://localhost/childDB'),
},
];
https://mongoosejs.com/docs/connections.html#multiple_connections
I'm having a problem with overriding a provider in a test context. I know I'm missing something blindingly obvious, but I've tried loads of permutations and none have worked, so I'm putting this out to the hive mind.
I have the following structure:
shared.module.ts
#Module({
providers: [
DatabaseProvider,
ConfigService,
CacheService
],
exports: [
DatabaseProvider,
ConfigService
CacheService
],
imports: [HttpModule],
})
where DatabaseProvider is defined as:
const DatabaseProvider = {
provide: "MongoConnection",
inject: [ConfigService],
useFactory: async (config: ConfigService): Promise < typeof mongoose | Object > => {
return await mongoose.connect(config.getString("MONGO_URI"), { useNewUrlParser: true });
}
};
my.module.ts
#Module({
imports: [SharedModule, HttpModule],
exports: [MyService],
controllers: [MyController],
providers: [MyService],
})
app.module.ts
#Module({
imports: [
SharedModule,
MyModule
],
providers: [
...
],
controllers: [
...
]
})
my.service.ts
export class MyService {
constructor(
#Inject("MongoConnection") private readonly dbConnection: Connection,
private _cache: CacheService
) { }
...
}
my.spec.ts
let cacheService = {...};
this.db = {...};
const module = await Test.createTestingModule({
imports: [SharedModule, MyModule, HttpModule]
})
.overrideProvider(CacheService)
.useValue(cacheService)
.overrideProvider("MongoConnection")
.useValue(this.db)
.compile();
My issue is that the CacheService appears to be being overridden (it's using redis-mock defined in cacheService, instead of the default redis implementation in CacheService), but the MongoConnection is not - it is still calling the original DatabaseProvider from shared.module.ts.
Can someone point out what I'm missing?