Inject correct service based on parameter - javascript

Let's assume I have a two modules which are exporting BService and CService where both of those services extends AService
So code looks like this:
abstract class AService {
public run() {}
}
#Injectable()
export class BService extends AService {}
#Injectable()
export class CService extends AService {}
#Module({
providers: [BService],
exports: [BService],
})
export class BModule {}
#Module({
providers: [CService],
exports: [CService],
})
export class CModule {}
#Injectable()
class AppService {
constructor(protected readonly service: AService) {}
public run(context: string) { // let's assume context may be B or C
this.service.run();
}
}
#Module({
imports: [CModule, BModule],
providers: [{
provide: AppService,
useFactory: () => {
return new AppService(); // how to use BService/CService depending on the context?
}
}]
})
export class AppModule {}
But the key is, I cannot use REQUEST (to inject it directly in useFactory) from #nestjs/core as I'm using this service in cron jobs and with the API call
I also don't think Factory pattern is useful there, I mean it would work but I want to do it correctly
I was thinking about property based injection.
But I'm not sure how to use it in my case

In my opinion, the factory approach is exactly what you need. You described that you need a different service based on the context which is a great for for the factory approach. Let's try this:
Create an injectable factory:
import { Injectable } from '#nestjs/common';
import { AService } from './AService';
import { BService } from './BService';
import { CService } from './CService';
#Injectable()
export class ServiceFactory {
public getService(context: string) : AService {
switch(context) {
case 'a': return new BService();
case 'b': return new CService();
default: throw new Error(`No service defined for the context: "${context}"`);
}
}
}
Now import that factory into your app module:
import { ServiceFactory } from './ServiceFactory';
import { AService } from './AService';
#Module({
providers: [AppService, ServiceFactory]
})
export class AppModule {}
Now your app service will get the factory as a dependency which will create the appropriate service based on the context:
import { ServiceFactory } from './ServiceFactory';
import { AService } from './AService';
#Injectable()
class AppService {
constructor(readonly serviceFactory: ServiceFactory) { }
public run(context: string) {
const service: AService = this.serviceFactory.getService(context);
service.run();
}
}

