Dynamic Service Invokation - IOC - nestjs

how can I invoke a service's method dynamically (looked up from a hash-map) from a controller:
registration.service.ts
import { Injectable } from '#nestjs/common';
import { CacheService } from '../cache/cache.service';
import { LoggerService } from '../../config/logger/logger.service';
#Injectable()
export class RegistrationService {
constructor(private readonly cache: CacheService,
private readonly logger: LoggerService) {
}
async process() {
this.logger.info(`RegistrationService - testThis - hit!`);
}
}
app.controller.ts
import { Controller, Get, Res } from '#nestjs/common';
import { RegistrationService } from './modules/campaignOne/registration.service';
const SCREENS = {
'start': 'this.registration.process'
};
#Controller()
export class AppController {
constructor(private readonly registration: RegistrationService) {}
#Get('test')
async test(#Res() res: any): any {
await eval(SCREENS['start'])();
return Promise.resolve('ok');
}
}
When await eval(SCREENS['start'])(); is executed, the method is called but an error is thrown at this.logger.. because it can't resolve the logger.
The idea is to have multiple providers and logic in the controller will determine which provider to invoke dynamically.
I know the problem and solution lies in dependency injection and IOC - but I don't know how to resolve.
Help please?

I would use the factory approach with this.
First create an interface to define the shape of the "dynamic" service you want
//IService.ts
export interface IService {
invoke(): Promise<void>;
}
Now all of your services that you want dynamic can implement this interface.
NOTE: This service is not using the nest #Injectable annotation because this class is not what will be actually injected as you'll see below.
//registration.service.ts
import { CacheService } from '../cache/cache.service';
import { LoggerService } from '../../config/logger/logger.service';
export class RegistrationService implements IService {
private readonly cache: CacheService;
private readonly logger: LoggerService;
constructor(cache: CacheService, logger: LoggerService) {
this.cache = cache;
this.logger = logger;
}
async invoke() : Promise<void> {
this.logger.info(`RegistrationService - testThis - hit!`);
return Promise.resolve();
}
}
Now create your factory class that WILL be the thing that gets injected by the IOC
//ScreenFactory.ts
import { Injectable } from '#nestjs/common';
import { RegistrationService } from './modules/campaignOne/registration.service';
#Injectable()
export class ScreenFactory {
private readonly cache: CacheService;
private readonly logger: LoggerService;
constructor(cache: CacheService, logger: LoggerService) {
this.cache = cache;
this.logger = logger;
}
public getService(screenName: string) : IService {
switch(screenName) {
case 'start': return new RegistrationService(this.cache, this.logger);
default: throw new Error('No service defined for the given screen');
}
}
}
Now in your module, inject the factory instead
import { Controller, Get, Res } from '#nestjs/common';
import { ScreenFactory } from './modules/ScreenFactory';
#Controller()
export class AppController {
constructor(private readonly screenFactory: ScreenFactory) {}
#Get('test')
async test(#Res() res: any): any {
//await eval(SCREENS['start'])();
const service: IService = this.screenFactory.getService('start');
await service.invoke();
return Promise.resolve('ok');
}
}
That's a lot of pseudo code but this is how I generally handle the "I need a different thing per request" type of scenario.

Related

NestJS / TypeORM: Custom repository method is not accessible in service

