What is the best way to link three different types of http requests in succession and adaptively?

Is it possible to chain together 3 types of http requests correctly, where each request depends on data from the previous one and the number of required requests can vary?

In my database, there is a team table with a corresponding businessId, a supervisor table with a teamId, and an employee table with a supervisorId.

I hope this explanation so far makes sense.

As I am still learning RxJS, please bear with me if my understanding of these operators is not entirely accurate. Here is how I envision the workflow:

  1. Retrieve all teams associated with the selected business and save them
  2. Iterate through each team, saving observables in an array
  3. Use forkJoin on the array of observables to ensure they all complete
  4. Use concatMap again to proceed with fetching employees, based on supervisor data
  5. Create an array of observables for all supervisors
  6. Again, use forkJoin to get observables for employees
  7. Finally, subscribe to the last forkJoin to obtain employee data

Here is the current code snippet:

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
          supervisorObservables.push(this._service.getAllSupervisors(t.teamId))
        })
        return forkJoin({supervisorObservables}).pipe(
          concatMap((_supervisors) => {
            let employeeObservables: any[] = [];
            _supervisors.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 issue lies in the final subscribe function which does not seem to be functioning as expected. While I am able to retrieve team and supervisor data, employee data remains inaccessible.

I have attempted to introduce another piece of code:

// Same as above code
// ...
return forkJoin({employeeObservables}).pipe(
  concatMap(_employees) => {
    // However, I do not have another http request to return, which is necessary for concatMap
    // Furthermore, subscribing directly to this forkJoin is not feasible due to the requirement of concatMap at the end
  }
)

Am I even able to achieve what I desire with the forEach loops and forkJoins within concatMaps? Any help, tips, or feedback would be greatly valued! :)

Answer №1

My approach in handling a similar task is a bit different from yours. Here's how I tackled it:

  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
            }))
          )
        )
      )
    )
  );

In my implementation, only two sets of retrieves are chained rather than three. However, you might find a way to achieve something similar.

This code first fetches all the posts. Then, for each post, it extracts the user Id and fetches the corresponding user record. The data from both retrievals are then combined into one Observable in this example.

If I have time today, I will try to tweak this method to align more closely with your goals.

UPDATE: I managed to implement something akin to your requirement. Due to limitations in my dataset, the secondary retrieve does not return an array. Nevertheless, you may be able to apply the concept.

  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)));
  }

A full stackblitz demonstration can be found here:

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

Answer №2

It seems like you are on the right track with your approach, but the devil is in the details when it comes to solving the problem.

I have a hunch that passing an array into forkJoin wrapped as an object, only to treat the result as an array, may be causing unnecessary confusion. It might simplify things to just pass the array directly to forkJoin.

Additionally, since the final result will be an array of arrays of employees, you'll need to flatten it out for better handling.

You could streamline the code by using map instead of manually building the inner arrays, which won't change the functionality but can make it clearer. Also, you don't need to nest the data if you only want the employees at the end:

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: After some testing, I've discovered that the supervisors are also arrays of arrays and need to be flattened. You could achieve this by piping the result of the forkJoin to map(supervisors => supervisors.flat()), and do the same for employees. This way, you're always working with data in the expected format after each step.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Utilize React to transform PDF files into PNG images and seamlessly upload them to Cloudinary

Currently, I am utilizing react typescript and looking to select a PDF file, transform the PDF into an image, and then upload the final image onto Cloudinary. Although I have a service set up for uploading images in my Cloudinary media library, I am unsu ...

Unlocking the Power of Dependent Types in TypeScript: Unveiling Type by Property Name Declaration

