Nest can't resolve dependencies of the AuthService (AuthRepository, ?) - javascript

I'm trying to implement JWT into my project. I've followed the steps as outline in https://www.npmjs.com/package/#nestjs/jwt#usage
auth.module.ts
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { TypeOrmModule } from '#nestjs/typeorm';
import { JwtModule } from '#nestjs/jwt';
import { AuthRepository } from './auth.repository';
#Module({
imports: [
JwtModule.register({ secret: process.env.JWT_SECRET || 'ABCDE12345' }),
TypeOrmModule.forFeature([AuthRepository]),
],
exports: [TypeOrmModule, AuthService],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
auth.service.ts
import { Injectable, NotFoundException, UnauthorizedException } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { AuthEntity } from './auth.entity';
import { LoginDTO } from './dto/login.dto';
import * as bcrypt from 'bcrypt';
import { JwtService } from '#nestjs/jwt';
import crypto from 'crypto';
import { AuthRepository } from './auth.repository';
// export interface FindWhereData {
// readonly email: string;
// readonly password: string;
// }
export interface LoginResponse {
readonly token: string;
readonly refresh_token: string;
}
#Injectable()
export class AuthService {
constructor(
#InjectRepository(AuthRepository)
private authRepository: AuthRepository,
private jwtService: JwtService
) {}
/**
* Login user service
*
* #param doc
*/
public async login(doc: LoginDTO): Promise<LoginResponse> {
// verify user email
const user = await this.authRepository.findOne({ email: doc.email });
if (!user) {
throw new NotFoundException('Could not find user');
}
// verify password
const passwordsMatch = await this.passwordsAreEqual(doc.password, user.password);
if (!passwordsMatch) {
throw new UnauthorizedException('Incorrect login credentials');
}
// generate JWT
const token = await this.jwtService.signAsync({ id: user.id });
// create the refresh token
const refreshToken = crypto.randomBytes(256).toString('hex');
// store the refresh token
return {
token: token,
refresh_token: refreshToken,
};
}
/**
* Generate a hashed password
*
* #param password
*/
public async hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt();
return await bcrypt.hash(password, salt);
}
/**
* Compare password against an encrypted string
*
* #param password
* #param encryptedPassword
*/
public async passwordsAreEqual(password: string, encryptedPassword: string): Promise<boolean> {
return await bcrypt.compare(password, encryptedPassword);
}
/**
* Find a record by column attribute and value
*
* #param queryObject
*
*/
public async findWhere(queryObject): Promise<AuthEntity> {
const authEntity = await this.authRepository.findOne({ where: queryObject });
if (!authEntity) {
return null;
}
return authEntity;
}
public async findOne(id: string): Promise<AuthEntity> {
return this.authRepository.findOne(id);
}
}
auth.repository.ts
import { EntityRepository, Repository } from "typeorm";
import { AuthEntity } from "./auth.entity";
#EntityRepository(AuthEntity)
export class AuthRepository extends Repository<AuthEntity> {}
app.module.ts
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RouterModule } from 'nest-router';
import { routes } from './routes';
import { ConfigModule } from '#nestjs/config';
import configuration from './config/configuration';
import { TypeOrmModule } from '#nestjs/typeorm';
import { Connection } from 'typeorm';
import { AuthModule } from './auth/auth.module';
#Module({
imports: [
RouterModule.forRoutes(routes),
ConfigModule.forRoot({
load: [configuration],
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.POSTGRES_HOST || 'localhost',
port: 5432,
username: process.env.POSTGRES_USERNAME || 'postgres',
password: process.env.POSTGRES_PASSWORD || 'password',
database: process.env.POSTGRES_DATABASE || 'service-auth',
autoLoadEntities: true,
synchronize: true,
}),
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {
constructor(private readonly connection: Connection) {
console.log('connection status: ' + connection.isConnected);
}
}
auth.service.spec.ts
import { JwtModule, JwtService } from '#nestjs/jwt';
import { Test, TestingModule } from '#nestjs/testing';
import { AuthEntity } from './auth.entity';
import { AuthRepository } from './auth.repository';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let authService: AuthService;
let authRepository: AuthRepository;
const mockAuthRepository = () => ({
login: jest.fn(),
});
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
// JwtModule,
{
provide: getRepositoryToken(AuthRepository),
useFactory: mockAuthRepository,
},
{
provide: JwtService,
useValue: {
signAsync: jest.fn(() => 'token'),
}
}
]
}).compile();
authService = await module.get<AuthService>(AuthService);
authRepository = await module.get<AuthRepository>(AuthRepository);
});
/**
* Test that the service is defined
*/
it('should be defined', () => {
expect(authService).toBeDefined();
});
});
When I run npm run test I get the following error message:
FAIL src/auth/auth.service.spec.ts
● AuthService › should be defined
Nest can't resolve dependencies of the AuthService (AuthRepository, ?). Please make sure that the argument JwtService at index [1] is available in the RootTestModule context.
Potential solutions:
- If JwtService is a provider, is it part of the current RootTestModule?
- If JwtService is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing JwtService */ ]
})
I know the error is probably pretty clear to seasoned Node/Nest developer but I cannot figure out what the RootTestModule is and how to get JwtModule imported.
I believe I have followed the setup correctly but adding this JwtModule to the AuthService is causing the service to be undefined in my unit tests.
Repo
https://github.com/marcuschristiansen/nestjs-auth

