3

I have a collection, where each single document have an array of references.

The structure in Firestore is like this:

collection1
   doc1
   doc2
   doc3
       name: string
       ...
       refs: Array
           collection2/x786123nd...
           collection2/z1237683s...
           ...

I want to convert that into:

[
   {<doc1 as object>},
   {<doc2 as object},
   { name: "Document 3", refsResult: 
     [
       {<object here mapped from reference>},
       {<object here mapped from reference>},
     ]
   }

]

Long story short, from firebase I need to get an output of a list of object where each object has a list of objects instead of references.

THE FOLLOWING SNIPPET IS NOT WORKING

I am trying to do it at service level in Angular using RxJS to transform the output, something like this:

return this.afs.collection('collection1')
            .valueChanges()
            .pipe(
                switchMap(objects => from(objects).pipe(
                    switchMap(obj => from(obj.refs).pipe(
                        switchMap(ref => ref.get()),
                        map(r => ({ ...obj, refsResult: r.data() })),
                        tap(obj => console.log('OBJ', obj)),
                    )),
                )),
                tap(objects => console.log(objects))
            );

But it seems that I only receive one object instead of a list of objects with 2. Also, it seems that the refsResult is also a single object instead of an array. I am sure I am using the switchMaps wrong, probably they are cancelling the others results or similar.

I would like to keep the solution within the chain of rxjs operators.

EDIT 1 I got it working in a hacky way, I am just sharing it here in order to give more context, and see if we can find a way to do it with RxJS.

return this.afs.collection('collection1')
            .valueChanges()
            .pipe(
                tap(objects => {
                    objects.forEach(object => {
                        object.refsResult = [];
                        object.refs.forEach(ref => {
                            ref.get().then(d => object.refsResult.push(d.data()));
                        })
                    })
                }),
                tap(programs => console.log(programs))
            );

The code above works but not very well, since first return the collection and then, mutates the objects to inject the items within the array. Hopefully it helps.

Help is very appreciated! :)

4
  • As it seems like you want to go the way to mutate your objects list, using tap is fine. If the objects list would be an external source you could consider creating a new list with objects (immutable) as you might dont want to mutate the an object from somewhere else. The way to create a new object would be something like: map(objects => ([...objects.map(...)])) Commented Aug 5, 2021 at 1:01
  • That is exactly the opposite of what I am asking. I do not want to mutate them. Using tap is hacky and not ok because the observable emits before references have been resolved, I want to emit once everything is ready not before + mutation. Commented Aug 9, 2021 at 10:32
  • The moment you push data to ever object (object.refsResult.push(..)) you mutate your original object from the afs.collection('collection1'). If you want to mutate, as you do, tap is fine. If you want to go immutable map is prefered. I might also just do not understand your question correctly and cannot help you. In this case - just ignore my comments :/ Commented Aug 9, 2021 at 18:10
  • 1
    @JonathanStellwag thank you for your comments, maybe I did not explain myself correctly and made a confusion. My target was to have it all fully rxjs operators oriented, emit once all mapping is done, and immutable. Thanks again for the help :) Commented Aug 11, 2021 at 11:18

1 Answer 1

1
+50

You can use combineLatest to create a single observable that emits an array of data.

In your case, we can use it twice in a nested way:

  • to handle each object in collection
  • to handle each ref in refs array for each object

The result is a single observable that emits the full collection of objects, each with their full collection of ref result data. You then only need a single switchMap to handle subscribing to this single observable:

  return this.afs.collection('collection1').valueChanges().pipe(
      switchMap(objects => combineLatest(
          objects.map(obj => combineLatest(
                  obj.refs.map(ref => from(ref.get()).pipe(map(r => r.data())))
              ).pipe(
                  map(refsResult => ({ ...obj, refsResult }))
              )
          ))
      ))
  );

For readability, I'd probably create a separate function:

function appendRefData(obj) {
    return combineLatest(
        obj.refs.map(ref => ref.get().pipe(map(r => r.data())))
    ).pipe(
        map(refsResult => ({ ...obj, refsResult }))
    );
}
return this.afs.collection('collection1').valueChanges().pipe(
    switchMap(objects => combineLatest(objects.map(appendRefData)))
);
Sign up to request clarification or add additional context in comments.

2 Comments

Note: I haven't tested this, so there could be typos, but hopefully it points you in the right direction.
Thanks! I tried combineLatest but not in this way. This is super helpful. I just accepted to give you the bounty and mark it as correct. Just what I was looking for. One little thing in case you want to edit it for further impact/help to others. ref.get() returns a promise in Firebase, so I did from(ref.get()).pipe... to make it fully compatible. Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.