'Iterate through Observable object-array and add additional data from another Observable to every object

I have a Angular service returning an array of "cleaningDuty" objects. Inside a duty object, there is a nested object called "currentCleaner" with an id.

[
  {
    // other cleaningDuty data
    currentCleaner: {
      id: string;
      active: boolean;
    };
  },
  {
    // other cleaningDuty data
    currentCleaner: {
      id: string;
      active: boolean;
    };
  },
  {
    // other cleaningDuty data
    currentCleaner: {
      id: string;
      active: boolean;
    };
  }
]

with the help of the currentCleaner.id I want to fetch the user data of the currentCleaner from a UserService dynamically in the same pipe() method and add the returned user data to the cleaningDuty object. Then the object should look like this:

{
  // other cleaningDuty data
  currentCleaner: {
    id: string;
    active: boolean;
  },
  cleanerData: {
    name: string;
    photoUrl: string;
    // other user data
  }
},

Unfortunately I just cant get this to work even after investing days into it. I tried almost every combination from forkJoin(), mergeMap() and so on. I know a nested subscribe() method inside the target component would get the job done, but I want to write the best code possible quality-wise. This is my current state of the service method (it adds the user observable instead of the value to the cleaningDuty object):

getAllForRoommates(flatId: string, userId: string) {
    return this.firestore
      .collection('flats')
      .doc(flatId)
      .collection('cleaningDuties')
      .valueChanges()
      .pipe(
        mergeMap((duties) => {
          let currentCleaners = duties.map((duty) =>
            this.userService.getPublicUserById(duty.currentCleaner.id),
          );
          return forkJoin([currentCleaners]).pipe(
            map((users) => {
              console.log(users);
              duties.forEach((duty, i) => {
                console.log(duty);
                duty.cleanerInfos = users[i];
              });
              return duties;
            }),
          );
        }),
      );
  }

The getPublicUserById() method:

getPublicUserById(id: string) {
    return this.firestore.collection('publicUsers').doc(id).valueChanges();
  }

Any help would be greatly appreciated!



Solution 1:[1]

  1. Check if you need switchMap or mergeMap. IMO I'd cancel the existing observable when the valueChanges() emits. See here for the difference b/n them.
  2. Use Array#map with RxJS forkJoin function to trigger the requests in parallel.
  3. Use RxJS map operator with destructuring syntax to add additional properties to existing objects.
  4. Ideally defined types should be used instead of any type.

Try the following

getAllForRoommates(flatId: string, userId: string) {
  return this.firestore
    .collection('flats')
    .doc(flatId)
    .collection('cleaningDuties')
    .valueChanges()
    .pipe(
      switchMap((duties: any) =>
        forkJoin(
          duties.map((duty: any) =>          // <-- `Array#map` method
            this.userService.getPublicUserById(duty.currentCleaner.id).pipe(
              map((data: any) => ({          // <-- RxJS `map` operator
                ...duty,                     // <-- the `currentCleaner` property
                cleanerData: data            // <-- new `cleanerData` property
              }))
            )
          )
        )
      )
    );
}

Solution 2:[2]

Firstly I would suggest you to improve the typings on your application, that will help you to see the issue more clear. For my answer I will assume you have 3 interfaces defined CurrentCleaner, CleanerData and RoomData wich is just:

interface RoomData {
    currentCleaner: CurrentCleaner
    cleanerData: CleanerData
}

Then you can write a helper function to retrieve the room data from a current cleaner:

getRoomData$(currentCleaner: CurrentCleaner): Observable<RoomData> {
    return this.userService.getPublicUserById(currentCleaner.id).pipe(
        map(cleanerData => ({ currentCleaner, cleanerData }))
    )
}

As you can see, this functions take a current cleaner, gets the cleaner data for it and maps it into a RoomData object.

Having this next step is to define your main function

getAllForRoommates(flatId: string, userId: string): Observable<RoomData[]> {
    return this.firestore
        .collection('flats')
        .doc(flatId)
        .collection('cleaningDuties')
        .valueChanges()
        .pipe(
            mergeMap((duties) => {
                // here we map all duties to observables of roomData
                let roomsData: Observable<RoomData>[] = duties.map(duty => this.getRoomData$(duty.currentCleaner));

                // and use the forkJoin to convert Observable<RoomData>[] to Observable<RoomData[]>
                return forkJoin(roomsData)
            }),
        );
    }

I think this way is easy to understand what the code is doing.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Andres Ballester