I am new to both NestJS & RxJS. I'm working very hard to write things in an idiomatic RxJS, since one of the purposes of this project is to learn both of them much better, rather than trying to bypass or hack around their APIs.
Anyway, so I have a piece of logic where I am looking up a JWKSet from an OAuth2 server. I use the NestJS HttpService for that, which returns an Observable. I then use that observable to set the result to a ReplaySubject. Then, in my JwtStrategy, I use the secretOrKeyProvider function to subscribe to the ReplaySubject in order to get the value each time an HTTP request comes in.
Now, I'm sure there are tons of things wrong with this approach, since I just barely understand how to work with RxJS. My biggest issue is the last part. When I subscribe to the ReplaySubject in the secretOrKeyProvider function, I immediately unsubscribe. This is because I want to cleanup leftover subscriptions.
Overall, this feels very wrong to me, to subscribe and immediately unsubscribe. I feel like I am doing something wrong. I am seeking a review of this code to learn what improvements I can make.
While all the code below works, my goal is to be guided into making better, more proper use of RxJS.
#Injectable()
export class JwkService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(JwkService.name);
readonly key = new ReplaySubject<string>();
private subscription: Subscription;
constructor(
private httpService: HttpService,
private configService: ConfigService
) {}
onModuleInit(): void {
this.subscription = this.httpService
.get(`${this.configService.get<string>(AUTH_SERVER_HOST)}${jwkUri}`)
.pipe(
map((res: AxiosResponse<JwkSet>) => jwkToPem(res.data.keys[0]))
)
.subscribe({
next: (key) => this.key.next(key),
error: (error: Error) => {
this.logger.error(
'CRITICAL ERROR: Unable to load JWKSet',
ajaxErrorHandler(error)
);
}
});
}
onModuleDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
private readonly logger = new Logger(JwtStrategy.name);
constructor(
private readonly jwkService: JwkService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKeyProvider: (
req: Request,
rawJwt: string,
done: doneFn
) => {
jwkService.key
.subscribe({
next: (value: string) => done(null, value),
error: (error: Error) => {
this.logger.error(
'Error getting JWK key',
ajaxErrorHandler(error)
);
done(error);
}
})
.unsubscribe();
}
});
}
}
If you don't need unsubscribe, RxJS gives you a few pipes for that:
.get(`${this.configService.get<string>(AUTH_SERVER_HOST)}${jwkUri}`)
.pipe(
take(1),
map((res: AxiosResponse<JwkSet>) => jwkToPem(res.data.keys[0]))
)
...
or
.get(`${this.configService.get<string>(AUTH_SERVER_HOST)}${jwkUri}`)
.pipe(
first(),
map((res: AxiosResponse<JwkSet>) => jwkToPem(res.data.keys[0]))
)
...
first(), take(1) pipes do unsubscribe for you if takes first value. If value in any cases don't come, then you can unsubscribe your subscription manually, or just complete the subject.
Related
I have implemented FireDatabaseService as #Injectable({ providedIn: 'root' }) in my app. It of course works with DI. In this service I need Firebase UID from another service, but this UID is being fetched with subscription. Because of this methods that need UID are called before this UID is available. I have no idea how to get UID before getUserGroup() is called. Here is my service:
...
#Injectable({ providedIn: 'root' })
export class FireDatabaseService {
uid: string;
constructor(
private readonly db: AngularFireDatabase,
private readonly authService: AuthService
) {
this.authService.authState.subscribe(
(state: firebase.User) => this.uid = state.uid
);
}
getUserGroup(): Observable<string> {
return this.db
.object(`users/${this.uid}`)
.valueChanges()
.pipe(map((userData: any) => userData?.group as string));
...
}
Calling fireDatabaseService.getUserGroup() goes with uid = ''. Is there any possibility to fix it? Thanks for help.
It is a little unclear to me what your requirements are. The easiest way to accomplish what I think you are asking would be the following:
...
#Injectable({ providedIn: 'root' })
export class FireDatabaseService {
constructor(
private readonly db: AngularFireDatabase,
private readonly authService: AuthService
) {
}
getUserGroup(): Observable<string> {
return this.authService
.authState
.pipe(
switchMap((state: firebase.User) =>
this.db
.object(`users/${state.uid}`)
.valueChanges()
),
map((userData: any) => userData?.group as string)
);
...
}
Everytime getUserGroup is called, there would be another call to the authService. That might be undesireable, but that could be mitigated through the use of shareReplay(1) or through another strategy depending on the functionality you are expecting.
I suggest that you do not subscribe at all in your service, but rather define your uid and userGroup as observables. The userGroup$ can be defined from the uid$ observable:
#Injectable({ providedIn: 'root' })
export class FireDatabaseService {
uid$ = this.authService.authState.pipe(
map(state => state.uid)
);
userGroup$ = this.uid$.pipe(
switchMap(uid => this.db.object(`users/${uid}`).valueChanges())
map(userData => userData?.group as string)
);
constructor(
private readonly db: AngularFireDatabase,
private readonly authService: AuthService
) { }
}
Observables are lazy, so it is not necessary to wrap them as functions. They don't emit a value until .subscribe() is called on them.
I'm implementing an interface that has a function that returns Observable.
I also need to pass some value to the Observable, but it may take some time to receive that value.
How can I still return the Observable and also make it wait for the needed value?
To be more specific, I'm implementing an HttpInterceptor and I want to set a token to the request header.
The token value could be unavailable, so need to wait a little (asynchronously) and try again, until the value is received.
Then set the token in the request header and continue.
How can I implement such mechanism?
#Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {
constructor(private tokenService: HttpXsrfTokenExtractor) { }
getToken(callback) {
let token = this.tokenService.getToken();
if (!token) {
// a valid token wasn't received. wait a little and try again
setTimeout(() => {
this.getToken(callback); //recursive call
}, 1000);
} else {
// found valid token
callback(token);
}
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// this part should set req when a token is received, but it is asynchronous
this.getToken((token) => {
req = req.clone({headers: req.headers.set('X-XSRF-TOKEN', token)});
});
// this returns Observable. I must return Observable, but req is not ready at this point
return next.handle(req);
}
}
The easiest thing to do is use RxJs operators. Using switchMap should be a good solution here. Essentially in this case, switchMap allows you to chain dependent observables together and only return the inner observable. It should look something like this:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return getToken.pipe(
switchMap(token => {
req = req.clone({headers: req.headers.set('X-XSRF-TOKEN', token)});
return next.handle(req);
}
);
}
Please note, You'll need to adjust your getToken to return an observable as well in order for this to work.
Looking at the code, it seems that we have callback from getToken and observable from intercept. It is better to always use observable if possible.
We could convert getToken(callback) to use observable. RxJS has retryWhen operator that we could use to handle retry.
getToken() {
const tokenFromService = of(this.tokenService.getToken()); // convert to observable
return tokenFromService
.pipe(
map(token => {
if (!token) {
throw new Error('token is not specified'); // it will be caught by retryWhen
}
return token;
}),
retryWhen(error => {
return error
.pipe(
tap(() => console.log('error happened, retry request token')),
delay(1000)
)
})
)
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.getToken()
.pipe(
switchMap(token => {
const modifiedReq = req.clone({headers: req.headers.set('X-XSRF-TOKEN', token)});
return next.handle(modifiedReq);
})
)
}
Hope it helps
Reference:
retryWhen
I use Nx to create a new application using NgRx and DataPersistence.
I have generated several states in my application (example: State A, State B).
To retrieve the data via an API request to populate State A, I use in the effect associated with this.dataPersistence.fetch.
However to launch my API request, I need data contained in State B.
This is where I block.
I saw that by using the basic effects, we can use the "withLastestFrom" operator to retrieve a different state than the one currently used.
#Injectable()
export class ElementEffects {
constructor (private store: Store<any>, private apiService: ApiService) {}
#Effect()
public yourEffect: Observable<Action> = this.actions$.pipe(
ofType<yourActionClass>(ActionsEnum.YOUR_ACTION),
withLatestFrom(this.store.pipe(select(selectSomethingFromTheStore))),
concatMap(([action, selectedDateFromTheStore]) => this.apiService.doBackendCall(selectedDateFromTheStore, action.payload).pipe(
map(([resultFromTheBackendCall, selectedDateFromTheStore]) => {
// Do Stuff
},
catchError((error) => of(new FailureAction(error)))
)
),
);
}
However I do not know how to handle that with this.dataPersistence.fetch.
By opening the DataPersistent code (https://github.com/nrwl/nx/blob/master/packages/angular/src/runtime/nx/data-persistence.ts), I see that the fetch feature is already using the withLatestFrom operator. I do not see how to recover another State.
fetch<A extends Action = Action>(
actionType: string,
opts: FetchOpts<T, A>
): Observable<any> {
return this.actions.pipe(
ofType<A>(actionType),
withLatestFrom(this.store),
fetch(opts)
);
}
My question is therefore:
How to recover State B when I use a State A effect?
Hoping to be clear enough :)
Here's an example from data-persistence.d.ts
#Injectable()
class TodoEffects {
#Effect() loadTodo = this.s.navigation(TodoComponent, {
run: (a, state) => {
console.log(state); // Here you should see all the states in your app
return this.backend.fetchTodo(a.params['id']).map(todo => ({
type: 'TODO_LOADED',
payload: todo
}));
},
onError: (a, e: any) => {
// we can log and error here and return null
// we can also navigate back
return null;
}
});
constructor(private s: DataPersistence<TodosState>, private backend: Backend) {}
}
Yes fetch will return the entire state store.
So you will need to pick what part (slice) of the store you want to use:
const state1 = state['state1'];
const state2 = state['state2'];
As withLatestFrom pull the "latests" state of the store.
I want to test a method inside of an Angular 2 component that is subscribed to an observable that is returned from a method in a service. Here is the code for the service method in summary:
public create(user: User): Observable<any> {
return this.http.post(this._api.create,
JSON.stringify(user), {
headers: this.apiConfig.getApiHeaders()
}).map((res: Response) => res.json());
}
It's easy to unit test this method because it returns an observable so I can just subscribe to it. But I want to test the method in the component that is already subscribed to this:
public onSubmit(user: User): void {
this._authentication.create(user).subscribe((token) => {
localStorage.setItem('token', token);
this.router.navigate(['/Home']);
});
}
Heres my spec so far but when I try to spyOn the localStorage.setItem it comes back as not being called. My understanding is it's probably checking to see if it's been called before it's actually been called.
it('Should login a user and on success store a token in localStorage',
injectAsync([TestComponentBuilder], (tcb) => {
return tcb.createAsync(Login).then((fixture) => {
let instance = fixture.debugElement.componentInstance;
localStorage.clear();
spyOn(localStorage, 'setItem');
instance.onSubmit({userId: 'some#email.com', password: 'password', siteName: 'sample'});
expect(localStorage.setItem).toHaveBeenCalled();
});
})
);
I'm wondering if I need to mock out the this._authentication.create method to return a new observable with a mock response in it?
After more research a few articles indicated that I do need to mock out the service and return an Observable.of() which runs synchronously to solve the problem, ill copy the code below. This however still doesn't work, I've been working on this most of the day, I don't feel this should be that hard, any help appreciated.
class MockAuthentication extends Authentication {
public create(user: Object): Observable<any> {
return Observable.of({'test': 'test'});
}
}
Ok so it's taken me most of the day but I finally cracked it. Instead of using the injectAsync and TestComponentBuilder to set up the spec I just need to use inject and inject the component in just like you do a service. This seems fine because I don't need to test anything in the view like events.
Heres the final spec that does work:
it('Should set token in localStorage, set the new user,
and navigate to home page on succesful login',
inject([Login], (login) => {
login.router.config([ { path: '/', name: 'Home', component: Home }]);
spyOn(localStorage, 'setItem');
spyOn(login._currentUser, 'set');
spyOn(login.router, 'navigate');
login.onSubmit({ userId: 'some#email.com', password: 'password', siteName: 'sample' });
expect(localStorage.setItem).toHaveBeenCalledWith('token', 'newToken');
expect(login._currentUser.set).toHaveBeenCalledWith({ 'test': 'one' });
expect(login.router.navigate).toHaveBeenCalledWith(['/Home']);
}));
Hope this might help someone in the future.
I guess you want to inject a mock Router instance to your component and then after navigate(['/Home']) was called on the mock Router, you check if localStorage.setItem(...) was called.
See my gist here.
Basically you can do several things here. First of all, stub your http call (I'm guessing from a service) with a simple observable response of the token (or other response) you want.
service.stub.ts
export class MyStub {
public create(user: User): Observable<User> {
return Observable.of('insert test token here');
}
// other stubbed methods ...
}
And then inside your test:
myComp.spec.ts
let comp: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let sst: ServiceStub;
describe('MyComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(OnboardFacilityNewComponent, {
set: {
providers: [
{ provide: MyService, useClass: ServiceStub },
]
}
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(MyComponent);
comp = fixture.componentInstance;
st = fixture.debugElement.injector.get(MyService);
});
}));
it('should submit new onboardFacility', fakeAsync(() => {
const sst = spyOn(sst, 'create').and.returnValue(
Observable.of('some token here')
);
comp.onSubmit(testUser);
fixture.detectChanges();
expect(comp.token).toEqual('some token here');
expect(spy.calls.any()).toEqual(true);
}));
});
Here, you can simply replace actual data with test data to test the behavior of your testing, rather then your testbed, your services, localStorage, etc. Obviously the test I wrote here assumes you would store the token returned from your service in your component, rather then localStorage (though there is a way to do that), but I'm just simply to show the concept rather then your specific use case.
In your use case you'll also need to the stub the router, which you can learn how to do here.
Since couple evening I've played with form validation in augular2.
All basic cases were easy to implement and they works fine but I stick with asynchronous validation. I have created a very tiny example http://plnkr.co/edit/Xo8xwJjhlHkXrunzS8ZE and it didn't work.
According to test "should fire an event after the status has been updated to pending" from model_spec.ts Registration via creation of control group suppose to work in a way
builder.group({login: ["",Validators.required,validationFuctionWhichReturnsPromise]
I spent a full evening to discovered that this code has been released in alfa-46 (and I used alfa-45) and after update depencies the async validation started to work. The feature is very fresh and is not fully documented but
(for those who haven't tried it yet) Basically async validator is a function which have a Control argument and return a promise which validation result. There are two ways to register a validator. 1) the one which I used in my example and 2) as a directive which Provide validators via NG_ASYNC_VALIDATORS (See UniqLoginValidator and NgFormControl to see how it work). You can compose more than one validator (not tested yet but functions to do this are in code, see https://github.com/angular/angular/commit/cf449dd).
But when I finally reach to up and running validators a new problem arrived. Async validator is perfect to used it in server side validation. But the validation is invoked after each change of model.fe after each keyup. So if we will send request to a server after each key up, it won't be too efficient way ;) I checked how it is done in angular 1 and they is a possibility to debounce validation events.
My questions are:
How to implement throttle or debounce with async validators? I saw some ideas but none of them were fine (mostly because they need to change angular code itself). Is there any valid way to do this without waiting for new angular release ?
I was thinking about to warping a validator function with debounce (from underscorejs) but it will not work because angular expects to get a valid promise every time.
My second though was that if all event use RxJs under the hood then maybe I can apply debounce on stream of event which is responsible for validation. In model.ts the promise returned from validator is change to observable and a new subscribed is added. We don't have any access to obs(Observable) to apply debounce there.
Is there any way or id to change,easy extend a control over the form validation ?
I spotted a close related problem in How to trigger Form Validators in angular2
PS there is other issue related to async validators and it is still open https://github.com/angular/angular/issues/1068
Here is a helper class that you can use to debounce all your async validators:
import {Component} from 'angular2/core';
import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import {Control} from 'angular2/common';
export class AsyncValidator {
_validate;
constructor(validator: (control: Control) => any, debounceTime = 1000) {
let source: any = new Observable((observer: Observer<Control>) => {
this._validate = (control) => observer.next(control);
});
source.debounceTime(debounceTime)
.distinctUntilChanged(null, (x) => x.control.value)
.map(x => { return { promise: validator(x.control), resolver: x.promiseResolver }; })
.subscribe(
(x) => x.promise.then(resultValue => x.resolver(resultValue),
(e) => { console.log('async validator error: %s', e); }));
}
private _getValidator() {
return (control) => {
let promiseResolver;
let p = new Promise((resolve) => {
promiseResolver = resolve;
});
this._validate({ control: control, promiseResolver: promiseResolver });
return p;
};
}
static debounce(validator: (control: Control) => any, debounceTime = 400) {
var asyncValidator = new this(validator, debounceTime);
return asyncValidator._getValidator();
}
}
Then all you have to do where use async validators is just wrap your validator with this call and write your validator the same as you would normally:
AsyncValidator.debounce(control => this.asyncValidator(control));
Here is an example usage:
export class AppComponent {
form: ControlGroup;
constructor(private _formBuilder: FormBuilder) {
var validator = AsyncValidator.debounce(control => this.asyncValidator(control));
this.form = _formBuilder.group({
name: ['', Validators.required, validator],
});
}
asyncValidator(control): any {
let p = new Promise(resolve => {
// get from server information need to validate control
if (control.value === 'valid value') {
resolve(null);
} else {
resolve({
asyncValidator: {
valid: false
}
});
}
});
return p;
}
}
There is an awesome issue on angular site that deals with the problem of both debouncing and switchMapping the validation:
https://github.com/angular/angular/issues/6895
This is mine working solution (but all the credit goes to guys from thread)
class AsyncValidator{
private validatorInput: Subject<string>;
private validatorChain: Observable<any>;
constructor(service: ManageUsersService) {
this.validatorInput = new Subject();
this.validatorChain = this.validatorInput
.debounceTime(400)
.distinctUntilChanged()
.switchMap(value => service.findUsersByName(value)
.map(() => ({error: 'Error'})) //example of failed validation
.catch(() => Observable.of(null))) //example of successful validation
.do(v => console.log('mapped', v))
.share()
.take(1);
}
validate = (control: AbstractControl) => {
// An immediate timeout is set because the next has to occur after the
// validator chain is subscribed to.
setTimeout(() => this.validatorInput.next(control.value), 0);
return this.validatorChain;
}
You use it like this:
this.createUserForm = fb.group({
login: [ null,
Validators.required,
new AsyncValidator(userService).validate
],
});
}