How can I dynamically inject different providers with Nest.js? - oop

I'm facing the following issue. I have a service used by a controller. The service (in the snippets below QueueService) injects a provider imported from a package. I aim to reuse the QueueService across the controller methods, but I also need to dynamically specify which provider QueueService should use.
My question is, how can I achieve this behaviour?
import { PubsubService } from '#myorg/queue'
#Module({
imports: [
ConfigModule.forRoot({
SHARED_RESOURCES_PROJECT_ID: Joi.string().required()
})
})
],
controllers: [AppController],
providers: [
{
provide: 'PUBSUB',
useValue: new PubsubService()
},
{
provide: 'INTEGRATION_PUBSUB',
useValue: new PubsubService({ projectId: process.env.SHARED_RESOURCES_PROJECT_ID })
}
]
})
export class AppModule {}
#Controller()
export class AppController {
constructor(private queueService: QueueService) {}
#Post()
async create() {
...
// here I want to use queueService with `PUBSUB` injected
return this.queueService.sendMessage(...)
}
#Patch()
async update() {
...
// here I want to use queueService with `INTEGRATION_PUBSUB` injected
return this.queueService.sendMessage(...)
}
}
#Injectable()
export class QueueService {
constructor(
// how can I dynamically change `#Inject('PUBSUB')` to `#Inject('INTEGRATION_PUBSUB')`?
#Inject('PUBSUB') private readonly pubsubService: PubsubService
) {}
async sendMessage(payload): Promise<void> {
return this.pubsubService.sendMessage(payload)
}
}

dynamic inject is not possible after object(in this case controller) created . so you have two option
1- create two QueueService (one for PUBSUB and another for INTEGRATION_PUBSUB) and inject both to controller. use those in your controller functions. (i recommend this)
2- inject both PUBSUB and INTEGRATION_PUBSUB into QueueService and pass another param in sendMessage function . so check this param to choose between PUBSUB and INTEGRATION_PUBSUB

Related

How to test dynamic modules in Nest.js?

My implementation is based on this article: https://dev.to/nestjs/advanced-nestjs-how-to-build-completely-dynamic-nestjs-modules-1370
I want to test my generic, Twilio-based SMS sender service that I share between multiple parts of my application. I want to configure it when I'm importing it from somewhere else, so I'm writing it as a dynamic module. On top of that, the options that I pass to the dynamic module are themselves constructed dynamically, they are read from my .env file. I'm using the factory pattern when I'm registering my provider:
// app.module.ts
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: [
'.env',
],
validationSchema,
}),
SharedSmsModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService<EnvironmentVariables>) => {
return {
accountSid: configService.get('TWILIO_ACCOUNT_SID'),
authToken: configService.get('TWILIO_AUTH_TOKEN'),
smsSenderPhoneNumber: configService.get(
'TWILIO_SMS_SENDER_PHONE_NUMBER'
),
};
},
}),
],
})
export class AppModule {}
My shared-sms module calls the function provided in the registerAsync method in app.module.ts:
// shared-sms.module.ts
export interface SharedSmsModuleOptions {
accountSid: string;
authToken: string;
smsSenderPhoneNumber: string;
}
export interface SharedSmsModuleAsyncOptions extends ModuleMetadata {
imports: any[];
inject: any[];
useFactory?: (
...args: any[]
) => Promise<SharedSmsModuleOptions> | SharedSmsModuleOptions;
}
#Module({})
export class SharedSmsModule {
static registerAsync(
sharedSmsModuleAsyncOptions: SharedSmsModuleAsyncOptions
): DynamicModule {
return {
global: true,
module: SharedSmsModule,
imports: sharedSmsModuleAsyncOptions.imports,
providers: [
{
provide: 'SHARED_SMS_OPTIONS',
useFactory: sharedSmsModuleAsyncOptions.useFactory,
inject: sharedSmsModuleAsyncOptions.inject || [],
},
SharedSmsService,
],
exports: [SharedSmsService],
};
}
}
Now I have access to the options variables in my shared-sms.service:
// shared-sms.service
#Injectable()
export class SharedSmsService {
private twilioClient: Twilio;
constructor(
#Inject('SHARED_SMS_OPTIONS') private options: SharedSmsModuleOptions
) {
this.twilioClient = new Twilio(
this.options.accountSid,
this.options.authToken
);
}
async sendSms(sendSmsDto: SendSmsDto): Promise<MessageInstance> {
await validateOrReject(plainToInstance(SendSmsDto, sendSmsDto));
const smsData = {
from: this.options.smsSenderPhoneNumber,
to: sendSmsDto.to,
body: sendSmsDto.body,
};
return await this.twilioClient.messages.create(smsData);
}
}
So long everything seems to be working. But I'm having issues when I'm trying to test the service's sendSms function. I can write tests that work when I'm providing hardcoded Twilio test account values in my test file. But I don't want to commit them to the repository, so I would want to get them from my .env file. I have tried providing everything to the Test.createTestingModule function when I'm creating my moduleRef, based on what I did in the code that I already wrote, but I couldn't specify the Twilio test account values dynamically. As I don't see documentation regarding this issue, I feel like that I'm either missing a conceptual point (providing so many things in the test seems like an overkill) or there is a trivial work-around. Please help me figure out how to pass those values to my tests from my .env file

