1

I'm creating a StencilJS app (no framework) with a Google Firestore backend, and I want to use the RxFire and RxJS libraries as much as possible to simplify data access code. How can I combine into a single observable stream data coming from two different collections that use a reference ID?

There are several examples online that I've read through and tried, each one using a different combination of operators with a different level of nested complexity. https://www.learnrxjs.io/ seems like a good resource, but it does not provide line-of-business examples that make sense to me. This question is very similar, and maybe the only difference is some translation into using RxFire? Still looking at that. Just for comparison, in SQL this would be a SELECT statement with an INNER JOIN on the reference ID.

Specifically, I have a collection for Games:

{ id: "abc000001", name: "Billiards" },
{ id: "abc000002", name: "Croquet" },
...

and a collection for Game Sessions:

{ id: "xyz000001", userId: "usr000001", gameId: "abc000001", duration: 30 },
{ id: "xyz000002", userId: "usr000001", gameId: "abc000001", duration: 45 },
{ id: "xyz000003", userId: "usr000001", gameId: "abc000002", duration: 55 },
...

And I want to observe a merged collection of Game Sessions where gameId is essentially replace with Game.name.

I current have a game-sessions-service.ts with a function to get sessions for a particular user:

import { collectionData } from 'rxfire/firestore';
import { Observable } from 'rxjs';
import { GameSession } from '../interfaces';

observeUserGameSesssions(userId: string): Observable<GameSession[]> {

    let collectionRef = this.db.collection('game-sessions');
    let query = collectionRef.where('userId', '==', userId);

    return collectionData(query, 'id);
}

And I've tried variations of things with pipe and mergeMap, but I don't understand how to make them all fit together properly. I would like to establish an interface GameSessionView to represent the merged data:

export interface GameSessionView {
    id: string,
    userId: string,
    gameName: string,
    duration: number
}

observeUserGameSessionViews(userId: string): Observable<GameSessionView> {

    this.observeUserGameSessions(userId)
    .pipe(
        mergeMap(sessions => {
            // What do I do here? Iterate over sessions 
            // and embed other observables for each document?
        }
    )
}

Possibly, I'm just stuck in a normalized way of thinking, so I'm open to suggestions on better ways to manage the data. I just don't want too much duplication to keep synchronized.

Geoffrey
  • 61
  • 4
  • you can array functions to do that instead of rxjs – Fan Cheung May 07 '19 at 14:34
  • @FanCheung I'm not sure what that means, "array functions." Isn't that essentially what `pipe(...)` does? I'm just not sure what - or how - to chain things together. RxJS has so many building blocks, and I'm fairly new to JavaScript as it is. – Geoffrey May 08 '19 at 17:43
  • I've gone through my data model and denormalized quite a bit just to work around this issue. I understand normalization isn't necessarily the way to go in a NoSql setup, but it would be good to know if this sort of "join" can be done client-side with RxJS. – Geoffrey May 10 '19 at 18:43

1 Answers1

0

You can use the following code (also available as Stackblitz):

const games: Game[] = [...];
const gameSessions: GameSession[] = [...];

combineLatest(
  of(games),
  of(gameSessions)
).pipe(
  switchMap(results => {
    const [gamesRes, gameSessionsRes] = results;
    const gameSessionViews: GameSessionView[] = gameSessionsRes.map(gameSession => ({
      id: gameSession.id,
      userId: gameSession.userId,
      gameName: gamesRes.find(game => game.id === gameSession.gameId).name,
      duration: gameSession.duration
    }));
    return of(gameSessionViews);
  })
).subscribe(mergedData => console.log(mergedData));

Explanation:
With combineLatest you can combine the latest values from a number of Obervables. It can be used if you have "multiple (..) observables that rely on eachother for some calculation or determination".
So assuming you lists of Games and GameSessions are Observables, you can combine the values of each list.
Within the switchMap you create new objects of type GameSessionView by iterating over your GameSessions, use the attributes id, userId and duration and find the value for gameName within the second list of Games by gameId. Mind that there is no error handling in this example.
As switchMap expects that you return another Observable, the merged list will be returned with of(gameSessionViews).
Finally, you can subscribe to this process and see the expected result.

For sure this is not the only way you can do it, but I find it the simplest one.

pschild
  • 2,918
  • 1
  • 19
  • 26