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?
Related
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: {} }
]
});
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,
},
],
}) {}
When trying to import another module inside of my dynamic modules nest throws the following error when using a service exported by that module inside of my services:
Error: Nest can't resolve dependencies of the ActivationService (UsersService, ?, USERS_MODULE_OPTIONS, MailService). Please make sure that the argument JwtService at index [1] is available in the UsersModule context.
When importing the Module with static configuration inside the #Module decorator everything works fine.
/** UsersModule */
#Global()
#Module({
imports: [
MikroOrmModule.forFeature([UserEntity]),
JwtModule.register({
secret: 'secret',
}),
],
providers: [UsersService, ActivationService, PasswordResetService],
exports: [UsersService],
controllers: [
MyUserController,
UsersController,
ActivationController,
PasswordResetController,
],
})
export class UsersModule {
/**
* Creates UsersModule asynchronously
* #param options
*/
public static forRootAsync(
options: AsyncModuleOptions<UsersModuleOptions>,
): DynamicModule {
return {
module: UsersModule,
imports: [...(options.imports || [])],
providers: createAsyncModuleOptionsProviders(
options,
USERS_MODULE_OPTIONS,
),
exports: [USERS_MODULE_OPTIONS],
};
}
}
By importing the JwtModule inside of the forRootAsync method of my dynamic module im getting the error above.
/** UsersModule */
#Global()
#Module({
imports: [MikroOrmModule.forFeature([UserEntity])],
providers: [UsersService, ActivationService, PasswordResetService],
exports: [UsersService],
controllers: [
MyUserController,
UsersController,
ActivationController,
PasswordResetController,
],
})
export class UsersModule {
/**
* Creates UsersModule asynchronously
* #param options
*/
public static forRootAsync(
options: AsyncModuleOptions<UsersModuleOptions>,
): DynamicModule {
return {
module: UsersModule,
imports: [
JwtModule.registerAsync({
inject: [USERS_MODULE_OPTIONS],
useFactory: async (options: UsersModuleOptions) => ({
secret: options.secret,
privateKey: options.privateKey,
publicKey: options.publicKey,
}),
}),
...(options.imports || []),
],
providers: createAsyncModuleOptionsProviders(
options,
USERS_MODULE_OPTIONS,
),
exports: [USERS_MODULE_OPTIONS],
};
}
}
I have already used this kind of configuration successfully in another module without obtaining the error:
/** JwtAuthenticationModule */
#Global()
#Module({
imports: [PassportModule],
providers: [
JwtAuthenticationService,
JwtStrategy,
{
provide: APP_GUARD,
useClass: JwtAuthenticationGuard,
},
],
exports: [JwtAuthenticationService],
controllers: [JwtAuthenticationController],
})
export class JwtAuthenticationModule {
/**
* Creates JwtAuthenticationModule asynchronously
* #param options
*/
public static forRootAsync(
options: AsyncModuleOptions<JwtAuthenticationModuleOptions>,
): DynamicModule {
return {
module: JwtAuthenticationModule,
imports: [
JwtModule.registerAsync({
inject: [JWT_AUTHENTICATION_MODULE_OPTIONS],
useFactory: async (options: JwtAuthenticationModuleOptions) => ({
secret: options.secret,
privateKey: options.privateKey,
publicKey: options.publicKey,
}),
}),
...(options.imports || []),
],
providers: [
this.getUsersServiceProvider(),
...createAsyncModuleOptionsProviders(
options,
JWT_AUTHENTICATION_MODULE_OPTIONS,
),
],
exports: [JWT_AUTHENTICATION_MODULE_OPTIONS],
};
}
/**
* Fetches UsersServiceProvider from options
* #private
*/
private static getUsersServiceProvider(): Provider<UsersServiceContract> {
return {
inject: [JWT_AUTHENTICATION_MODULE_OPTIONS],
provide: USERS_SERVICE,
useFactory: (options: JwtAuthenticationModuleOptions) =>
options.usersService,
};
}
}
Importing the JwtModule with a static secret inside of the forRootAsync method also throws the same error.
/** UsersModule */
#Global()
#Module({
imports: [MikroOrmModule.forFeature([UserEntity])],
providers: [UsersService, ActivationService, PasswordResetService],
exports: [UsersService],
controllers: [
MyUserController,
UsersController,
ActivationController,
PasswordResetController,
],
})
export class UsersModule {
/**
* Creates UsersModule asynchronously
* #param options
*/
public static forRootAsync(
options: AsyncModuleOptions<UsersModuleOptions>,
): DynamicModule {
return {
module: UsersModule,
imports: [
JwtModule.register({
secret: 'secret',
}),
...(options.imports || []),
],
providers: createAsyncModuleOptionsProviders(
options,
USERS_MODULE_OPTIONS,
),
exports: [USERS_MODULE_OPTIONS],
};
}
}
Try to import the AuthModule on the UsersModule
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