Dependency injection issue for `#ntegral/nestjs-sentry` package in nestjs app

I am have an issue with this package #ntegral/nestjs-sentry in nestjs. I have a custom logger I use in my application
#Injectable()
export class CustomLogger implements LoggerService {
constructor(#InjectSentry() private readonly client: SentryService) {}
log(message: any, ...optionalParams: any[]) {
this.client.instance().captureMessage(message, ...optionalParams);
}
}
I then inject the into User Controller and in the user.controller.spec.ts
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
CustomLogger,
UsersService,
SentryService,
],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
I get this error
FAIL src/users/users.controller.spec.ts (9.449 s)
● UsersController › should be defined
Nest can't resolve dependencies of the CustomLogger (?). Please make sure that the argument Symbol(SentryToken) at index [0] is available in the RootTestModule context.
Potential solutions:
- If Symbol(SentryToken) is a provider, is it part of the current RootTestModule?
- If Symbol(SentryToken) is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing Symbol(SentryToken) */ ]
})
I have tried adding the SentryService to the spec providers but that does not fix the error. Has anyone encountered this and how did you fix it.
I run in exactly the same issue. It seemed to be that the library uses a different token for its own Inject annotation. I was able to fix it in my tests by using the provided token for the SentryService mock.
import { SENTRY_TOKEN } from '#ntegral/nestjs-sentry';
// ...
const module: TestingModule = await Test.createTestingModule({
providers: [
// ...
{
provide: SENTRY_TOKEN,
useValue: { debug: jest.fn() }, // provide SentryService Mock here
},
],
})

NestJs version for modules

I would like to add version for my modules, but i don't know how can I do this. I tried to create a common module.ts but the same name of services killed each us. I tried different module.ts for the versions, it was better but the services with same names didn't work.
This is my last structure:
test-module
1.0
controllers
test.controller.ts
services
test.service.ts
test.module.ts
1.1
controllers
test.controller.ts
services
test.service.ts
test.module.ts
This is my test service(s) for the versions:
import * as _ from 'lodash';
import { Injectable } from '#nestjs/common';
#Injectable()
export class TestService {
public test() {
return '1.0'; // and 1.1 in 1.1 directory
}
}
This is my module.ts:
import { Module, Logger } from '#nestjs/common';
import { TestModule as DorotTwo } from 'test-module/1.1/test.module';
import { TestModule as DorotOne } from 'test-module/1.0/test.module'
#Module({
controllers: [ProtobufController],
providers: [],
imports: [
DorotTwo,
DorotOne,
],
})
export class ProjectModule {
constructor() {
Logger.log('App initialized');
}
}
This is a simple test Controller in the project who want use the modules. A tried import TestService from 1.0 or 1.1 but the test function's response is always 1.0 because that is the last element in the import.
#Controller()
export class ProtobufController {
constructor(private readonly testService: TestService) {
console.log(this.testService.test()); // Always 1.0
}
.....
It is working if I use full different names for services for example (eg: UserAuthenticationService10, RegisterAuthenticationService10), but this is horrible and if i forget rename it in new version, it will overwrite.
Is exists an example where I can read how can I create this versioned module?
Would using custom providers be a satisfying solution for you?
Example:
// 1.0
#Module({
providers: [
{ provide: 'TestService_1.0', useClass: TestService }
]
})
export class TestModule {}
// 1.1
#Module({
providers: [
{ provide: 'TestService_1.1', useClass: TestService }
]
})
export class TestModule {}
// Then
#Controller()
export class ProtobufController {
constructor(
#Inject('TestService_1.0') private readonly testService_10,
#Inject('TestService_1.1') private readonly testService_11
) {
console.log(this.testService_10.test());
console.log(this.testService_11.test());
}
}
I obviously haven't tested this and you should adapt it to your usecase. I suggest you to have a look at https://docs.nestjs.com/fundamentals/custom-providers.

