Firestore: Using Reference Types for Joins
Tags: firebase, firestore, rxjs, angular
Published: 2021-09-08 04:17:38

## Update 3/31/24
This page is no longer up-to-date. Please see my latest blog article on [Firestore Reference Type](https://code.build/p/firestore-reference-type-fxhopT).

____
## Original Post
___

What do we even use these "reference" types for? I mean, Firestore doesn't even have any joins.

Okay, very true. But, I finally found a use for them in Firebase 9 SDK when I "expanded" my mind. Technically you can search for a reference just like anything else:

**Note:** - I am using some Angular Examples here from [Angular Firebase 9](https://dev.to/jdgamble555/angular-12-with-firebase-9-49a0), but the theory is the same in all Firebase frameworks and in Version 8.

```typescript
userRef = doc(this.afs, 'users', 'CnbasS9cZQ2SfvGY2r3b');

this.posts = collectionData<Post>(
  query(
    collection(this.afs, 'posts'),
    where('userDoc', '==', userRef),
    orderBy('createdAt')
  ), { idField: 'id' }
);
```

So what is the point of that? Actually, nothing. I couldn't really find an advantage. You could just as easily store and search for a document ID. Please let me know if someone finds this useful... lol.

However...

## Querying

While browsing the inner-deep-hole of stackoverflow, I found [this post](https://stackoverflow.com/a/47673346/271450). Someone wrote in the comments that they wish Firebase populated these documents automatically. So I figure, why not? Then I realized how useful this is going to be!

## Code

### Doc

```typescript
expandRef<T>(obs: Observable<T>, fields: any[] = []): Observable<T> {
  return obs.pipe(
    switchMap((doc: any) => doc ? combineLatest(
      (fields.length === 0 ? Object.keys(doc).filter(
        (k: any) => {
          const p = doc[k] instanceof DocumentReference;
          if (p) fields.push(k);
          return p;
        }
      ) : fields).map((f: any) => docData<any>(doc[f]))
    ).pipe(
      map((r: any) => fields.reduce(
        (prev: any, curr: any) =>
          ({ ...prev, [curr]: r.shift() })
        , doc)
      )
    ) : of(doc))
  );
}
```

### Collections
```typescript
expandRefs<T>(
  obs: Observable<T[]>,
  fields: any[] = []
): Observable<T[]> {
  return obs.pipe(
    switchMap((col: any[]) =>
      col.length !== 0 ? combineLatest(col.map((doc: any) =>
        (fields.length === 0 ? Object.keys(doc).filter(
          (k: any) => {
            const p = doc[k] instanceof DocumentReference;
            if (p) fields.push(k);
            return p;
          }
        ) : fields).map((f: any) => docData<any>(doc[f]))
      ).reduce((acc: any, val: any) => [].concat(acc, val)))
        .pipe(
          map((h: any) =>
            col.map((doc2: any) =>
              fields.reduce(
                (prev: any, curr: any) =>
                  ({ ...prev, [curr]: h.shift() })
                , doc2
              )
            )
          )
        ) : of(col)
    )
  );
}

```
## Usage

Simply put `expandRef(...)` around your doc observable and `expandRefs(...)` around your collection observable. Done!

```typescript
this.posts = expandRefs(
  collectionData(
    query(
      collection(this.afs, 'posts'),
      where('published', '==', true),
      orderBy(fieldSort)
    ), { idField: 'id' }
  )
);
```
If I save `{ userDoc: ...some doc ref }` in a document, it will automatically grab that document, and set the values to the document data. (Make sure to import all the appropriate rxjs operators.)

## Update 9/11/21

I did some speed adjustments as well as added options to get rid of extraneous loops, and not throw an error if there are no documents! You can now input the `fields` you want to expand, which not only is another speed enhancement, but it also gives you options if you don't want to expand all fields! Simply input all fields you want to expand in the second argument. It works for both functions!

```typescript
this.posts = expandRefs(
  collectionData(
    query(
      collection(this.afs, 'posts'),
      where('published', '==', true),
      orderBy(fieldSort)
    ), { idField: 'id' }
  ),
  ['authorDoc', 'imageDoc']
);
```

## Promise

Don't forget you can get the promise version with
`.pipe(take(1)).toPromise();` at the end!

This is a simple JOIN. Amazing!

You're welcome.

J