18

Using RxJS v6 is challenging to retrieve data from sub collection for every item in the loop. There is no way to iterate throw array that is retrieved from HTTP call. Merge map does it only for single item where there is a requirement to do it on all array items.

I've been stuck on this for 3 days. I've tried everything. Problem is that new pipe syntax while provides you with neat way to organise code there is no easy way to loop throw collection of data. It is impossible to use map javascript function as it is outside observable and items returned are undefined. On internet I could only find basic examples for a single mergeMap which works great. However I need to use every item in array with http call and append response to same object.

I start with two endpoints from 3rd party which I have no control off. I have mocked some example API to illustrate.

Endpoint 1 - Returns all items ID's: http://localhost:3000/contact

{
  "name": "test",
  "contact": [
    {
      "_id": "5c9dda9aca9c171d6ba4b87e"
    },
    {
      "_id": "5c9ddb82ca9c171d6ba4b87f"
    },
    {
      "_id": "5c9ddb8aca9c171d6ba4b880"
    }
  ]
}

Endpoint 2 - Returns single item information: http://localhost:3000/contact/5c9dda9aca9c171d6ba4b87e

[
   {
     "_id": "5c9dda9aca9c171d6ba4b87e",
     "firstName": "Luke",
     "lastName": "Test",
     "email": "vgadgf@adsgasdg.com",
     "created_date": "2019-03-29T08:43:06.344Z"
   }
]

DESIRED RESULT - By using RxJs v6+ return Observable with nested data:

{
  "name": "test",
  "contact": [
    {
      "_id": "5c9dda9aca9c171d6ba4b87e".
      "relationship":  {
                         "_id": "5c9dda9aca9c171d6ba4b87e",
                         "firstName": "Luke",
                         "lastName": "Test",
                         "email": "vgadgf@adsgasdg.com",
                         "created_date": "2019-03-29T08:43:06.344Z"
                       }
    },
    {
      "_id": "5c9ddb82ca9c171d6ba4b87f",
      "relationship": {
                         "_id": "5c9ddb82ca9c171d6ba4b87f",
                         "firstName": "Name2",
                         "lastName": "Test2",
                         "email": "vgadgf@adsgasdg2.com",
                         "created_date": "2019-03-29T08:43:06.344Z"
                       }
    },
    {
      "_id": "5c9ddb8aca9c171d6ba4b880",
      "relationship": {
                         "_id": "5c9ddb8aca9c171d6ba4b880",
                         "firstName": "Name3",
                         "lastName": "Test3",
                         "email": "vgadgf@adsgasdg3.com",
                         "created_date": "2019-03-29T08:43:06.344Z"
                       }
    }
  ]
}

I just get errors when trying to use Javascript loop and using different operators doesn't give me much either as problem is with looping in RxJs. This is code I have produced. Please provide any useful suggestions or documentation.

testService.service.ts

import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {concatMap, map, mergeMap} from 'rxjs/operators';

@Injectable()
export class TestServiceService {

  constructor(
    private http: HttpClient
  ) { }

  public getCombinedData(): Observable<any> {
    return this.getMultipleRelationData()
      .pipe(
        map((result: any) => {
          result = result.contact;

          return result;
        }),
        mergeMap((fullResults: any) => {
          return fullResults[0].relationship = this.getSingleData(fullResults[0]._id);
        })
      );
  }

  public getSingleData(id): Observable<any> {
    return this.http.get('http://localhost:3000/contact/' + id);
  }

  public getMultipleRelationData(): Observable<any> {
    return this.http.get('http://localhost:3000/contact');
  }

}

My current end data looks like just a single item. Its great if I wanted to make just a single call... However its far from desired output described above.

  [
  {
    "_id": "5c9dda9aca9c171d6ba4b87e",
    "firstName": "Luke",
    "lastName": "Test",
    "email": "vgadgf@adsgasdg.com",
    "__v": 0,
    "created_date": "2019-03-29T08:43:06.344Z"
  }
]
Snackoverflow
  • 5,332
  • 7
  • 39
  • 69
