Pass variable to module using forRoot - javascript

I've developed a library with shared components in Angular and I want to pass configuration there.
Everything is basically the same as in:
Pass config data using forRoot
Problem is, that I need to pass user to this module, which is fetched on start of application and saved in Redux state.
Is it possible to pass observable with user using forRoot while importing module? Maybe it's possible to pass something to this module 'later' with some lazy loading?
#select() private user$: Observable<User>
#NgModule({
imports: [
LibModule.forRoot({
user: user$
})
...
})

I've made it another way - by injecting my implementation of abstract service for getting settings. Code below:
Lib module declaration:
export interface LibSettings {
user: User;
url: string;
}
export abstract class AbstractLibSettingsService {
abstract getSettings(): LibSettings;
}
#NgModule({
declarations: [...],
imports: [...],
exports: [...]
})
export class LibModule {
static forRoot(implementationProvider: Provider): ModuleWithProviders {
return {
ngModule: LibModule,
providers: [
implementationProvider
]
}
}
}
Lib service, where I needed the user:
constructor(private settingsService: AbstractGlassLibSettingsService) {
}
In application that uses the lib, module declaration with import:
export const LIB_SETTINGS_PROVIDER: ClassProvider = {
provide: AbstractLibSettingsService,
useClass: LibSettingsService
};
#NgModule({
imports: [...
LibModule.forRoot(LIB_SETTINGS_PROVIDER)
],
...
})
Finally, the implementation of the service in application:
#Injectable()
export class LibSettingsService extends AbstractLibSettingsService {
#select() private user$: Observable<User>;
private user: User;
constructor() {
super();
this.user$.subscribe(user => this.user = user);
}
public getSettings(): GlassLibSettings {
...
}

Related

Nest can't resolve dependencies to exported service

I'm trying to export and import a service in NestJS. It seems easy and I thought it should work like this but I got an error saying that Nest can't resolve the dependencies.
SettingsModule
This module has the service that should be imported, and exports it.
#Module({
imports: [
MongooseModule.forFeature([{ name: Setting.name, schema: SettingSchema }]),
],
providers: [SettingsService],
exports: [SettingsService],
})
export class SettingsModule {}
MsgraphModule
This module should import the service through the module because the service is injected in their service.
#Module({
imports: [SettingsModule],
providers: [MsgraphService],
})
export class MsgraphModule {}
AppModule
#Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/lead-import', {
useCreateIndex: true,
}),
MsgraphModule,
SettingsModule,
...
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
What am I doing wrong here?
The problem was that I used the #Inject() decorator which is only needed for custom dependency injections.
#Injectable()
export class MsgraphService {
private client: Client;
private authenticator;
constructor(#Inject() private settingsService: SettingsService) {
this.init();
this.authenticator = new MSGraphAuthenticator();
}
...
}
So removing the #Inject() did the trick.

Inject correct service based on parameter

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();
}
}
}

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.

Initialize Guard with value