You should be adding a custom provider for the JwtService so that you can mock it. A custom provider could look like
{
provide: JwtService,
useValue: {
signAsync: jest.fn(() => 'token'),
}
}
To tell Nest to inject an object that has a signAsync() method that when called returns the string 'token' so that it will always be the same in your tests.
This object goes in the providers array, just like the AuthRepository mock

Related

TypeError: Cannot read properties of undefined (reading 'subscribe') got error when try to run unit test

I have created a test case for my component with all dependency but when I execute the test case it shows error.
My unit test scenario is when form data is submitted formSubmit() method will be called and it send the data authservice there it makes an HTTP request and return some response. After that it navigate to another page.
Can anyone provide a solution for this test case??
Here are my code
login.component.ts
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
// Services
import { ActivatedRoute, Router } from '#angular/router';
import { AuthService } from 'src/app/Service/auth.service';
// Material Component
import { MatSnackBar } from '#angular/material/snack-bar';
import { Title } from '#angular/platform-browser';
import { HttpErrorResponse } from '#angular/common/http';
import { LoginResponse } from '../../Model/auth';
import { duration, Notify } from '../../Model/notify';
import { NotificationComponent } from '../../shared/notification/notification.component';
import { NotifyService } from '../../Service/notify.service';
#Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css'],
})
export class LoginComponent implements OnInit {
constructor(
private documentTitle: Title,
private fb: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private auth: AuthService,
private notify: NotifyService
) {}
passwordHide!: boolean;
returnUrl!: string;
pageTitle!: string;
loginForm!: FormGroup;
res!: LoginResponse;
ngOnInit(): void {
this.passwordHide = true;
this.pageTitle = 'Login | Online Shopping for Men & Women Shoes';
this.documentTitle.setTitle(this.pageTitle);
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '';
this.loginForm = this.fb.group({
username: ['', [Validators.required, Validators.minLength(8)]],
password: ['', [Validators.required, Validators.minLength(6)]],
});
}
/**
* send a request to auth service to make HTTP POST request
* if form is valid
*/
formSubmit() {
if (this.loginForm.valid) {
console.log(this.returnUrl);
this.auth.login(this.loginForm.value).subscribe({
next: (data: LoginResponse) => {
console.log(data);
this.res = data;
localStorage.setItem('access_token', data.accessToken);
this.auth.authStatusToggle(true);
this.router.navigateByUrl(this.returnUrl);
},
error: (error: HttpErrorResponse) => {
console.log(error);
if (error.status == 400) {
this.notify.warning(error.error.message);
} else {
this.notify.warning(error.error.message);
}
},
});
}
}
}
login.component.spec.ts
import { CommonModule } from '#angular/common';
import { ComponentFixture, TestBed, getTestBed } from '#angular/core/testing';
import {
FormBuilder,
FormsModule,
ReactiveFormsModule,
Validators,
} from '#angular/forms';
import { AppModule } from 'src/app/app.module';
import { AuthService } from 'src/app/Service/auth.service';
import { LoginComponent } from './login.component';
import * as Rx from 'rxjs';
import { Title } from '#angular/platform-browser';
import { HttpClientModule } from '#angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController,
} from '#angular/common/http/testing';
import { LoginResponse } from 'src/app/Model/auth';
import { ActivatedRoute, Router } from '#angular/router';
import { RouterTestingModule } from '#angular/router/testing';
import { NotifyService } from 'src/app/Service/notify.service';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let authService = jasmine.createSpyObj('AuthService', ['login']);
let mockRouter = jasmine.createSpyObj('Router', ['navigate']);
let route: ActivatedRoute;
let documentTitle: Title;
let injector: TestBed;
let fb = new FormBuilder();
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
AppModule,
HttpClientModule,
HttpClientTestingModule,
RouterTestingModule,
],
providers: [
{ provide: FormBuilder, useValue: fb },
{ provide: AuthService, useValue: authService },
{ provide: Router, useValue: mockRouter },
{
provide: ActivatedRoute,
useValue: {
snapshot: {
queryParams: {
returnUrl: '',
},
},
},
},
NotifyService,
Title,
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
fixture.detectChanges();
component = fixture.componentInstance;
injector = getTestBed();
route = TestBed.get(ActivatedRoute);
documentTitle = injector.get(Title);
fb = TestBed.inject(FormBuilder);
component.passwordHide = true;
component.pageTitle = 'Login | Online Shopping for Men & Women Shoes';
documentTitle.setTitle(component.pageTitle);
component.returnUrl = route.snapshot.queryParams['returnUrl'] || '';
component.loginForm = fb.group({
username: ['', [Validators.required, Validators.minLength(8)]],
password: ['', [Validators.required, Validators.minLength(6)]],
});
});
it('should form submit successfully', () => {
// console.log(route.snapshot.queryParamMap.get('returnUrl'))
// console.log(component.returnUrl);
component.loginForm.get('username')?.setValue('ramkumar');
component.loginForm.get('password')?.setValue('Ramkumar#45');
expect(component.loginForm.valid).toBeTruthy();
authService.login.and.returnValue(
Rx.of({
accessToken: 'testToken',
username: 'test',
userId: 1,
} as LoginResponse)
);
component.formSubmit();
expect(component.res).toEqual({
accessToken: 'testToken',
username: 'test',
userId: 1,
} as LoginResponse);
expect(mockRouter.navigate).toHaveBeenCalledWith(['']);
});
});
auth.service.ts
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Router } from '#angular/router';
import { Observable, Subject } from 'rxjs';
import { environment } from 'src/environments/environment';
import { Login, LoginResponse, Register } from '../Model/auth';
import { JwtHelperService } from '#auth0/angular-jwt';
import { UserResponse } from '../Model/user';
#Injectable({
providedIn: 'root',
})
export class AuthService {
private AUTH_API: string = environment.AUTH_API;
private USER_API: string = environment.USER_API;
public isUserLoggedIn: boolean = this.getAuthStatus();
private isLoggedIn: Subject<boolean> = new Subject<boolean>();
private token: any = localStorage.getItem('access_token')?.toString() || null;
private jwt: JwtHelperService;
/**
* Dependency Injections
* #param http
* #param route
*/
constructor(private http: HttpClient, private route: Router) {
this.jwt = new JwtHelperService();
this.isLoggedIn.subscribe((value) => {
console.log(value);
this.isUserLoggedIn = value;
});
}
/**
* Send user credentials to Auth API for create JWT token
* #param data
*/
login(data: Login): Observable<LoginResponse> {
return this.http.post<LoginResponse>(`${this.AUTH_API}/CreateToken`, data);
}
/**
* Send HTTP request to register new user
* #param data
*/
register(data: Register): Observable<UserResponse> {
return this.http.post<UserResponse>(`${this.USER_API}`, data);
}
/**
* Remove JWT token from localstorage to logout
*/
logout(): void {
localStorage.removeItem('access_token');
this.authStatusToggle(false);
if (this.getUserToken() == null) this.route.navigate(['login']);
}
/**
* get user JWT token from localstorage
* return token as string or null
*/
getUserToken(): string {
if (localStorage.getItem('access_token') != null) {
this.token = localStorage.getItem('access_token')?.toString() || null;
} else {
this.token = null;
}
return this.token;
}
authStatusToggle(value: boolean) {
this.isLoggedIn.next(value);
}
getAuthStatus(): boolean {
let token = localStorage.getItem('access_token')?.toString();
console.log(token);
if (token == null) {
return false;
}
return true;
}
}