New to NestJS and TypeORM, and the similar questions on SO didn't solve my problem.
I have a custom TypeORM repository in NestJS using it in service, but it fails with error:
TypeError: this.tenantRepository.createTenant is not a function.
tenants.module.ts:
import { TenantRepository } from './tenant.repository';
#Module({
imports: [
TypeOrmModule.forFeature([TenantRepository]),
],
controllers: [TenantsController],
providers: [TenantsService],
})
export class TenantsModule { }
tenant.repository.ts:
// ...
import { TenantEntity } from './entities/tenant.entity';
#EntityRepository(TenantEntity)
export class TenantRepository extends Repository<TenantEntity>{
async createTenant(createTenantDto: CreateTenantDto): Promise<TenantEntity> {
const { name, email } = createTenantDto;
const newTenant = new TenantEntity()
newTenant.name = name;
newTenant.email = email;
await newTenant.save()
return newTenant;
}
}
And here's where the error is triggered (tenants.service.ts)
// ...
import { TenantEntity } from './entities/tenant.entity';
import { TenantRepository } from './tenant.repository';
#Injectable()
export class TenantsService {
constructor(
#InjectRepository(TenantRepository)
private tenantRepository: TenantRepository
) { }
async createTenant(createTenantDto: CreateTenantDto): Promise<TenantEntity> {
return await this.tenantRepository.createTenant(createTenantDto); // <-- ERROR
}
}
I can inject entity in service and use it for simple CRUD, but I want to separate concerns and use the repository pattern.
This is a POST endpoint and the error is only after submission from Swagger.
Also, VS Code autocomplete is suggesting createTenant after typing this.tenantRepository
Where am I going wrong?
EntityRepository decorator was deprecated, and as far as I know, you need to define a custom class that extends Repository and decorate it with #Injectable. Hence, you need to have some changes as follows:
tenant.repository.ts:
import { Injectable } from '#nestjs/common';
import { DataSource, Repository } from 'typeorm';
#Injectable()
export class TenantRepository extends Repository<TenantEntity>{
constructor(private dataSource: DataSource) {
super(TenantEntity, dataSource.createEntityManager());
}
async createTenant(createTenantDto: CreateTenantDto): Promise<TenantEntity> {
const { name, email } = createTenantDto;
const newTenant = this.create({ name, email });
await this.save(newTenant);
return newTenant;
}
}
tenants.module.ts:
import { TenantRepository } from './tenant.repository';
#Module({
imports: [
TypeOrmModule.forFeature([TenantRepository]),
],
controllers: [TenantsController],
providers: [TenantsService, TenantRepository],
})
export class TenantsModule { }
tenants.service.ts:
import { TenantEntity } from './entities/tenant.entity';
import { TenantRepository } from './tenant.repository';
#Injectable()
export class TenantsService {
constructor(
private tenantRepository: TenantRepository
) { }
async createTenant(createTenantDto: CreateTenantDto): Promise<TenantEntity> {
return await this.tenantRepository.createTenant(createTenantDto);
}
}
You also have access to built-in typeorm methods like save, create, find, etc. since the custom repository is derived from Repository class.

User and its data is undefined on the context.switchToHttp().getRequest() with nestjs guards

I'm new to nestJs and I needed to add role based access to the application so I followed the documentation but in the execution context user doesn't exist. I can't seems to find the problem
here is the full repo https://github.com/callme-MJ/test-sample
roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "#nestjs/common";
import { Reflector } from "#nestjs/core";
import { use } from "passport";
import { Role } from "src/typeorm/roles.enum";
#Injectable()
export class RolesGuard implements CanActivate{
constructor (private reflector:Reflector){}
canActivate(context: ExecutionContext): boolean {
const requiredRole = this.reflector.getAllAndOverride<Role[]>('roles',[
context.getHandler(),
context.getClass(),
]);
if (!requiredRole) {
return true
}
const {user} = context.switchToHttp().getRequest();
return requiredRole.some((role)=> user.role.includes(role));
}
}
auth.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '#nestjs/common';
import { AuthService } from 'src/auth/services/auth/auth.service';
import { Role } from 'src/typeorm/roles.enum';
import { JwtAuthGuard } from 'src/utils/guards/jwt.auth.guard';
import { LocalAuthGuard } from 'src/utils/guards/local-auth.guards';
import { RolesGuard } from 'src/utils/guards/roles.guards';
import { Roles } from 'src/utils/roles.decorator';
#Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) { }
#UseGuards(JwtAuthGuard,RolesGuard)
#Get('dashboard')
#Roles(Role.CORDINATOR)
getResponse(#Request() req): any {
return req.user;
}
}
At this line in your repo you have APP_GUARD which is a global guard binding. This means that this RolesGuard will be registered and used before your JwtAuthGuard in the getResponse method of the AuthController. Take a look at the section on "Enabling Authentication Globally" for a way around this. Otherwise, you can remove the APP_GUARD for the RolesGuard and just bind it where necessar

