I have a service for authentication based on JWT. To reuse this service in all my projects i created a library which should be shipped with npm.
For this service to work i need some API-Calls. In every project the API could look completely different so i don't want to provide this functionality inside my library instead inject another service which handles my API-Calls.
My idea was to create a module which contains my service and provide an interface to describe the service for API-Calls and inject it forRoot. The Problem is that my api service has some dependencies like HttpClient and i cannot simple instantiate it in my app.module.
My library looks like:
auth.module.ts
import { NgModule, ModuleWithProviders, InjectionToken } from '#angular/core';
import { AuthService } from '../services/auth.service';
import { AuthAPI } from '../models/authAPI';
import { AuthapiConfigService } from '../services/authapi-config.service';
#NgModule()
export class AuthModule {
static forRoot(apiService: AuthAPI): ModuleWithProviders {
return {
ngModule: AuthModule,
providers: [
AuthService,
{
provide: AuthapiConfigService,
useValue: apiService
}
]
};
}
}
auth-api.interface.ts
import { Observable } from 'rxjs';
export interface AuthAPI {
reqLogin(): Observable<{ access_token: string; }>;
reqRegister(): Observable<{ access_token: string; }>;
}
auth-api-config.service.ts
import { InjectionToken } from '#angular/core';
import { AuthAPI } from '../models/authAPI';
/**
* This is not a real service, but it looks like it from the outside.
* It's just an InjectionTToken used to import the config object, provided from the outside
*/
export const AuthapiConfigService = new InjectionToken<AuthAPI>('API-Service');
auth.service.ts
constructor(#Inject(AuthapiConfigService) private apiService) {}
How i am trying to implement it:
auth-rest-service.ts
import { Injectable } from '#angular/core';
import { AuthAPI } from 'projects/library-project/src/lib/auth/models/authAPI';
import { Observable } from 'rxjs';
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class AuthRestService implements AuthAPI {
constructor(private http: HttpClient) {}
reqLogin(): Observable<{ access_token: string; }> {
return this.http.post<{access_token: string}>(`/login`, 'test');
}
reqRegister(): Observable<{ access_token: string; }> {
return this.http.post<{access_token: string}>(`/login`, 'test');
}
}
app.module.ts
import { AuthRestService } from './components/auth-service/auth-rest.service';
#NgModule({
declarations: [
...
],
imports: [
...
AuthModule.forRoot(AuthRestService),
...
],
providers: [AuthModule],
bootstrap: [AppComponent]
})
export class AppModule { }
I can't create an instance of AuthRestService because of the dependencies this service has (HttpClient). Is there any method to tell angular to provide me this service.
This is possible with usage of angular's Injector.
import { Injector, ModuleWithProviders, NgModule, Optional, Provider, SkipSelf } from '#angular/core';
import { isFunction } from 'lodash';
export function resolveService(cfg: SharedConfig, inj: Injector): IncompleteService {
const provider = cfg?.service;
// if service is an angular provider, use Injector, otherwise return service instance as simple value
const service = isFunction(service) ? inj.get(provider) : provider;
return service;
}
/**
* Service to be implemented from outside the module.
*/
#Injectable()
export abstract class IncompleteService {
abstract strategyMethod();
}
// Optional: A config object is optional of course, but usually it fits the needs.
export interface SharedConfig {
service: IncompleteService | Type<IncompleteService> | InjectionToken<IncompleteService>;
// other config properties...
}
/*
* Optional: If a Config interface is used, one might resolve the config itself
* using other dependencies (e.g. load JSON via HTTPClient). Hence an InjectionToken
* is necessary.
*/
export const SHARED_CONFIG = new InjectionToken<SharedConfig>('shared-config');
// Optional: If SharedConfig is resolved with dependencies, it must be provided itself.
export type ModuleConfigProvider = ValueProvider | ClassProvider | ExistingProvider | FactoryProvider;
/**
* One can provide the config as is, i.e. "{ service: MyService }" or resolved by
* injection, i.e.
* { provide: SHARED_CONFIG: useFactory: myConfigFactory, deps: [DependentService1, DependentService2] }
*/
#NgModule({
declarations: [],
imports: []
})
export class SharedModule {
static forRoot(config: SharedConfig | ModuleConfigProvider): ModuleWithProviders<SharedModule> {
// dynamic (config is Provider) or simple (config is SharedConfig)
return {
ngModule: SharedModule,
providers: [
(config as ModuleConfigProvider).provide ? (config as Provider) : { provide: SHARED_CONFIG, useValue: config },
{ provide: IncompleteService, useFactory: resolveService, deps: [SHARED_CONFIG, Injector] },
// ... provide additional things
],
};
}
/**
* In general not really useful, because usually an instance of IncompleteService
* need other dependencies itself. Hence you cannot provide this instance without
* creating it properly. But for the sake of completeness, it should work as well.
*/
#NgModule({
declarations: [],
imports: []
})
export class MostSimpleSharedModule {
static forRoot(service: IncompleteService): ModuleWithProviders<SharedModule> {
// dynamic (config is Provider) or simple (config is SharedConfig)
return {
ngModule: SharedModule,
providers: [
{ provide: IncompleteService, useValue: service },
// ... provide additional things
],
};
}
EDIT
If you really need an interface iso. an (injectable) abstract class IncompleteService, you just need to define another InjectionToken<IncompleteServiceInterface> and provide this token explicitly.
Related
I am trying to inject a service from one module to another module. I am keep on getting
Error: Nest can't resolve dependencies of the AwsSnsService (?). Please make sure that the argument function() {\n if (klass !== Object) {\n return klass.apply(this, arguments);\n }\n } at index [0] is available in the AwsModule context.
Below is the code
aws-sns.service.ts
#Injectable()
export class AwsSnsService {
constructor(private readonly awsSNS: AWS.SNS) {}
}
aws.module.ts
import { Module } from '#nestjs/common';
import { AwsSnsService } from './aws-sns.service';
#Module({
providers: [AwsSnsService],
exports: [AwsSnsService],
})
export class AwsModule {}
I want to use the AwsSnsService in my User module. I am doing it in a following way
#Module({
imports: [
SomeOtherModule,
AwsModule,
],
providers: [UserService, UserDevicePushTokensService],
exports: [UserService, UserDevicePushTokensService],
})
export class UserModule {}
#Injectable()
export class UserDevicePushTokensService {
constructor(private readonly awsSnsService: AwsSnsService) {}
}
Looks to me the dots are connected in the proper way. Still I am not figuring it out.
You must resolve dependency for AWS.SNS in AwsSnsService
Example with my s3, the same for SNS and any service:
s3.provider.ts
export const s3Providers: Provider = {
provide: S3_PROVIDER_KEY,
useFactory: async (): Promise<S3> => {
return new S3({
accessKeyId: "Key",
secretAccessKey: "AccessKey",
});
},
};
s3.service.ts
#Injectable()
export class S3Service {
constructor(
#Inject(S3_PROVIDER_KEY)
private readonly s3: S3) {}
}
You can read more here
Nestjs custom provider
I want to ensure a singleton service is created on application boot. I could add it as injection parameter to my AppComponent and not use it at all, but that looks a bit dirty. Right now I'm going with this solution:
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '#angular/core';
import { NavigationService } from './navigation.service';
#NgModule()
export class NavigationServiceModule {
public static forRoot(): ModuleWithProviders<NavigationServiceModule> {
return {
ngModule: NavigationServiceModule,
providers: [
{
provide: APP_INITIALIZER,
deps: [NavigationService],
multi: true,
useFactory: () => () => { }
}
]
}
}
}
But don't really love it, too. Any ideas how this could be achieved best?
Just use the default service setup and don't add it to the providers array - only one instance will be created (unless you explicitly provide it outside of constructors)
#Injectable({
providedIn: 'root'
})
export class NavigationService
How do I Access AppModule imports from Lazy-loaded Modules ?
My Angular10 App imports AngularMaterial and NXTranslate Modules in to the AppModule.
NxTranslate calls an ApiService to get a large Lookup object of thousands of translations.
This is translated at the initial loading of the AppModule.
The App has multiple lazy-loaded routes that also need to use the AnagularMaterial and NXTranslate Modules in their features.
If I use a SharedModule to load the Modules then the ApiService is called multiple times. This is obviously not good.
It should only call the ApiService & AngularMaterial once and be available for all modules.
How do I resolve this? I am struggling.
Thanks.
Update
(sorry for the long post)
This is the NXTranslate implementation - it uses a custom class.
import { environment } from './../../../../environments/environment';
import { OSCITranslateService } from './translate.service';
import { NgModule, Injector } from '#angular/core';
import { CommonModule } from '#angular/common';
import {TranslateLoader, TranslateModule} from '#ngx-translate/core';
import {TranslateHttpLoader} from '#ngx-translate/http-loader';
import {HttpClient, HttpClientModule} from '#angular/common/http';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
export class CustomLoader implements TranslateLoader {
localeResourcesUrl =
`${environment.baseUrl}${environment.apiUrl.localeResources}`;
constructor(private http: HttpClient) {}
getTranslation(lang: string): Observable<any> {
let options;
const uri = `${this.localeResourcesUrl}${options && options.key ?
'/' + options.key : ''}`;
let mapped = this.http.get(uri).pipe(
map((response: any) => {
let localeData = {};
let languageCode = response?.languageVariantCode;
response.resources.forEach(item => {
localeData[item.keyName] = item.keyValue;
});
return localeData;
})
);
return mapped;
}
}
#NgModule({
declarations: [],
imports: [
CommonModule,
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: CustomLoader,
deps: [HttpClient]
}
})
],
exports: [ TranslateModule ]
})
export class NxTranslateModule {
constructor(private http: HttpClient) {
}
}
This is the sharedRootModule that imports the AngularMaterial & NXTranslate
import { SharedModule } from './shared.module';
import { NgModule, ModuleWithProviders } from '#angular/core';
#NgModule({
})
export class SharedRootModule {
static forRoot(): ModuleWithProviders<SharedModule> {
return {
ngModule: SharedModule
};
}
}
In AppModule SharedRootModule is imported
...
#NgModule({
declarations: [
AppComponent
],
imports: [
...
SharedRootModule.forRoot()
],
exports: [
...
SharedRootModule
]
....
Are you concerned about the multiple ApiService instances you might end up with? Provide the ApiService within AppModule only, or even better, use the providedIn property right in your service's decorator so it gets injected at application level. (https://angular.io/api/core/Injectable#providedIn)
I would just use a SharedModule that exports the mentioned lazy loaded modules.
I have an injectable authentication service written for Angular 4. The code looks similar to the following:
auth.service.ts
import { CookieService } from 'ngx-cookie';
import { Identity } from './auth.identity';
export function authInit(authService: AuthService): () => Promise<any> {
return (): Promise<any> => authService.checkAuthenticated();
}
#Injectable()
export class AuthService {
identity: Identity;
isAuthenticated:boolean = false;
apiUrl: string = 'https://myUrl/api';
constructor(private _http: HttpClient, private _cookieService: CookieService) {
this.identity = new Identity();
}
checkAuthenticated(): Promise<any> {
return new Promise((res, rej) => {
let identity = this._cookieService.getObject('myToken');
if (!!identity) {
this.setAuthenticated(identity);
}
});
}
login(username: string, password: string) {
let creds = {
username: username,
password: password
};
this._http.post<any>(this.apiUrl + '/auth/login', creds).subscribe(data => {
this.setAuthenticated(data);
});
}
logout() {
}
private setAuthenticated(data: any) {
this._cookieService.putObject('myToken', data);
this.isAuthenticated = true;
// hydrate identity object
}
}
auth.module.ts
import { NgModule, APP_INITIALIZER } from '#angular/core';
import { CommonModule } from '#angular/common';
import { AuthService, authInit } from './auth.service';
#NgModule({
imports: [CommonModule],
providers: [
AuthService,
{
provide: APP_INITIALIZER,
useFactory: authInit,
deps: [AuthService],
multi: true
}
]
})
export class AuthModule { }
The idea is that when the app loads, I want to be able to check the local storage (cookies, sessionStorage or localStorage) to see if the value exists. (This is demonstrated by the commented if statement in the constructor.) Based on the isAuthenticated property I want to be able to show specific content.
Currently, if I uncomment the lines in the constructor, I'll get an exception document.* is not defined. I know what that means. Unfortunately, I don't know how to accomplish what I'm going for.
Keep in mind, this is a service and not a view component, so there's no ngOnInit method available.
EDITED
So I've added the factory provider as suggested. However, I'm still getting the exception: document is not defined
Thanks!
When you have a service that you need to have run before everything else might be initialized you can use the APP_INITIALIZER token (the documentation is sparse to say the least :)
The gist is that in your application providers array you add a factory provider:
{
provide: APP_INITIALIZER,
useFactory: authInit,
deps: [AuthService],
multi: true
}
Make sure to have provide set specifically to APP_INITIALIZER and the multi value to true. The authInit function is factory that returns a function that returns a promise. It has to return a promise and not an observable. It would be something like:
export function authInit(authServ: AuthService) {
return () => authServ.check();
}
The authServ.check() function is where you can put the logic you currently have commented in your service (just make sure it returns a promise as the result). Setting it up this way will let that logic run while the application loads.
Edit: Now that I take a look at the app.module.ts add the initialization of the cookie service and add the BrowserModule:
import { NgModule, APP_INITIALIZER } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { CommonModule } from '#angular/common';
import { CookieModule } from 'ngx-cookie';
import { AuthService, authInit } from './auth.service';
#NgModule({
imports: [BrowserModule, CommonModule, CookieModule.forRoot()],
providers: [
AuthService,
{
provide: APP_INITIALIZER,
useFactory: authInit,
deps: [AuthService],
multi: true
}
]
})
export class AuthModule { }
Also, make sure to add ngx-cookie to your systemjs.config.js (if that's what you're using as your loader).
Angular 2 - When injecting(importing) a service into my constructor I get a "No provider" error.
Here's my service I'm importing:
export class EnvironmentService {
public baseurl: string = "/baseurl/example/";
}
Here's my class with the error:
import {Http, Headers} from "angular2/http";
import {Injectable} from 'angular2/angular2';
import {EnvironmentService} from './environmentService';
#Injectable()
export class AuthService {
constructor(private _environmentService: EnvironmentService) {
//This is where the error is.
console.log(this._environmentService);
}
isUserLoggedIn = (): Promise<boolean> => {
return new Promise((resolve, reject) => {
resolve(true);
})
}
}
Add AuthService in app.module.ts Providers
Most likely you didn't specify EnvironmentService as a provider for your application. You can do it by passing all providers in a second argument of the bootstrap function, or using providers and viewProviders properties of the Component decorator:
bootstrap(AppComponent, [EnvironmentService /*, other providers */]);
// or
#Component({
// ... component config
providers: [EnvironmentService],
// or
viewProviders: [EnvironmentService]
})
class AppComponent { /* ... */ }
GO to the module that you want to use the EnvironmentService i.e app.module.ts or other modules.
after imports
you will see provider as an array
add the EnvironmentService
providers: [AuthService]