Search code examples
angularrxjsobservableangular-route-guards

Observable<Observable<T>> to Observable<T>


RxJS beginner here. I am using Angular 6, and am trying to figure out how to get Observable<T> from Observable<Observable<T>>. I'm not sure if this is even valid and I'm struggling to understand it conceptually, however it seems like a simple problem.

I've looked into switchMap, flatMap, forJoin, however I don't think they fit my needs.

What I'm trying to do is an Angular route guard, which will prevent users from accessing a route unless they have the necessary permissions. 2 dependencies are a user profile to get their info from, which is then used to fetch their permissions. This mix is resulting in the Observable of Observable issue. Here's what I've got:

export class AuthPermissionsRouteGuard implements CanActivate {
    constructor(
    private router: Router,
    private authService: AuthPermissionsService,
    private openIdService: AuthOpenIdService) {}
    
    /**Navigates to route if user has necessary permission, navigates to '/forbidden' otherwise */
    canActivate(routeSnapshot: ActivatedRouteSnapshot): Observable<boolean> {
        return this.canNavigateToRoute(routeSnapshot.data['permissionId'] as number);
    }

    /**Waits on a valid user profile, once we get one - checks permissions */
    private canNavigateToRoute(permissionId: number): Observable<boolean> {
        const observableOfObservable = this.openIdService.$userProfile
            .pipe(
                filter(userProfile => userProfile ? true : false),
                map(_ => this.hasPermissionObservable(permissionId)));

            // Type Observable<Observable<T>> is not assignable to Observable<T> :(
        return observableOfObservable;
    }

    /**Checks if user has permission to access desired route and returns the result. Navigates to '/forbidden' if no permissions */
    private hasPermissionObservable(permissionId: number): Observable<boolean> {
        return this.permissionsService.hasPermission(permissionId).pipe(
            map(hasPermission => {
                if (!hasPermission) {
                    this.router.navigate(['/forbidden']);
                }

                return hasPermission;
            }
        ));
    }
}

Solution

  • As it stands, you are returning an Observable from the hasPermissionObservable function, which is going to be wrapped in an observable from the map operator.

    You'll want to look at the mergeMap/flatMap operator or contactMap operator.

    MergeMap: This operator is best used when you wish to flatten an inner observable but want to manually control the number of inner subscriptions. Example from the Learn RXJS link:

    // RxJS v6+
    import { of } from 'rxjs';
    import { mergeMap } from 'rxjs/operators';
    
    // emit 'Hello'
    const source = of('Hello');
    // map to inner observable and flatten
    const example = source.pipe(mergeMap(val => of(`${val} World!`)));
    // output: 'Hello World!'
    const subscribe = example.subscribe(val => console.log(val));
    
    

    ContactMap: Map values to inner observable, subscribe and emit in order. Example from the Learn RXJS link:

    // RxJS v6+
    import { of } from 'rxjs';
    import { concatMap } from 'rxjs/operators';
    
    // emit 'Hello' and 'Goodbye'
    const source = of('Hello', 'Goodbye');
    // example with promise
    const examplePromise = val => new Promise(resolve => resolve(`${val} World!`));
    // map value from source into inner observable, when complete emit result and move to next
    const example = source.pipe(concatMap(val => examplePromise(val)));
    // output: 'Example w/ Promise: 'Hello World', Example w/ Promise: 'Goodbye World'
    const subscribe = example.subscribe(val =>
      console.log('Example w/ Promise:', val)
    );
    

    So for your example:

    /**Waits on a valid user profile, once we get one - checks permissions */
    private canNavigateToRoute(permissionId: number): Observable<boolean> {
      const observableOfObservable = this.openIdService.$userProfile
        .pipe(
           filter(userProfile => userProfile ? true : false),
           concatMap(_ => this.hasPermissionObservable(permissionId))); // <- try changes here
    
      // Type Observable<Observable<T>> is not assignable to Observable<T> :(
      return observableOfObservable;
    }