Is it possible to initialize guard with a specifig value ?
For example the current example will not work:
#Module({
imports: [
CoreModule,
],
providers: [
{
provide: AuthGuard, // while using APP_GUARD works
useFactory: (configService: ConfigService) => {
return new AuthGuard(configService.get('some_key'));
},
inject: [ConfigService],
},
],
})
While using APP_GUARD for provide will initialise the guard with config value. So it works only for global scope, but not for #UseGuards(AuthGuard)
This doesn't work because guards are not registered as providers in a module. They get directly instantiated by the framework.
You can either use dependency injection in the guard:
#Injectable()
export class MyAuthGuard {
constructor(private readonly configService: ConfigService) {
// use the configService here
}
}
and
#UseGuards(MyAuthGuard)
or instantiate the guard yourself:
#UseGuards(new AuthGuard(configService.get('some_key')))
In the special case of the AuthGuard, you can set a defaultStrategy in the PassportModule. Then you can just use #UseGuards(AuthGuard())
PassportModule.register({ defaultStrategy: 'jwt'})
or async:
PassportModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ defaultStrategy: configService.authStrategy}),
inject: [ConfigService],
})
Let's say you want your specific guard instance to perform differently depending on some input, basically be able to configure it. There is no option to consume this config from constructor(). Factory way might look like a bit bulky solution. But you're still able to utilise static methods to achieve wanted behaviour.
Example:
#Injectable()
class SomeController {
#Get()
#UseGuard(AuthGuard) // but how to pass smth inside AuthGuard?
public async doSomething() {}
}
Solution:
// [auth.guard.ts] file
import { UnauthorizedException, Injectable } from '#nestjs/common';
import type { CanActivate, ExecutionContext } from '#nestjs/common';
import type { GuardOptions, PatchedRequest } from './auth.types';
export interface GuardOptions {
allowAnonymous?: boolean,
allowExpired?: boolean,
}
#Injectable()
export class AuthGuard
implements CanActivate {
public options: GuardOptions = {};
public canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> {
// Guard logic
return true;
}
static configure(options: GuardOptions) {
const instance = new AuthGuard;
instance.options = options;
return instance;
}
}
// [someEntity.controller.ts] file
// imports...
#Injectable()
class SomeController {
#Get()
#UseGuard(AuthGuard.configure({ allowExpired: true })) // voila
public async doSomething() {}
}
Enjoy! Glory to Ukraine!
I would try ht less verbose approach and inject ConfigService directly into the AuthGuard in such a manner:
#Module({
imports: [
CoreModule,
],
providers: [
AuthGuard,
],
exports: [
AuthGuard,
],
})
#Injectable()
export default class AuthGuard {
constructor (protected readonly config: ConfigService) {
}
/*
...
*/
}

Use fetched config in forRoot

I'm writing an angular app which uses #ngx-translate. With TranslateModule.forRoot(...) i provide a TranslateLoader:
#NgModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient, ConfigService, LogService]
}
})
]
})
Also i have a ConfigService which loads an config.json utilizing APP_INITIALIZER.
The problem is, the TranslateLoader needs an url from the config. But forRoot() runs before APP_INITIALIZER which leads to ConfigService not having loaded the config and an empty url.
Is there another way to do this?
Currently i'm thinking about manually bootstrapping angular.
For anyone still looking at this, I found a way to load translations using the TranslateLoader provider after App Init. The ngx-translate lib allows you to override the current loader. So we can pass a new factory after the app has completed bootstrapping.
export function HttpLoaderFactory(handler: HttpBackend, valueAvailableAfterInit) {
const http = new HttpClient(handler);
return new TranslateHttpLoader(http, valueAvailableAfterInit, '.json');
}
export class AppModule {
constructor(
private translate: TranslateService,
private handler: HttpBackend
) {}
ngDoBootstrap() {
const valueAccessableAfterBootstrap = `I'll leave this to your use-case. For me it is an environment variable overwritten via ngOnInit in app.component`;
this.translate.currentLoader = HttpLoaderFactory(this.handler, valueAccessableAfterBootstrap); // replace loader
this.translate.reloadLang('en-US').pipe(take(1)).subscribe(); // reload translations with new loader
}
}
I think the following solution is simpler, since you don't need to reload the translations:
#NgModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient, AppConfigService],
},
})
]
})
export function HttpLoaderFactory(http: HttpClient, appConfigService: AppConfigService) {
return new CustomTranslateHttpLoader(http, appConfigService, '', `?v=${environment.version}`);
}
export class CustomTranslateHttpLoader extends TranslateHttpLoader {
constructor(http: HttpClient,
private readonly appConfigService: AppConfigService,
prefix?: string,
suffix?: string) {
super(http, prefix, suffix);
}
override getTranslation(lang: string): Observable<Object> {
this.prefix = `${this.appConfigService.getConfig().apiUrl}api/translations/`;
return super.getTranslation(lang);
}
}
You can retrieve your config values after the app is initialized, as soon as getTranslation is called.
You could also resolve the translation service and make the http call to the endpoint. This only works if your services resolve the api url as well.
export class CustomTranslateHttpLoader extends TranslateLoader {
constructor(private readonly injector: Injector) {
super();
}
override getTranslation(lang: string): Observable<Object> {
const translationService = this.injector.get(TranslationsService);
return translationService.getAllTranslations(lang, environment.version)
.pipe(map(response => {
return response as Object;
}));
}
}

Categories

Resources