NestJS - Default (wildcard) route?

In my Angular app, if I load the home page / and then navigate to, say, /products, it works fine (it's a lazy-loaded module). But if now I reload the page, the browser makes a GET /products call to the server, which results in a 404.
The solution is to send index.html and the Angular app is back on rails. So in Express I do app.all("*", (req,res) => { res.sendFile("index.html") }) and it works.
How to do the same thing in Nest?
There is a #All decorator, but each controller in a given component handles a subroute, for instance #Controller("cats") will match /cats routes, so if I add #All in this controller, it will match only /cats/*, not *.
Must I really create a whole separate module with a controller, just for this? That's what I did
#Controller() // Matches "/"
export class GenericController {
#All() // Matches "*" on all methods GET, POST...
genericFunction(){
console.log("Generic route reached")
}
}
And in my main module :
#Module({
imports: [
ItemsModule, // Other routes like /items
GenericModule, // Generic "*" route last
],
})
It works, but it seems overkill. Is this the way to go or is there a simpler trick?
So, will be best to use global-scoped exception filter.
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalFilters(new NotFoundExceptionFilter());
await app.listen(3000);
}
bootstrap();
NotFoundExceptionFilter:
import { ExceptionFilter, Catch, NotFoundException } from '#nestjs/common';
import { HttpException } from '#nestjs/common';
#Catch(NotFoundException)
export class NotFoundExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
// here return `index.html`
}
}
Maybe it will not work, will test later
You don't need to create a separated GenericModule. However, GenericController is fully valid and you approach is definitely a good one. The question is rather what would you like to achieve using this generic route. If handling "Route not found" error is your requirement, a better choice is an exception filter.
An alternative answer in 2022.
I've solved this by specifying the routes in the order I want them evaluated. In my instance I am using a fallback route to catch all requests, but if I need custom processing I want to create a route which superceeds the fallback route.
However, in defining a catchall route /api/:resource in the AppController, I found the fallback route would overwrite all other routes.
My solution to this is to define the fallback route in it's own module and ensure that it is appended to the list of modules. This way it is created last and will only catch what falls through.
#router.ts
import {RouterModule} from '#nestjs/core/router';
import {ContentBlockModule} from './content_block/content_block.module';
import {FallbackModule} from './fallback/fallback.module';
const APIRoutesWithFallbackRoute = RouterModule.register([
{
// This lets me avoid prepending my routes with /api prefixes
path: 'api',
// Overload the /api/content_blocks route and foward it to the custom module
children: [
{
path: 'content_blocks',
module: ContentBlockModule,
},
],
},
{ //Fallback Route catches any post to /api/:resource
path: 'api',
module: FallbackModule,
},
]);
#app.module
App module imports the fallback module. Important Ensure FallbackModule is the last module to be declaired or it will overwrite routes that are included after it.
import {Module} from '#nestjs/common';
import {AppService} from './app.service';
import {APIRoutesWithFallbackRoute} from './APIRoutesWithFallbackRoute';
import {ContentBlockModule} from './content_block/content_block.module';
import {FallbackModule} from './fallback/fallback.module';
// APIRoutes include first, Fallback Routes prepended.
#Module({
imports: [APIRoutesWithFallbackRoute, ContentBlockModule, FallbackModule],
controllers: [],
providers: [AppService],
})
export class AppModule {}
FallbackController
import {Controller, Post, Req, Res} from '#nestjs/common';
import {defaultHandler} from 'ra-data-simple-prisma';
import {FallbackService} from './fallback.service';
#Controller()
export class FallbackController {
constructor(private readonly prisma: FallbackService) {}
#Post(':resource')
fallback(#Req() req, #Res() res) {
// return this.appService.getData();
console.log('executing from the default fallback route');
return defaultHandler(req, res, this.prisma);
}
}
ContentBlockController
The content block controller, included here for completeness.
#Controller()
export class ContentBlockController {
constructor(
private readonly contentBlockService: ContentBlockService,
private readonly prisma: PrismaService,
) {}
#Post()
async create(
#Body() contentBlock: content_blocks,
#Req() req: Request,
#Res() res: Response,
): Promise<void> {
console.log('executing from the resource specific route');
// lean on my service to do heavy business logic
const [model, values] = await this.contentBlockService.createContentBlock(
contentBlock,
);
// inject custom logic...
const alteredRequest: CreateRequest = {
...req,
body: {
...req.body,
params: {
data: values,
},
},
};
return createHandler(alteredRequest, res, model);
}
}
Using this system I am able to define a single route to handle 90% of the routes necessary to expose my Prisma models to my private API. And if I need custom logic I have full control.