If the property is static (e.g. environment variable), you can use a custom provider to choose the proper instance. However, if the property is in someway dynamic, you cannot soley rely on nest's dependency injection as it instantiates the provider on startup (with the exception of REQUEST scope, which isn't an option for you).
Static Property
Create a custom provider that instantiates the needed implementation based on a static property (e.g. environment variable).
{
provide: AService,
useClass: process.ENV.useBService ? BService : CService,
}
Dynamic Property with Request-Scope
Let's assume we have two different implementations of a service:
#Injectable()
export class BService {
public count = 0;
run() {
this.count++;
return 'B';
}
}
#Injectable()
export class CService {
public count = 0;
run() {
this.count++;
return 'C';
}
}
When the sum of the count variables of both is even, the BService should be used; CService when it's odd. For this, we create a custom provider with request scope.
{
provide: 'MyService',
scope: Scope.REQUEST,
useFactory: (bService: BService, cService: CService) => {
if ((bService.count + cService.count) % 2 === 0) {
return bService;
} else {
return cService;
}
},
inject: [BService, CService],
},
If our controller now injects the MyService token (#Inject('MyService')) and exposes its run method via an endpoint it will return B C B ...
Dynamic Property with Default-Scope
As we want to use the default scope (Singleton!), the static instantiation of nest's dependency injection cannot be used. Instead you can use the delegate pattern to select the wanted instance in the root class (AService in your example).
Provide all services as they are:
providers: [AService, BService, CService]
Decide dynamically in your AService which implementation to use:
#Injectable()
export class AService {
constructor(private bService: BService, private cService: CService) {}
run(dynamicProperty) {
if (dynamicProperty === 'BService') {
return this.bService.run();
} else {
return this.cService.run();
}
}
}

Related

Conditionally instantiate service class as a provider in NestJS

I have a controller called User and two service classes: UserAdminService and UserSuperAdminService.
When a user makes a request to any endpoint of the User controller, I want to check if the user making the request is an Admin or a Super Admin (based on the roles in the token) and instantiate the correct service (UserAdminService or UserSuperAdminService). Note that the two services implement the same UserService interface (just the internals of the methods that change a bit). How can I make this with NestJS?
What I tried:
user.module.ts
providers: [
{
provide: "UserService",
inject: [REQUEST],
useFactory: (request: Request) => UserServiceFactory(request)
}
],
user-service.factory.ts
export function UserServiceFactory(request: Request) {
const { realm_access } = JwtService.parseJwt(
request.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin))
return UserSuperAdminService;
else
return UserAdminService;
}
user.controller.ts
constructor(
#Inject("UserService") private readonly userService: UserServiceInterface
) {}
One of the reasons my code is not working is because I am returning the classes and not the instantiated objects from the factory, but I want NestJS to resolve the services dependencies. Any ideas?
Rather than passing back the class to instantiate, which Nest doesn't handle, you could add the UserSuperAdminService and UserAdminService to the inject array, and pass back the instance that Nest then would create per request.
providers: [
{
provide: "UserService",
inject: [REQUEST, UserSuperAdminService, UserAdminService],
useFactory: (request: Request, superAdminService: UserSuperAdminService, adminService: UserAdminService) => UserServiceFactory(request, superAdminService, adminService)
}
...
]
export function UserServiceFactory(request: Request, superAdminService: UserSuperAdminService, adminService: UserAdminService) {
const { realm_access } = JwtService.parseJwt(
request.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin))
return superAdminService;
else
return adminService;
}
Instead of trying to conditionally instantiate a service class you could create a global middleware to redirect the request to the appropriate controller e.g.
import { Injectable, NestMiddleware } from '#nestjs/common';
#Injectable()
export class AdminUserMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
const { realm_access } = JwtService.parseJwt(
req.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin)) {
req.url = req.url.replace(/^\/, '/super-admin/');
}
next();
}
}
Then you can apply it to all routes in your app.module.ts
#Module({
imports: [HttpModule],
controllers: [UserAdminController, UserSuperAdminController]
providers: [UserSuperAdminService, UserAdminService]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AdminUserMiddleware)
.forRoutes('/');
}
}
and have the following controlers:
#Controller('/')
export class UserAdminController {
private readonly logger: Logger = new Logger(UserAdminController.name);
constructor(private readonly userAdminService: UserAdminService) {}
#Controller('/super-admin')
export class UserSuperAdminController {
private readonly logger: Logger = new Logger(UserSuperAdminController.name);
constructor(private readonly userSuperAdminService: UserSuperAdminService) {}
}
See the NestJS docs and this post for further details

Nestjs cant resolve dependencies

Can't figure out what's the problem of my code. (I'm new with nestjs, I'm trying to learn it by passing some apps to it). Console log says:
Nest can't resolve dependencies of the UrlsAfipService (?). Please
make sure that the argument at index [0] is available in the ApiModule
context.
UrlsAfipService
import { Injectable } from '#nestjs/common';
import { AfipUrls } from './urls'
#Injectable()
export class UrlsAfipService {
constructor(
private readonly afipUrls: AfipUrls,
) {}
getWSAA () {
return this.afipUrls.homo().wsaa; // <- change to prod() for production
}
getService (service: string) {
return this.afipUrls.homo().service.replace('{service}', service)
}
}
AfipUrls
export class AfipUrls {
homo() {
return {
wsaa: 'https://url.com',
service: 'https://url.com'
}
}
prod() {
return {
wsaa: 'url.com',
service: 'url.com'
}
}
}
ApiModule
import { Module } from '#nestjs/common';
import { ApiController } from './api.controller';
import { UrlsAfipService } from './urls-afip.service'
import { WsaaService } from './wsaa.service'
import { DescribeService } from './describe.service';
#Module({
controllers: [ApiController],
providers: [UrlsAfipService, WsaaService, DescribeService]
})
export class ApiModule {}
AppModule
import { Module } from '#nestjs/common';
import { ApiModule } from './api/api.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
#Module({
imports: [ApiModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
You have declared AfipUrls as a dependency for UrlsAfipService but it is not provided in any module.
So you have to add AfipUrls to the providers array of your ApiModule. Then it can be injected.
providers: [UrlsAfipService, WsaaService, DescribeService, AfipUrls]
// ^^^^^^^^
Note though, that encoding environment specific values in your code base might be a code smell. Consider creating a ConfigService that encapsulates environment specific variables that are read from environment variables or .env files using dotenv. See this answer for more information.

Nest can't resolve dependencies of guard wrapped inside a decorator

I'm trying to inject a provider inside a guard that is wrapped in a decorator, but Nest is not being able to resolve dependencies, giving me the next error:
[ExceptionHandler] Nest can't resolve dependencies of the SecuredGuard (Reflector, ?). Please make sure that the argument at index [1] is available in the SecuredGuard context.
The main purpose of my approach is to avoid using two separate decorators like this:
#Controller()
export class AppController {
#Get()
#Secured('admin')
#UseGuards(SecuredGuard)
getHello(): string {
return this.appService.getHello();
}
}
And instead insert the #UseGuards(SecuredGuard) inside my custom decorator #Secured('admin') so it ends up like this:
#Controller()
export class AppController {
#Get()
#Secured('admin')
getHello(): string {
return this.appService.getHello();
}
}
This is how I'm implementing my custom decorator:
export function Secured(...roles: string[]){
const setMetadata = ReflectMetadata('roles', roles)
const setupGuard = UseGuards(SecuredGuard)
return (target: any, key?: string, descriptor?: any) => {
setMetadata(target, key, descriptor);
setupGuard(target, key, descriptor);
}
}
And this is my SecuredGuard, the AuthService is the dependency that couldn't be resolved:
#Injectable()
export class SecuredGuard implements CanActivate {
constructor(
private readonly _reflector: Reflector,
private readonly _authService: AuthService
) { }
async canActivate(context: ExecutionContext): Promise<boolean> {...}
}
Both secured.guard.ts and secured.decorator.ts are part of secured.module.ts
#Module({
imports: [
SecuredGuard,
AuthModule
],
exports: [
SecuredGuard
],
providers: [
AuthService
]
})
export class SecuredModule {}
Which is using the AuthService being exported from auth.module.ts
#Module({
controllers: [
AuthController
],
providers: [
AuthService
],
imports: [
EmailModule
],
exports: [
AuthService
]
})
export class AuthModule { }
And secured.module.ts is being imported by app.module.ts
#Module({
imports: [
SecuredModule
],
controllers: [
AppController
],
providers: [
AppService
],
})
export class AppModule { }
I don't know if I'm using the appropriate approach, or even if it's possible what I'm trying to do, any clues would be really appreciated!
In general, your solution seems to work, see this running example:
However, there are some mistakes in your module declarations:
1) In your AppModule, the AuthService is not available, since neither is the AuthModule imported directly or exported by the SecuredModule. That's why you're getting the error.
2) You don't have to declare your guards in any module, they will just be available globally. Only put modules in your imports array.
3) You're providing the AuthService multiple times, so you will have different instances of it. You should only provide it once and then only export (or re-export) your provider, but not provide it again.
4) ReflectMetadata was deprecated in v6; use SetMetadata instead.