Create Jest unit test for Passport ApiKey Strategy in NestJS

Let say I have a file named api-key.strategy.ts
import { PassportStrategy } from '#nestjs/passport';
import { Injectable } from '#nestjs/common';
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { ConfigService } from '#nestjs/config';
#Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
constructor(private readonly configService: ConfigService) {
super(
{
header: 'x-api-key',
prefix: ''
},
true,
(apiKey: string, done: any) => {
if (apiKey === 'MY_API_KEY') return done(true);
else return done(false);
}
);
}
}
Based on above codes, how to create a Jest unit test?
Thanks

How to test Auth 0 implemented with passport strategy with Nestjs

I have been working on a feature where the goal is to allow a user to login via Auth0. I am using a passport such as passport-auth0 package to implement it. I was able to get working. However, I am not able to test it. I would like to know how I can test auth/login and auth/callback controllers methods.
Moreover, I would like to understand how to mock #UseGuards(AuthGuard('auth0')) and a middleware since I have used them.
Different ways I have tried I got the following error
[Nest] 23402 - 03/30/2020, 5:45:37 PM [ExceptionHandler] Cannot set property 'authParams' of undefined
**TypeError: Cannot set property 'authParams' of undefined**
at Auth0Strategy.Strategy.authenticate (/Users/directory/node_modules/passport-auth0/lib/index.js:82:28)
at attempt (/Users/directory/node_modules/passport/lib/middleware/authenticate.js:366:16)
at authenticate (/Users/directory/node_modules/passport/lib/middleware/authenticate.js:367:7)
at /Users/directory/node_modules/#nestjs/passport/dist/auth.guard.js:84:3
at new Promise (<anonymous>)
at /Users/directory/node_modules/#nestjs/passport/dist/auth.guard.js:76:83
at MixinAuthGuard.<anonymous> (/Users/directory/node_modules/#nestjs/passport/dist/auth.guard.js:48:36)
at Generator.next (<anonymous>)
at /Users/directory/node_modules/#nestjs/passport/dist/auth.guard.js:20:71
at new Promise (<anonymous>)
✨ Done in 38.11s.
// Auth.module.ts
import { Module, NestModule, MiddlewareConsumer } from '#nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { Auth0Strategy } from './auth.strategy';
import { AuthMiddleware } from './ middlewares/auth.middleware'
#Module({
controllers: [AuthController],
providers: [
AuthService,
Auth0Strategy
],
exports: [AuthService],
})
export class AuthModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.forRoutes('auth/callback');
}
}
// auth.strategy.ts
import { Injectable, Query } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { Strategy } from 'passport-auth0';
import { AuthService, Provider } from './auth.service';
import { config } from 'dotenv';
config();
#Injectable()
export class Auth0Strategy extends PassportStrategy(Strategy, 'auth0') {
constructor(
private readonly authService:AuthService
)
{
super(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL: process.env.AUTH0_CALLBACK_URL,
redirectUri: process.env.AUTH0_CALLBACK_URL,
audience: process.env.AUTH0_AUDIENCE,
responseType: 'code',
scope: 'openid profile email',
},
)
}
async validate(request: any, accessToken: string, refreshToken: string, profile, done: Function): Promise<any> {
try {
const jwt: string = await this.authService.validateOAuthLogin(profile, Provider.Auth0);
const user =
{
jwt
}
return done(null, user);
}
catch (err) {
return done(err, false);
}
}
}
//auth.service.ts
import { Injectable, InternalServerErrorException, HttpException } from '#nestjs/common';
import { sign } from 'jsonwebtoken';
export enum Provider {
Auth0 = 'auth0'
}
#Injectable()
export class AuthService {
private readonly JWT_SECRET_KEY = process.env.JWT_SECRET_KEY
async validateOAuthLogin(profile: object, provider: Provider): Promise<string> {
try {
const isProfileExist = Object.entries(profile).length;
if (isProfileExist === 0) {
throw new HttpException('User profile is empty please login again', 400);
}
const payload = {
profile,
provider
}
const jwt: string = sign(payload, this.JWT_SECRET_KEY, { expiresIn: '1h' });
return jwt;
}
catch (err) {
throw new InternalServerErrorException('validateOAuthLogin', err.message);
}
}
}
import { Controller, Get, UseGuards, Res, Req } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Controller('auth')
export class AuthController {
#Get('login')
#UseGuards(AuthGuard('auth0'))
auth0Login() {}
#Get('callback')
#UseGuards(AuthGuard('auth0'))
auth0LoginCallback(#Req() req, #Res() res) {
const jwt: string = req.user.jwt;
if (jwt) {
res.redirect(`${process.env.AUTH0_SUCCESS_REDIRECTION_URL}/${jwt}`);
} else {
res.redirect(process.env.AUTH0_FAILURE_REDIRECTION_URL);
}
}
}
// auth.controller.spec.ts
import { Test, TestingModule } from '#nestjs/testing';
import { Auth0Strategy } from '../auth.strategy';
import { AuthModule } from '../auth.module';
import { auth0SuccessRequest, fekeToken } from './auth.mock';
describe('AuthService', () => {
let strategy: Auth0Strategy;
const { request, profile, accessToken, refreshToken } = auth0SuccessRequest;
let done: Function = jest.fn();
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AuthModule],
providers: [Auth0Strategy]
}).compile();
strategy = module.get<Auth0Strategy>(Auth0Strategy);
});
afterAll(async () => {
jest.clearAllMocks();
});
it('should validate Auth 0 data', async () => {
await strategy.validate(request, accessToken, refreshToken, profile, done)
expect(done).toBeCalledTimes(1);
});
it('should not proceed without a profile function', async () => {
const failuredAuth = await strategy.validate(request, accessToken, refreshToken, {}, done);
expect(failuredAuth).toBeFalsy();
expect(failuredAuth).toBeUndefined();
});
});
// custom-guard.ts
import { ExecutionContext, Injectable, UnauthorizedException } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class CustomGuard extends AuthGuard('auth0') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
use your CustomGuard and test with it
#Controller('auth')
export class AuthController {
#UseGuards(CustomGuard) // use you guard
auth0LoginCallback(#Req() req, #Res() res) {
}
}
Hope it will help you and it is https://docs.nestjs.com/guards documentation
you are right #harut Barseghyan. with the custom guard you shared with me I was able to activate or override guard in the test environment when I needed it.
// auth.controller.ts
import { Controller, Get, UseGuards, Res, Req } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
import { AuthCustomGuard } from '../auth/guards/auth.guard';
#Controller('auth')
export class AuthController {
#Get('login')
// #UseGuards(AuthGuard('auth0')). // I no longer use this guard
#UseGuards(AuthCustomGuard). // I instead use Auth0 custom guards
auth0Login() {}
#Get('callback')
// #UseGuards(AuthGuard('auth0'))
#UseGuards(AuthCustomGuard)
auth0LoginCallback(#Req() req, #Res() res) {
const jwt: string = req.user.jwt;
if (jwt) {
res.redirect(`${process.env.AUTH0_SUCCESS_REDIRECTION_URL}/${jwt}`);
} else {
res.redirect(process.env.AUTH0_FAILURE_REDIRECTION_URL);
}
}
}
Since I want to authorize the Auth0 routes, I still need Auth 0 guards. The snippet code-shared by #harut Barseghyan.
// auth.guard.ts
import { ExecutionContext, Injectable, UnauthorizedException } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class AuthCustomGuard extends AuthGuard('auth0') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
The fact I have a custom guard, I have the power to activate it vice versa in a testing environment
// auth.controller.spec.ts
import { Test, TestingModule } from '#nestjs/testing';
import { AuthController } from '../auth.controller';
import { INestApplication } from '#nestjs/common';
import { AuthModule } from '../auth.module';
import * as request from 'supertest';
import { AuthCustomGuard } from '../guards/auth.guard';
describe('Auth Controller', () => {
let app: INestApplication;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AuthModule],
controllers: [AuthController],
})
.overrideGuard(AuthCustomGuard) // can you see that I override it
.useValue({ canActivate: () => true }) // true helped me to skip
.compile();
app = module.createNestApplication();
app.init()
});
it('should be visit auth login route', async () => {
return request(app.getHttpServer())
.get('/auth/login')
.expect(302)
});
it('should not redirect user to login page if auth 0 throws an error such as an invalid error request', async () => {
return request(app.getHttpServer())
.get('/auth/callback?error=invalid_request')
.expect(302)
});
afterAll(async () => {
jest.clearAllMocks();
await app.close();
});
});