Trying to use in Nestjs Custom Repository typeorm

by example:
[https://stackoverflow.com/questions/72549668/how-to-do-custom-repository-using-typeorm-mongodb-in-nestjs][1]
Created custom repository в yandex-ndd-api-client.module:
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '#nestjs/common';
// import {Team} from '#Domain/Team/Models/team.entity';
import { TestRepositoryTypeorm } from '../entity/testRepositoryTypeorm.entity';
#Injectable()
export class TestRepository extends Repository<TestRepositoryTypeorm> {
constructor(private dataSource: DataSource) {
super(TestRepositoryTypeorm, dataSource.createEntityManager());
}
async findTest(): Promise<any> { //TestRepositoryTypeorm | undefined
const findTests = await this.dataSource
.getRepository(TestRepositoryTypeorm)
.createQueryBuilder('test')
.getMany();
return await findTests;
}
}
Connected in the module::
providers: [YandexDeliveryService, YandexNddApiClientService, ConfigService, SyncService, TestRepository],
Connected it to the service yandex-ndd-api-client.service:
import { TestRepository } from './repository/testRepositoryTypeorm.retository';
#Injectable()
export class YandexNddApiClientService {
constructor(
// private yandexDeliveryApiService: YandexDeliveryApiService,
private httpService: HttpService,
private dataSource: DataSource,
private configService: ConfigService,
#Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
// #InjectRepository(TestRepository)
private testRepository: TestRepository,
) {}
Called in service:
//testRepositoryTypeorm
async testRepositoryTypeorm(): Promise<any> {
try {
console.log('testRepositoryTypeorm');
// return 'testRepositoryTypeorm';
return await this.testRepository.findTest();
} catch (e) {
console.log('ERROR testRepositoryTypeorm:', e);
}
}
As a result:
ERROR [ExceptionHandler] Nest can't resolve dependencies of the YandexNddApiClientService (HttpService, DataSource, ConfigService, winston, ?, SchedulerRegistry). Please make sure that the argument TestRepository at index [4] is available in the DetmirApiClientModule context.
Potential solutions:
- If TestRepository is a provider, is it part of the current DetmirApiClientModule?
- If TestRepository is exported from a separate #Module, is that module imported within DetmirApiClientModule?
#Module({
imports: [ /* the Module containing TestRepository */ ]
})
[1]: https://stackoverflow.com/questions/72549668/how-to-do-custom-repository-using-typeorm-mongodb-in-nestjs
DetmirApiClientModule.ts:
import { Module } from '#nestjs/common';
import { DetmirApiClientService } from './detmir-api-client.service';
import { DetmirApiClientController } from './detmir-api-client.controller';
import { SyncService } from 'src/sync.service';
import { YandexNddApiClientService } from 'src/yandex-ndd-api-client/yandex-ndd-api-client.service';
import { HttpModule, HttpService } from '#nestjs/axios';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { YandexNddApiClientModule } from 'src/yandex-ndd-api-client/yandex-ndd-api-client.module';
#Module({
providers: [DetmirApiClientService, SyncService, YandexNddApiClientService],
controllers: [DetmirApiClientController],
imports: [HttpModule, YandexNddApiClientModule], //TestRepository
})
export class DetmirApiClientModule {}
Most likely your YandexNddApiClientModule does not add the YandexNddApiClientService to the exports array. Add YandexNddApiClientService to the exports if it is not already there and remove YandexNddApiClientService from the providers array of DetmirApiClientModule. The error is being raised because you have YandexNddApiClientService declared in the providers of DetmirApiClientModule so Nest is trying to create the provider in the new module rather than re-use the module from its original context