Angular 6 Universal service provided in Injector needs another app injected variable

I am using Angular Universal. I have created a PlatformService to detect which platform I am currently working on.
/* platform.service.js */
import { Injectable, Inject, PLATFORM_ID } from '#angular/core';
import { isPlatformBrowser, isPlatformServer } from '#angular/common';
#Injectable({
providedIn: 'root'
})
export class PlatformService {
constructor(
#Inject(PLATFORM_ID) private platformId: Object
) {
this.platformId; // this is coming out undefined
}
isBrowser() {
return isPlatformBrowser(this.platformId);
}
isServer() {
return isPlatformServer(this.platformId);
}
}
I am creating a BaseComponent for common handling of my route binded components.
/* base.component.ts */
import { Component, OnInit, Inject } from '#angular/core';
import { InjectorHolderService } from '#core/services/util/injector-holder.service';
import { PlatformService } from '#core/services/util/platform.service';
#Component({
selector: 'app-base',
template: '',
})
export class BaseComponent implements OnInit {
protected platformService: PlatformService;
constructor() {
this.platformService = InjectorHolderService.injector.get(PlatformService);
console.log(this.platformService);
}
}
Since this component will be inherited by many components, I didn't want to pass the PlatformService through super(). So I decided to go with creating an Injector.
/* app.module.ts */
import { InjectorHolderService } from '#core/services/util/injector-holder.service';
import { PlatformService } from '#core/services/util/platform.service';
#NgModule({ ... })
export class AppModule {
constructor() {
InjectorHolderService.injector = Injector.create({
providers: [
{
provide: PlatformService,
useClass: PlatformService,
deps: [], // I think i need to put something here, but not sure.
}
]
});
}
}
And a service which can hold all the injected module for future use.
/* injector-holder.service.ts */
import { Injectable, Injector } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class InjectorHolderService {
static injector: Injector;
}
But #Inject(PLATFORM_ID) private platformId: Object is giving out undefined, because of which I am not able to detect the platform.
What am I missing here? or If there is a better approach to achieve the above functionality.
Please let me know if you guys need to see any other file.
I am not sure whether the following approach is good or bad, currently, it is the only thing working for me. Would love to hear any new approach to it.
Since PlatformService needed #Inject(PLATFORM_ID) which is provided only from AppModule, the new Injector I created was not able to find any value for #Inject(PLATFORM_ID) and hence undefined.
So, instead of using class PlatformService in Injector, now I am using PlatformService's instantiated object and hence was able to access everything fine in BaseComponent.
Modified my AppModule like following:
/* app.module.ts */
import { InjectorHolderService } from '#core/services/util/injector-holder.service';
import { PlatformService } from '#core/services/util/platform.service';
#NgModule({ ... })
export class AppModule {
constructor(
private platformService: PlatformService,
) {
InjectorHolderService.injector = Injector.create({
providers: [
{
provide: PlatformService,
useValue: this.platformService, // notice the change of key, using value not class
deps: [],
}
]
});
}
}
Will try to add a minimal repo to recreate this issue and share with you guys, If anyone wants to explore more.