inject services inside filters in nestjs

I'm trying to inject nestjs-config inside the following exception handler i've created:
import { ExceptionFilter, Catch, ArgumentsHost, Injectable } from '#nestjs/common';
import { HttpException } from '#nestjs/common';
import { InjectConfig } from 'nestjs-config';
#Injectable()
#Catch()
export class HttpExceptionFilter implements ExceptionFilter {
constructor(
#InjectConfig()
private readonly config,
) {
this.config = config.get('errors');
}
catch(exception: HttpException, host: ArgumentsHost) {
// some code here that calls this.config
}
}
but it's returning undefined: TypeError: Cannot read property 'get' of undefined
this is how the exception handler is defined globally:
const app = await NestFactory.create(AppModule, { cors: true });
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
Ok so I've just realised that in your code you're creating the filter outside of the container therefore the ConfigService is not injected. There's a few ways to resolve this. One
ConfigService.load(path.resolve(__dirname, 'config', '*.ts'))
const app = await NestFactory.create(AppModule, { cors: true });
app.useGlobalFilters(new HttpExceptionFilter(ConfigService));
await app.listen(3000);
Or
const app = await NestFactory.create(AppModule, {cors: true});
const config = app.get<ConfigService>(ConfigService);
app.useGlobalFilters(new HttpExceptionFilter(config));
await app.listen(3000);
Depending that your AppModule looks like this
#Module({
imports: [ConfigModule.load(path.resolve(__dirname, 'config', '*.ts')],
})
export AppModule {}
Or like this:
const app = await NestFactory.create(AppModule, {cors: true});
const httpExceptionFilter = app.get(HttpExpectionFilter);
app.useGlobalFilters(httpExpectionFilter);
solved it by calling the ConfigService as follows:
export class HttpExceptionFilter implements ExceptionFilter {
constructor(private readonly config: ConfigService) {
this.config = ConfigService.get('errors');
}
catch(exception: HttpException, host: ArgumentsHost) {
// some code here that calls this.config
}
}
You can make the exception filter request scoped
import { ExceptionFilter, Catch, ArgumentsHost, Injectable, Scope } from '#nestjs/common';
import { HttpException } from '#nestjs/common';
import { InjectConfig } from 'nestjs-config';
#Injectable({ scope: Scope.REQUEST })
#Catch()
export class HttpExceptionFilter implements ExceptionFilter {
constructor(
#InjectConfig()
private readonly config,
) {
this.config = config.get('errors');
}
catch(exception: HttpException, host: ArgumentsHost) {
// some code here that calls this.config
}
}
Now with each request new instance of the HttpExceptionFilter gets created with its dependencies
For the Nestjs V8, you could put this in the AppModule providers:
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},

