4

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
}
Jonathan Stellwag
  • 3,843
  • 4
  • 25
  • 50
Smern
  • 18,746
  • 21
  • 72
  • 90
  • To clarify, the second call is dependent on the result of the first call? (you can't have them run in parallel) – mwilson Aug 19 '20 at 20:04
  • right, i'm attempting to do nested calls (the request calls can run in parallel after we get the workspace data)... but hoping to put this all together inside of getCatalog()... so /{workspace_id}/requests will add requests associated with each workspace that is returned from the /workspace call – Smern Aug 19 '20 at 20:11
  • I proposed a working answer below. Let me know if that's close to what you want. – mwilson Aug 20 '20 at 15:30

3 Answers3

2

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.

Rafi Henig
  • 5,950
  • 2
  • 16
  • 36
1

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;
}
mwilson
  • 12,295
  • 7
  • 55
  • 95
  • looking at the interface for mergedObject... shouldn't `todoItem` be type `TodoItem[]` – Smern Aug 21 '20 at 15:14
  • In this example, I'm just getting back of of the todo's and getting the user (by Todo.UserId) and merging the two User/Todo objects. So, the result is actually `SomeMergedObject[]` but you could totally change that to be a return type of `TodoItem[]`. I just went with a merged object since that's what it looked like you were doing. If you look at the stackblitz, there is a full, working example in there. Might take a second to fetch the data since it's doing a ton of HTTP requests. – mwilson Aug 21 '20 at 15:22
  • i mean, SomeMergedObject.todoItem should really be an array (todoItems) as "requests" in my question. There could be 0 or many "todoItem" per "SomeMergedObject" – Smern Aug 21 '20 at 16:58
  • You can certainly change it that way. You can form an object any way you like. Are you saying that you are essentially not trying to map two different responses to each other but just trying to consolidate them into an object `{ allResponsesForResponse1: [ , ... ], allResponsesForResponse2: [, ...] }` – mwilson Aug 21 '20 at 17:32
1

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 {} }
Jonathan Stellwag
  • 3,843
  • 4
  • 25
  • 50