typescript in angular2, how to access class variables through this

Given this code, how can I access to the object "sessions"? it fails due to "this" being null:
/// <reference path="chrome/chrome-app.d.ts" />
import { Component, Input, OnInit } from '#angular/core';
import { AppService } from './app.service';
#Component({
selector: 'tabs',
templateUrl: './templates/app.html',
providers: [ AppService ]
})
export class AppComponent implements OnInit {
public sessions : Object;
constructor( private appService : AppService ) {}
getBookmarkLists() {
console.log(this.sessions) // it gives undefined
this.sessions['test'] = 'yea'; // it fails
this.appService.getBookmarks().then(function(bookmarks : any) {
console.log(this.sessions) // it fails
});
}
ngOnInit() {
this.getBookmarkLists();
}
}
What I would expect is to be able to access to the variable and populate it.
You didn't initialized this Sessions object anywhere, should be as far I know:
export class AppComponent implements OnInit {
public sessions: Session[] = []; // You forgot here to initialize it
constructor( private appService : AppService ) {}
getBookmarkLists() {
console.log(this.sessions) // no it shouldn't give undefined
this.sessions['test'] = 'yea'; // and this shouldn't fail
this.appService.getBookmarks().then((bookmarks : any) => {
// this should be an arrow function or function bound to use it
// otherwise this will point to the function itself.
console.log(this.sessions) // it shouldn't fail
});
}
ngOnInit() {
this.getBookmarkLists();
}
}
with the sessions = []; being the crucial part.
So it's not only an issue of this which references the class instance in methods as it should.
The callback passed to the then should be an arrow function not a classic function, to keep the this reference to the class instance.

Categories

Resources