Luke
  • 386
  • 1
  • 4
  • 15

3 Answers3

25

This is quite possible using nested mergeMap and map. A key approach to stick with Observable is expanding the contacts array into an observable using from function. Then you collect the data using toArray operator. Providing documented example:

public getCombinedData(): Observable<any> {
  return this.getMultipleRelationData().pipe(
      mergeMap((result: any) =>
          // `from` emits each contact separately
          from(result.contact).pipe(
              // load each contact
              mergeMap(
                  contact =>
                      this.getSignleData(contact._id).pipe(
                          map(detail => ({ ...contact, relationship: detail })),
                      ),
                  // collect all contacts into an array
                  toArray(),
                  // add the newly fetched data to original result
                  map(contact => ({ ...result, contact })),
              ),
          ),
      ),
  );
}

--- Old answer (Using deprecated result selector api)

public getCombinedData(): Observable<any> {
  return this.getMultipleRelationData()
      .pipe(
          mergeMap((result: any) =>

              // `from` emits each contact separately 
              from(result.contact).pipe(
                  // load each contact
                  mergeMap(
                      contact => this.getSignleData(contact._id),
                      // in result selector, connect fetched detail 
                      (original, detail) => ({...original, relationship: detail})
                  ),
                  // collect all contacts into an array
                  toArray(),
                  // add the newly fetched data to original result
                  map(contact => ({ ...result, contact})),
              )
          ),
      );
}
kvetis
  • 6,682
  • 1
  • 28
  • 48
  • 13
    Thanks a ton for that. Works like a charm. If you are in London I will buy you beer. – Luke Apr 04 '19 at 14:33
  • hey sorry, stupid question incoming (I am a noob in RXJS), does this guarantee the call results are added in the order that the requests were made? – lanxion Sep 22 '21 at 18:48
  • 1
    No, it is not. If you want to keep things sorted, use `concatMap` instead of `mergeMap` (the second time it is used in code) or process results differently in the last `map` (you have the original and the newly fetched there). – kvetis Sep 23 '21 at 14:45
  • And thats why I keep coming to SO, thanks dude! – Cabuxa.Mapache May 27 '22 at 12:12
  • hey I have a similar kind of situation can you help me on this https://stackoverflow.com/questions/73643670/0angular-13-ngrx-multiple-get-queries-to-prepare-model-object-collection?noredirect=1#comment130046772_73643670 I will really be very thankful – Sunny Singh Sep 08 '22 at 07:29
7

Although kvetis's answer can work for you here is what I implemented, just posting it because I found it interesting to solve.

  public getCombinedData(): Observable<any> {
    return this.getMultipleRelationData()
      .pipe(
        mergeMap((result: any) => {
          let allIds = result.contact.map(id => this.getSingleData(id._id));
          return forkJoin(...allIds).pipe(
            map((idDataArray) => {
              result.contact.forEach((eachContact, index) => {
                eachContact.relationship = idDataArray[index];
              })
              return result;
            })
          )
        })
      );
  }

Here is an example solution for your question, I have made dummy Observables. https://stackblitz.com/edit/angular-tonn5q?file=src%2Fapp%2Fapi-calls.service.ts

Ashish Ranjan
  • 12,760
  • 5
  • 27
  • 51
0

You also can accomplish that by using nested mergeMap's like the below code:

public getCombinedData(): Observable<any> {
  return this.getMultipleRelationData()
    .pipe(
      mergeMap((result: any) => result.contact),
      mergeMap(contact => this.getSingleData(contact._id))
    )
}

The first mergeMap that returns result.contact is applied to the array of contacts. This step essentially takes each item in your array and converts it into an individual item in the stream which makes the next mergeMap to be called for each contact.

Tiago Ávila
  • 2,737
  • 1
  • 31
  • 34