How to pass values from one class's method to another class's method in Typescript?

I'm new to Object Oriented programming and am assuming this should be an easy concept for the seasoned OO programmers, but I'm certainly struggling with it.
In my Angular2 app I have a HttpService class as shown below:
http.service.ts
#Injectable()
export class HttpService {
constructor(private http: Http) { }
addLeaf(parentId, label, name){
var headers = new Headers();
headers.append('Content-Type', 'application/json');
return this.http.post('http://localhost:8000/addleaf/',
{'parentId':parentId,'label':label, 'name':name},
{ headers: headers })
.map(res => res).subscribe();
}
I try to call this method from within another class as below:
leaf.ts
import { HttpService } from './http.service';
export class Leaf{
name: string;
...
http: Http; // very unsure about these two lines
private httpService: HttpService = new HttpService(this.http)
constructor(input){
this.name = input.name;
...
add(){
//what should go here?
this.httpservice.addLeaf(this.id, this.label, this.name);
//error -> cannot read property 'post' of undefined
}
Reading this, I tried creating an instance of the HttpService class but I get the error that the post function does not exist. Also no luck putting the httpService in the constructor.
I call the method in my html like this:
(click)="leaf.add()"
EDIT: following #peeskillet's answer I modified leaf.ts and added leaf.component.ts as shown:
leaf.ts
export class Leaf{
name: string;
...
constructor(input){
this.name = input.name;
...
add(){
//what should go here?
}
}
leaf.component.ts
#Component({
providers: [HttpService],
})
export class LeafComponent {
leaf: Leaf;
constructor(private httpService: HttpService) {
this.httpService.addLeaf(this.leaf.id, this.leaf.type, this.leaf.name)
}
}
service works fine if I write pre-defined strings in place of the params, but still not sure how I can pass the parameters of the clicked leaf to this.
With Angular, we use dependency injection and Inversion of Control. What this means is that we do not create the service ourselves, but let Angular create it. Then we just ask for the service, then Angular will resolve any dependencies that service has. Take for example
#Injectable()
class Service {
constructor(private http: Http) {}
}
Here, Service has a dependency in Http. Http is not something that we can just grab out of thin air. We can't just do
let service = new Service(new Http());
Http is also dependent on some other services. Here what its constructor looks like
class Http {
constructor(backend: ConnectionBackend, options: RequestOptions) {}
}
You might think that maybe you can just instantiate it with the ConnectionBackend and RequestOptions
new Http(new ConnectionBackend(), new RequestOptions())`
But you can't do this either, as ConnectionBackend also has required dependencies. It's for this reason that we use Inversion of Control. We just add the service to a container, and when ask for the service, Angular look look up service, as see that it requires, Http, and see that Http requires ConnectionBackend and RequestOptions, etc, and Angular will create all the items, looking in its registry for all those items and putting them all together like Voltron. Then it will give us the service, fully populated.
So add our service to the container, we first need to add the Injectable decorator on the service
#Injectable()
class Service {
constructor(private http: Http) {}
}
Then we need to add it to the #NgModule.providers
#NgModule({
imports: [ HttpModule ],
providers: [ Service ]
})
class AppModule {}
Now, whenever we ask for Service, it will be fully populated with the Http (which is in the HttpModule).
How we ask for the service is through the constructor of either another service or component (directive, pipe, etc)
#Component({
})
class MyComponent {
constructor(private service: Service) {}
}
By seeing the Service type as a constructor argument, Angular knows to look up the Service in its container, then pass it through to us. This is the basics of Dependency Injection and Inversion of Control.
In your case of Leaf. If is is meant to be a service, then you can do the same
#Injectable()
class Leaf {
constructor(private service: Service) {}
}
#NgModule({
imports: [ HttpModule ],
providers: [ Leaf, Service ]
})
class AppModule {}
If you don't want to add Leaf as a provider, you don't need to. Just do
#Component({})
class MyComponent {
leaf: Leaf;
constructor(private service: Service) {
this.leaf = new Leaf(service);
}
}