In an attempt to tie the types to the arguments passed, consider the following example: type NS = "num" | "str" type Data<T extends NS> = T extends "num" ? number : string type Func<T extends NS> = (x: Data<T> ...

Tips for accessing Firebase document fields with Angular Firestore (version 7)

My current task involves retrieving a Firebase document property based on the specified model: After successfully locating a document with this code snippet: //Users - collection name, uid - document uid. I am attempting to access the isAdmin property u ...

Changing the default font size has no effect on ChartJS

I'm trying to customize the font size for a chart by changing the default value from 40px to 14px. However, when I set Chart.defaults.global.defaultFontSize to 14, the changes don't seem to take effect. Below is the code snippet for reference. An ...

It seems like there is an issue with your network connection. Please try again

Recently, I made the switch to EndeavourOS, which is based on Archlinux. I completed all my installations without any issues and attempted to create a new NestJs project after installing NVM, Node's latest version, and the NestJs/cli. However, when I ...

Typescript: The .ts file does not recognize the definition of XMLHttpRequest

I have encountered an issue with a .ts file containing the following code: var xhttp = new XMLHttpRequest(); After running the grunt task to compile the ts files using typescript, no errors were reported. However, when I attempt to instantiate the class ...

Utilizing Dialogflow's Conv.Data or Conv.User.Storage for a Basic Counter System

I am interested in developing a simple counter within a conversation using a Firebase function with Actions for Google. The documentation suggests the following: app.intent('Default Welcome Intent', conv => { conv.data.someProperty = &apo ...

The template literal expression is being flagged as an "Invalid type" because it includes both string and undefined values, despite my cautious use of

I am facing an issue with a simple component that loops out buttons. During the TypeScript build, I encountered an error when calling this loop: 17:60 Error: Invalid type "string | undefined" of template literal expression. In my JSX return, I ...

Encountering problem when creating new app with Angular CLI version 6.0.4 and specifying the

I just used npm to globally install angular cli version 6.0.4 with the command `npm install -g @angular/cli`. However, when I attempt to create a new project using `ng new my-first-app`, an error message is displayed: The input for the schematic does not ...

Leveraging the power of Framer Motion in combination with Typescript

While utilizing Framer Motion with TypeScript, I found myself pondering if there is a method to ensure that variants are typesafe for improved autocomplete and reduced mistakes. Additionally, I was exploring the custom prop for handling custom data and des ...

Generate ES6 prototypes using TypeScript

Is it possible to enhance specific class methods in TypeScript by making them prototypes, even when the target is ES6? Additionally, can a specific class be configured to only produce prototypes? Consider the following TypeScript class: class Test { ...

How to retrieve values from multiple mat-sliders that are dynamically generated using ngFor loop

Creating multiple mat-sliders dynamically in Angular looks like this: <ng-container *ngFor="let parameter of parameterValues; let i = index;"> <mat-slider (input)="onInputChange($event)" min="1" max="{{ parameter.length }}" step="1" value="1" i ...

Disabling behavior subjects in Angular 8: A guide to unsubscribing

Hello, I am currently working on an Angular8 application where I am utilizing Replay Subject and Behavior Subject. I have noticed that when initially clicking, the API is only hit once. However, if I switch between tabs, the subscription triggers multiple ...

Developing in VS Code and Angular: How to create a component without a spec file

Hello fellow developers! I am currently using the latest version of VS Code for my Angular application. One feature I have been utilizing is the ability to right-click a folder and select "New component" to quickly and easily create a new Angular componen ...

Is it possible to use the `fill` method to assign a value of type 'number' to a variable of type 'never'?

interface Itype { type: number; name?: string; } function makeEqualArrays(arr1: Itype[], arr2: Itype[]): void { arr2 = arr2.concat([].fill({ type: 2 }, len1 - len2)); } What is the reason for not being able to fill an array with an ob ...

When the state changes, the dialogue triggers an animation

Currently, I am utilizing Redux along with material-ui in my project. I have been experimenting with running a Dialog featuring <Slide direction="up"/> animation by leveraging the attribute called TransitionComponent. The state value emai ...

Is it possible to have a synchronous function imported in typescript?

// addons.ts export interface addon { name: string; desc: string; run: (someparam: any) => void; } export function loadaddons(): Array<addon> { let addons: Array<addon> = []; fs.readdirSync(path.join(__dirname, "addons")) .fi ...

The class 'GeoJSON' is mistakenly extending the base class 'FeatureGroup'

Encountering an error message in one of my projects: test/node_modules/@types/leaflet/index.d.ts(856,11): error TS2415: Class 'GeoJSON' incorrectly extends base class 'FeatureGroup'. Types of property 'setStyle' are incompa ...

String values not being set as default in Angular Material's mat-button-toggle-group

<div class="navigation"> <mat-button-toggle-group class="button-toggle" [(ngModel)]="langSelected" (change)="onToggleGroupChange($event)" [value]="langSelected"> <mat-button-toggle value=& ...

What is a way to merge all the letters from every console.log result together?

I'm encountering an issue - I've been attempting to retrieve a string that provides a link to the current user's profile picture. However, when I use console.log(), the output appears as follows: Console Output: https://i.sstatic.net/70W6Q ...