How to correctly chain together 3 types of http requests, sequentially and dynamically?

Viewed 255

How can I use correctly chain together 3 types of http requests, where each request relies on data from the previous, and there might be a dynamic amount of needed requests depending on what was returned?

In my database I have a team table, where a team has a businessId that it belongs to. I have a supervisor table, which has a teamId that it belongs to. And I have an employee table, which have supervisorId that they report to.

I hope this makes sense so far.

I'm still learning RxJS, so forgive me if my interpretation for these operators is wrong, but I'm going to try and describe the workflow.

  1. Depending on the business selected, we get all the teams associated with that business and save them
  2. Now there could be 4 teams, so we iterate through each team and store the observable in an array
  3. Then we return forkJoin on the array of observables so that it waits until they are all done
  4. Now concatMap again, because we won't know which employees to get without the supervisors data
  5. doing the same process, we might have 3 supervisor id's, so create an array of observables for all the supervisors
  6. Do the forkJoin again, returning observables for the employees
  7. finally, subscribe to the last forkJoin to get the employee data.

Here's my current code:

onSelectBusiness(event): void {
  this.selectedBusinessId = event.target.value;
  this._service.getAllTeams(this.selectedBusinessId).pipe(
    concatMap((_teams) => {
      let supervisorObservables: any[] = [];
        _teams.forEach(t => {
          // Need to call this http request for however many different teams there are
          this.teams.push(t); // Saving the teams
          supervisorsObservables.push(this._service.getAllSupervisors(t.teamId))
        })
        return forkJoin({supervisorObservables}).pipe(
          concatMap((_supervisors) => {
            let employeeObservables: any[] = [];
            employeeObservables.forEach(s => {
              this.supervisors.push(s); // saving supervisors
              // Need to call this http request for as many different supervisors there are
              employeeObservables.push(this._service.getAllEmployees(s.supervisorId))
            })
            return forkJoin({employeeObservables})
          })
        )
    })
  ).subscribe(
     ({employeeObservables}) => {
       // Even console.logs are not displaying
       employeeObservables.forEach(e => {
         this.employees.push(e);  // Supposed to get my employee data here, but im not
       })
     }
  )
}

The main problem is that in the final subscribe, nothing happens in there. I'm able to get teams and supervisors data, but not employees.

I've tried to add another:

// Same as above code
// ...
return forkJoin({employeeObservables}).pipe(
  concatMap(_employees) => {
    // but I have no other http request to return, which is required by concatMap
    // and I can't simply subscribe to this forkJoin because concatMap requires the subscribe at the bottom
  }
)

I'm not even sure if its possible to do what I want, regarding the forEach loops and forkJoins inside of the concatMaps..

Any and all comments/help/tips is greatly appreciated! :)

2 Answers

I've done something similar using an approach that's a bit inverse of yours. Something like this:

  posts$ = this.http.get<Post[]>(this.postUrl).pipe(
    mergeMap(posts =>
      forkJoin(
        posts.map(post =>
          this.http.get<User>(`${this.userUrl}/${post.userId}`).pipe(
            map(user => ({
              title: post.title,
              userName: user.name
            }))
          )
        )
      )
    )
  );

This is only chaining two sets of retrieves (not three). But you may be able to do something similar.

This code retrieves all of the posts. Then for each post, it pulls the user Id from the post and gets the associated user record. In this example, in then uses both sets of data to map into one Observable.

If I have a chance today, I'll try to adjust this approach to more closely match with what you are trying to achieve.

UPDATE: I was able to do something similar, though due to the limitations of my data, the "second" retrieve isn't an array. Hopefully you can extrapolate.

  posts$ = this.http
    .get<Post[]>(this.postUrl)
    .pipe(concatMap(posts => forkJoin(posts.map(post => this.getUser(post)))));

  constructor(private http: HttpClient) {}

  getToDos(post: Post, user: User) {
    return this.http.get<ToDo[]>(`${this.todoUrl}?userId=${post.userId}`).pipe(
      map(
        todos =>
          ({
            ...post,
            userName: user.name,
            postUser: {
              ...user,
              userToDos: [...todos]
            }
          } as Post)
      )
    );
  }

  getUser(post: Post) {
    return this.http
      .get<User>(`${this.userUrl}/${post.userId}`)
      .pipe(concatMap(user => this.getToDos(post, user)));
  }

Full stackblitz is here:

https://stackblitz.com/edit/angular-posts-with-three-chains

As far as I can see you are broadly doing the right things and it "should work", so the problem must be in the detail.

I am suspicious of the fact that you build an array but pass it into forkJoin wrapped as an object, but then treat the result as an array. If it isn't the cause of the problem is it at least unnecessary: just pass the array to forkJoin.

Also, your finally result will be an array of array of employees, so you will want to flatten it.

The code could be simplified by using map instead of building the inner arrays manually - it doesn't fundamentally change anything, but does make it a bit easier to see what is going on. Also, as you only want the employees at the end there is no need to "nest":

onSelectBusiness(event): void {
  this.selectedBusinessId = event.target.value;

  this._service.getAllTeams(this.selectedBusinessId).pipe(

    concatMap(_teams => 
      forkJoin(_teams.map(t => this._service.getAllSupervisors(t.teamId)))
    ),

    concatMap(_supervisors =>
      forkJoin(_supervisors.map(s => this._service.getAllEmployees(s.supervisorId)))
    )

  ).subscribe(employees => {
    console.log(employees.flat());
  });
}

EDIT: I have done a bit of testing now and I see the problem: the supervisors are also an array of arrays and need to be flattened as well. You can do this at various places, but I would probably pipe the result of the forkJoin to map(supervisors => supervisors.flat()), and the same for employees. That way after each step you are dealing with data in the format you are "expecting".

Related