Combine nested HTTP requests in single observable

Viewed 458

I have a Workspace model, a Request model... and now a CatalogItem model which needs to combine the two like this:

{
  workspace: Workspace
  requests: Request[]
}

I'm having a bit of difficulty with how I should create this with a getCatalog(): Observable<CatalogItem[]> function... which should return a CatalogItem for each Workspace, with any associated Request added to it.

I realize my attempt is way off, but I'm hoping it's enough to help understand my intent:

getCatalog(): Observable<CatalogItem[]> {
  return this.http.get<Workspace[]>(`${this.baseUrl}/workspace`).pipe(
    switchMap( (ws) => {
      return {
        workspace: ws,
        requests: this.http.get<Request[]>(`${this.baseUrl}/${ws.id}/requests`).subscribe();
      }
    })
  );
  //TODO: combine workspace and request create catalog obj
}
3 Answers

Looking at your code, it seems like you need to call http.get<Request[]> for each workspace.

Here's how to implement it, simply follow the 3 steps below.

1.
First use mergeAll to convert Obsevable<Workspace[]> (returned by get<Workspace[]>) into a stream (so we can handle one workspace at a time).

2.
Next use mergeMap to convert each Observable<Workspace> into Observable<CatalogItem> by calling http.get<Request[]> and forming a new CtalogItem once response arrived using map (utilizing closure to reference workspace).

3.
Finally convert the stream back into array using toArray.


getCatalog(): Observable<CatalogItem[]> {
  return this.http.get<Workspace[]>(`URL/workspace`).pipe(

   // 1  
    mergeAll(),

   // 2  
    mergeMap(workspace => this.http.get<Request[]>(`URL/${workspace.id}`).pipe(
      map(requests => ({ workspace, requests })))
    ),

   // 3  
    toArray()

  )
}

mergeAll can flattens an Observable containing an array into a stream, for further explanation refer to @Martin's post about the best way to “flatten” an array inside an RxJS Observable

toArray collects all source emissions and emits them as an array when the source completes.

Here's an example:

The idea is to chain your requests so they are dependent on each other and have the result being a merged object of some sort like you have laid out.

The key (at least from my learnings on this) is the toArray at the end of the pipeline

  getTodos(): Observable<SomeMergedObject[]> {
    return this.http.get<TodoItem[]>('https://jsonplaceholder.typicode.com/todos').pipe(
      mergeMap( resp => resp),
      concatMap( (t: TodoItem) => {
        console.log(t.userId)
        return this.http.get<User>(`https://jsonplaceholder.typicode.com/users/${t.userId}`).pipe(
          map( user => ({ todoItem: t, user } as SomeMergedObject))
        )
      }),
      toArray()
    );
  }

Interfaces

interface TodoItem {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

interface User {
  id: number,
  name: string,
  username: string,
  address: object,
  phone: string;
  website: string;
  company: object;
}

interface SomeMergedObject {
  todoItem: TodoItem;
  user: User;
}

You can use an inner pipe to solve your problem:

getCatalog(): Observable<CatalogItem[]> {
  return this.http.get<Workspace[]>(`${this.baseUrl}/workspace`).pipe(
    switchMap(workspace => this.http.get<Request[]>(`${this.baseUrl}/${ws.id}/requests`).pipe(
      map(requests => ({workspace, request}))
    )
  );
}

Details:

  • If you open another pipe inside the switchMap you still have the context of the switchMap. That means you can use the workspace.
  • If your variable name is the key you want (e.g. const key = 'test') as property you can shortwrite { key }. This will create an object { key: 'test }
  • If you want to return an object inside a map you can shortwrite ({ ... }) instead of { return {} }
Related