NestJS Nest can't resolve dependencies of the RolesService (+, +, ?)

Hi I'm programming using the NestJS framework (with MongoDB) and have build some modules now. When I try to import a model from another module it returns this error:
Nest can't resolve dependencies of the RolesService (+, +, ?).
Now, I've implemented the code like this:
app.module.ts
import { GroupsModule } from './groups/groups.module';
import { Module } from '#nestjs/common';
import { MongooseModule } from '#nestjs/mongoose';
import { UsersModule } from 'users/users.module';
import { RolesModule } from 'roles/roles.module';
#Module({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/example'),
UsersModule,
GroupsModule,
RolesModule,
],
providers: [],
})
export class AppModule {}
users.module.ts
import { Module } from '#nestjs/common';
import { MongooseModule } from '#nestjs/mongoose';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { RolesService } from 'roles/roles.service';
import { UserSchema } from './schemas/user.schema';
import { RoleSchema } from 'roles/schemas/role.schema';
#Module({
imports: [
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
MongooseModule.forFeature([{ name: 'Role', schema: RoleSchema }]),
],
controllers: [UsersController],
providers: [UsersService, RolesService],
exports: [UsersService],
})
export class UsersModule {}
users.service.ts
import { Model } from 'mongoose';
import { ObjectID } from 'mongodb';
import { InjectModel } from '#nestjs/mongoose';
import { Injectable, HttpException, HttpStatus } from '#nestjs/common';
import { User } from './interfaces/user.interface';
#Injectable()
export class UsersService {
constructor(#InjectModel('User') private readonly userModel: Model<User>) {}
}
groups.module.ts
import { MongooseModule } from '#nestjs/mongoose';
import { GroupsController } from './groups.controller';
import { RolesService } from '../roles/roles.service';
import { GroupsService } from './groups.service';
import { GroupSchema } from './schemas/group.schema';
import { UserSchema } from '../users/schemas/user.schema';
import { RoleSchema } from '../roles/schemas/role.schema';
#Module({
imports: [
MongooseModule.forFeature([{ name: 'Group', schema: GroupSchema }]),
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
MongooseModule.forFeature([{ name: 'Role', schema: RoleSchema }]),
],
controllers: [GroupsController],
providers: [GroupsService, RolesService],
exports: [GroupsService],
})
groups.service.ts
import { Injectable, HttpException, HttpStatus } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { ObjectID } from 'mongodb';
import { Model } from 'mongoose';
import { Group } from './interfaces/group.interface';
import { User } from '../users/interfaces/user.interface';
import { CreateGroupDto } from './dto/create-group.dto';
import { RolesDto } from 'roles/dto/roles.dto';
import { Role } from '../roles/interfaces/role.interface';
#Injectable()
export class GroupsService {
constructor(#InjectModel('Group') private readonly groupModel: Model<Group>,
#InjectModel('Role') private readonly roleModel: Model<Role>) {} }
roles.module.ts
import { Module } from '#nestjs/common';
import { MongooseModule } from '#nestjs/mongoose';
import { RolesController } from './roles.controller';
import { RolesService } from './roles.service';
import { RoleSchema } from './schemas/role.schema';
import { UserSchema } from '../users/schemas/user.schema';
import { GroupSchema } from '../groups/schemas/group.schema';
#Module({
imports: [
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
MongooseModule.forFeature([{ name: 'Role', schema: RoleSchema }]),
MongooseModule.forFeature([{ name: 'Group', schema: GroupSchema }]),
],
controllers: [RolesController],
providers: [RolesService],
exports: [RolesService],
})
export class RolesModule {}
roles.service.ts
import { Injectable, HttpException, HttpStatus } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { ObjectID } from 'mongodb';
import { Model } from 'mongoose';
import { Role } from './interfaces/role.interface';
import { User } from '../users/interfaces/user.interface';
import { Group } from '../groups/interfaces/group.interface';
import { CreateRoleDto } from './dto/create-role.dto';
import { RolesDto } from './dto/roles.dto';
#Injectable()
export class RolesService {
constructor( #InjectModel('Role') private readonly roleModel: Model<Role>,
#InjectModel('User') private readonly userModel: Model<User>,
#InjectModel('Group') private readonly groupModel: Model<Group> ) {} }
While the DI in the users and roles works fine, the error arise when I try to import the Group Model in the roles service. Please tell me if you see anything wrong, I've follow the same schema from users with groups but unfortunately can't see where the error lives.
Thanks in advance.
UPDATE: OK I think my error is when I try to use a module service function outside the module. I mean I modified (in order to simplify) I'll modify the code this way:
users.module.ts
#Module({
imports: [
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
RolesModule,
],
controllers: [UsersController],
providers: [UsersService, RolesService],
exports: [UsersService],
})
export class UsersModule {}
users.controller.ts
export class UsersController {
constructor(private readonly usersService: UsersService,
private readonly rolesService: RolesService){}
async addRoles(#Param('id') id: string, #Body() userRolesDto: UserRolesDto): Promise<User> {
try {
return this.rolesService.setRoles(id, userRolesDto);
} catch (e){
const message = e.message.message;
if ( e.message.error === 'NOT_FOUND'){
throw new NotFoundException(message);
} else if ( e.message.error === 'ID_NOT_VALID'){
throw new BadRequestException(message);
}
}
}
}
roles.module.ts
#Module({
imports: [
MongooseModule.forFeature([{ name: 'Role', schema: RoleSchema }]),
],
controllers: [RolesController],
providers: [RolesService],
exports: [RolesService],
})
export class RolesModule {}
roles.service.ts
#Injectable()
export class RolesService {
userModel: any;
constructor( #InjectModel('Role') private readonly roleModel: Model<Role> ) {}
// SET USER ROLES
async setRoles(id: string, rolesDto: RolesDto): Promise<User> {
if ( !ObjectID.isValid(id) ){
throw new HttpException({error: 'ID_NOT_VALID', message: `ID ${id} is not valid`, status: HttpStatus.BAD_REQUEST}, 400);
}
try {
const date = moment().valueOf();
const resp = await this.userModel.updateOne({
_id: id,
}, {
$set: {
updated_at: date,
roles: rolesDto.roles,
},
});
if ( resp.nModified === 0 ){
throw new HttpException({ error: 'NOT_FOUND', message: `ID ${id} not found or entity not modified`, status: HttpStatus.NOT_FOUND}, 404);
} else {
let user = await this.userModel.findOne({ _id: id });
user = _.pick(user, ['_id', 'email', 'roles', 'created_at', 'updated_at']);
return user;
}
} catch (e) {
if ( e.message.error === 'NOT_FOUND' ){
throw new HttpException({ error: 'NOT_FOUND', message: `ID ${id} not found or entity not modified`, status: HttpStatus.NOT_FOUND}, 404);
} else {
throw new HttpException({error: 'ID_NOT_VALID', message: `ID ${id} is not valid`, status: HttpStatus.BAD_REQUEST}, 400);
}
}
}
That's it, as you can see when I try to use from users.controller the roles.service setRole method it returns me an error:
Nest can't resolve dependencies of the RolesService (?). Please make sure that the argument at index [0]is available in the current context.
I don't understand where the problem is because I'm injecting the Role model in the roles.module already and it don't understand it. In fact if I don't create the call from users.module to this dependency everything goes fine.
Any tip?
(I've red the suggestion from stackoverflow, I'll don't do it again)
I think the problem is that you're importing the same model multiple times, like:
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]
and also providing the same service multiple times:
providers: [RolesService]
I would not inject the models themselves but rather the corresponding services that encapsulate the model. Then you export the service in its module and import the module where needed. So the RolesService would inject the UsersSerivce instead of the UserModel.
With your current setup, you would run into circular dependencies though. This can be handled with fowardRef(() => UserService) but should be avoided if possible, see the circular dependency docs. If this is avoidable depends on your business logic however.
If you don't have circular dependencies then for example export your RolesService
#Module({
imports: [MongooseModule.forFeature([{ name: 'Role', schema: RoleSchema }])]
controllers: [RolesController],
providers: [RolesService],
exports: [RolesService],
})
export class RolesModule {}
and import the RolesModule wherever you want to use the RolesService, e.g.:
#Module({
imports: [
RolesModule,
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])
],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})

Categories

Resources