How to get name of module, which controller processing request?

I want to get name of module, which controller processing request.
#Get('/')
getIndex() {
console.log('name of module');
}
I don't know exactly the purpose behind your question, but I'll leave you some alternatives.
First one and the dirtier. You can get the instance of the module that your productController is imported by finding it into the modules container.
import { Controller, Get, Query } from '#nestjs/common';
import { ModulesContainer } from '#nestjs/core';
import { Module } from '#nestjs/core/injector/module';
#Controller('path')
export class ProductController {
constructor(
private modulesContainer: ModulesContainer,
private productService: ProductService
) { }
#Get()
findAll(#Query() dto: any) {
let yourModule: Module;
this.modulesContainer.forEach((v) => {
if(v.controllers.has(this.constructor.name)) { // in this condition, you will find your module based in the imports from it, if your controller is importe in some module it will get the module and put in "yourModule" variable.
// Here
yourModule= v;
}
});
console.log(yourModule);
return this.productService.findAll();
}
}
And for a cleaner approach you can get the moduleRef in your controller
import { Controller, Get, Query } from '#nestjs/common';
import { ModuleRef} from '#nestjs/core';
#Controller('path')
export class ProductController {
constructor(
private moduleRef: ModuleRef,
private productService: ProductService
) { }
#Get()
findAll(#Query() dto: any) {
console.log(this.moduleRef) //your module ref
return this.productService.findAll();
}
}
But of course depends on what's you're trying to do.

How To Mock Repository, Service and Controller In NestJS (Typeorm & Jest)

I'm new at typescript. My Nestjs project app is something like this. I'm trying to use repository pattern, so i separated business logic (service) and persistance logic (repository)
UserRepository
import { Injectable } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserEntity } from './entities/user.entity';
#Injectable()
export class UserRepo {
constructor(#InjectRepository(UserEntity) private readonly repo: Repository<UserEntity>) {}
public find(): Promise<UserEntity[]> {
return this.repo.find();
}
}
UserService
import { Injectable } from '#nestjs/common';
import { UserRepo } from './user.repository';
#Injectable()
export class UserService {
constructor(private readonly userRepo: UserRepo) {}
public async get() {
return this.userRepo.find();
}
}
UserController
import { Controller, Get } from '#nestjs/common';
import { UserService } from './user.service';
#Controller('/users')
export class UserController {
constructor(private readonly userService: UserService) {}
// others method //
#Get()
public async getUsers() {
try {
const payload = this.userService.get();
return this.Ok(payload);
} catch (err) {
return this.InternalServerError(err);
}
}
}
How do i create unit testing for repository, service & controller without actually persist or retrieve data to DB (using mock)?
Mocking in NestJS is pretty easily obtainable using the testing tools Nest exposes is #nestjs/testing. In short, you'll want to create a Custom Provider for the dependency you are looking to mock, and that's all there is. However, it's always better to see an example, so here is a possibility of a mock for the controller:
describe('UserController', () => {
let controller: UserController;
let service: UserService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [UserController],
providers: [
{
provide: UserService,
useValue: {
get: jest.fn(() => mockUserEntity) // really it can be anything, but the closer to your actual logic the better
}
}
]
}).compile();
controller = moduleRef.get(UserController);
service = moduleRef.get(UserService);
});
});
And from there you can go on and write your tests. This is pretty much the same set up for all tests using Nest's DI system, the only thing to be aware of is things like #InjectRepository() and #InjectModel() (Mongoose and Sequilize decorators) where you'll need to use getRepositoryToken() or getModelToken() for the injection token. If you're looking for more exmaples take a look at this repository

Resources