1
votes

Currently, I am working on a pretty complicated set of queries to firestore.

I am trying, all at once, to populate an array full of references to other documents, and then read those documents and put the information in an array.

More specifically to this example, I have 4 references in one collection. I want to get those references, and then using those references, query 4 documents and populate an array with the information.

The order is as follows: do a query for all of the documents in the tags subcollection, which is handled by the function below:

getTagsOnPage(workspaceId: string, pageId: string) {
    // get all of the references in the tags sub-collection, and puts them in an array
    // get all id's, do not get data
    return this.afs
      .collection("users")
      .doc(`${auth().currentUser.uid}`)
      .collection<Workspace>("workspaces")
      .doc(`${workspaceId}`)
      .collection<Page>("pages")
      .doc(`${pageId}`)
      .collection("tags")
      .snapshotChanges()
      .pipe(
        map((actions) => {
          return actions.map((a) => {
            const ref = a.payload.doc.get("reference");
            return ref; // return an array of (id: reference) key-value pairs
          });
        })
      );
  }

This works fine with the following code performing the subscription:

this.pageService
      .getTagsOnPage(this.workspaceId, this.currentPage.id)
      .subscribe((data) => {
        temp = data;
      });

data is as follows, via the console:

(3) ["/users/ucB5cF4Pj3PWhRn10c9mvqQbS7x2/workspaces/It1…1tSnPI5GJrniY82vZL/localTags/1p5Tscwn14PyK6zRaFHX", "/users/ucB5cF4Pj3PWhRn10c9mvqQbS7x2/workspaces/It1tSnPI5GJrniY82vZL/localTags/lFKoB0jngmtnALut2QS2", "/users/ucB5cF4Pj3PWhRn10c9mvqQbS7x2/workspaces/It1tSnPI5GJrniY82vZL/localTags/r6sf2SX6v87arU2rKsD5"]

Now, to perform the next set of data reads is where my confusion begins.

My initial approach was to try a for loop (for the length of this array), but this would involve iterating performing a number of nested subscriptions, which I do not think is possible in this sense.

I am fairly new to rxjs, and have only used the map and switchMap operators. In this case, I am imagining I would use something like mergeMap and/or flatMap, but frankly, I am not sure how to make this work in this case. Also, dealing with the for loop where I need to grab documents based on the array of documentReferences I get is also throwing me for a loop.

This is my current implementation, which is all over the place; I hope the feel for what I am trying to do is there. Basically, get the array of references via getTagsOnPage, wait for the observable to end, use switchMap to take the data array and loop over it; ideally, subscribe to each ref and add to tagData, and then return that:

let tagData;
    this.pageService.getTagsOnPage(this.workspaceId, this.currentPage.id).pipe(
      switchMap((data) => {
        let references = data;
        for (let j = 0; j < references.length; j++) {
          let ref = this.afs.doc(`${references[j]}`);
          ref.snapshotChanges().pipe(
            map((actions) => {
              const data = actions.payload.data();
              tagData.push(data);
            })
          );
          // push the data (different data var)
        }
      })
    );
    return tagData;

Messy, I know, but I think once I know the right operators to use this will make a lot more sense.

Also, atm this returns an empty array. There is an error for when I use switchMap that says the following:

Argument of type '(data: any[]) => void' is not assignable to parameter of type '(value: any[], index: number) => ObservableInput<any>'.
  Type 'void' is not assignable to type 'ObservableInput<any>'.

Thank you for any help!

1
combineLatest could be an right operator for your case. What you can do is map getTagsOnPage results to this.afs.doc().snapshotChanges() and pass it to that operator.Buczkowski

1 Answers

1
votes

The reason for your error using switchMap is because you are not returning an Observable.

When using any of the "Higher Order Mapping Operators" (switchMap, concatMap, mergeMap, etc), the provided function must return an observable. As the error states: "Type void is not assignable to type ObservableInput<any>", you aren't returning anything (void).

The next thing that isn't quite right is that within your loop, you reference ref.snapshotChanges().pipe(), but you never subscribe to it. As you may know, observables are lazy and won't fire unless there is a subscriber.

As long as you return an observable inside of switchMap(), it will automatically subscribe to it and emit its values.

Let's think about this a little bit differently; instead of looping over the results of your first call, executing them, then pushing the values into an array. We can instead take the results and turn them into an observable stream that emits all the results of their individual calls, and combines them into an array for you. But... with a subtle difference: I'm suggesting not to have a separate tagData array outside the stream, but to have your observable return the tagData form that you need as an Observable<TagData[]>.

I think something like this will work for you:

tagData$ = this.pageService.getTagsOnPage(this.workspaceId, this.currentPage.id).pipe(
    switchMap(references => from(references)),
    mergeMap(ref => this.afs.doc(ref).snapshotChanges()),      
    map(actions => actions.payload.data()),
    scan((allTagData, tagData) => [...allTagData, tagData], [])
  })
);

Let's break that down!

  1. We use from to create an observable from your array, that emits each "reference" one at a time.

  2. mergeMap will subscribe to the observable we are creating and emit its emissions

  3. map simply transforms the value into your desired shape

  4. scan accumulates your emissions for you and emits on each change. if you don't want to emit until all calls come back, use reduce() instead

Now, you can simply do: tagData$.subscribe() and do what you want with your resulting array of data. Or you could even use the async pipe in your template rather than subscribing in your component.