Search code examples
angularrxjsrxjs-observablesrxjs-pipeable-operators

Angular - nested REST API calls only returning inner call


A ShoppingCart with ShoppingCartItems is fetched via an outer REST call, after which an Observable of the ShoppingCartItems makes the inner call to enhance the ShoppingCartItems with a Provider.

A tap(console.log) after the inner call, reveals that the contents of the ShoppingCart are as expected - with the 5 ShoppingCartItems enhanced with a Provider. Tapping the subscription however, returns 5 alerts each containing the Provider I wanted to add as a property of ShoppingCartItem.

It seems I am using the wrong mergeMap/concatMap/switchMap - or not doing a 'collect' of some sort at the end of one or both calls.

The calls:

  getShoppingCart$(userId: number): Observable<ShoppingCart> {
    return this.rest.getShoppingCart$(userId)
      .pipe(
        mergeMap(
          (shoppingCart) => from(shoppingCart.shoppingCartItems)
            .pipe(
              concatMap(
                item => this.rest.getProviderByWine$(item.wine.id)
                  .pipe(
                    map(provider => item.provider = provider),
                  )
              ),
              // Returns ShoppingCart with Providers added
              tap(() => console.log('ShoppingCart: ' + JSON.stringify(shoppingCart)))
            )
        ),
      )
  }

The subscription:

  ngOnInit(): void {
    this.shoppingCartService.getShoppingCart$(1037).subscribe(
      (shoppingCart: ShoppingCart) => {
        this.dataSourceShoppingCart = new NestedMatTableDataSource<ShoppingCartItem>(shoppingCart.shoppingCartItems);
        // Runs 5 times - each time displaying a Provider, not the ShoppingCart
        alert(JSON.stringify(shoppingCart))
      }
    );
  }

The actual REST calls:

  getShoppingCart$(userId: number): Observable<ShoppingCart> {
    return this.http.get<ShoppingCart>(this.getBaseUrl() + 'users/' + userId + '/shopping-cart');
  }

  getProviderByWine$(wineId: number): Observable<any> {
    return this.http.get<Provider>(this.getBaseUrl() + 'wine/' + wineId + '/provider');
  }

Any pointers are greatly appreciated. Angular version is 8.


Solution

  • Splitting the logic into multiple functions makes the reasoning easier and reduces the amount of nesting. Create a function that adds the provider to an item and a function that adds the enhanced items to a shopping cart.

    You can use forkJoin to execute multiple independent observables in parallel.

    getShoppingCart$(userId: number): Observable<ShoppingCart> {
      return this.rest.getShoppingCart$(userId).pipe(
        mergeMap(shoppingCart => enhanceShoppingCart(shoppingCart))
      );
    }
    
    // Add enhanced items to a shopping cart
    private enhanceShoppingCart(shoppingCart: ShoppingCart): Observable<ShoppingCart> {
      // Map each shopping cart item to an Observable that adds the provider to it.
      const enhancedItems = shoppingCart.shoppingCartItems.map(item => enhanceItem(item))
      return forkJoin(enhancedItems).pipe(
        map(shoppingCartItems => ({ ...shoppingCart, shoppingCartItems }))
      );
    }
    
    // Add provider to an item
    private enhanceItem(item: Item): Observable<Item> {
      return this.rest.getProviderByWine$(item.wine.id).pipe(
        map(provider => ({ ...item, provider }))
      );
    }