Christopher Kiessling
Christopher Kiessling

Reputation: 413

Get merged observable from id with rxjs Angular Firestore

I've a Firestore DB with persons and pets, pets has owner id.

// Collection / DocumentID: {data}

persons / 'person1': { name: 'Romea' }
persons / 'person2': { name: 'Julieto' }
pets / 'pet1': { name: 'Forky', ownerID: 'person1' }
pets / 'pet2': { name: 'Rasky', ownerID: 'person1' }
pets / 'pet3': { name: 'Tursy', ownerID: 'person2' }

so, Romea owns Forky and Rasky when Julieto owns Tursky.

I'm using @angular/fire and I want to use a recent version of rxjs (6.5.x) to get a list of users observables with their pets each, see following structure:

[
   0: { 
         name: 'Romea',
         pets: [ 0: { name: 'Forky', ownerID: 'person1' }, 1: { name: 'Rasky', ownerID: 'person1' } ]
      },
   1: {
         name: 'Julieto',
         pets: [ 0: { name: 'Tursky', ownerID: 'person2' } ]
      }
]

After all, I want to call person.pets, like that:

this.personService.getPersonsWithPets().subscribe(persons => {
   console.log(persons[0].pets)
   // result: [{ name: 'Forky', ownerID: 'person1' }, { name: 'Rasky', ownerID: 'person1' }]

   persons.forEach(person => person.pets.forEach(pet => console.log(pet.name)))
   // result: 'Forky', 'Rasky', 'Tursky'
})

SOLUTION

According to @bryan60's solution, this is what I've done (rxjs plus a class that receives in constructor both person and pets):

service:

private personsCollection: AngularFirestoreCollection<Person> = this.db.collection<Person>('persons')

private _getPersonsWithoutPets(): Observable<Person[]> {
   return this.personsCollection.valueChanges()
}

getPersons(): Observable<(Person)[]> {
   return this._getPersonsWithoutPets().pipe(
      switchMap(persons => {
         return combineLatest(persons.map(person => this.getPersonPets(person.id).pipe(
            map(pets => new Person(person, pets))
         )))
      })
   )
}

getPersonPets(personId: string): Observable<Pet[]> {
   return this.db.collection<Pet>('pets', ref => ref.where('personId', '==', personId)).valueChanges()
}

models:

export class Person {
   id?: string
   name: string

   public constructor(person: Person = null, private pets: Pet[] = null) {
       Object.assign(this, person)
       this.pets = pets
   }

   model?() {
      return {
         id: this.id,
         name: this.name
      }
   }
}

export class Pet {
   id?: string
   name: string

   constructor(pet: Pet = null) {
      Object.assign(this, pet)
   }

   model?() {
      return {
         id: this.id,
         name: this.name
      }
   }
}

component:

personsWithPets: Person[] = []

constructor(private personService: PersonService) {
   this.getPersons()
}

getPersons() {
   this.personService.getPersons().subscribe(p => this.personsWithPets = p)
}

Solution for situations where persons has multiple references (pets, cars):

getPersonsWithReferences(): Observable<Person[]> {
  return this.getPersons().pipe(switchMap(persons =>
    combineLatest(persons.map(person =>
      combineLatest(this.getPersonPets(person.id), this.getPersonCars(person.id))
        .pipe(map(([pets, cars]) => new Environment(person, pets, cars)))))))
}

Upvotes: 2

Views: 129

Answers (2)

bryan60
bryan60

Reputation: 29325

this is a switchMap use case.

supposing you had functions getPersons() and getPersonsPets(ownerID), which do what their names suggest, do it like this:

getPersonsWithPets() {
  return this.getPersons().pipe(
    switchMap(persons => {
      return combineLatest(persons.map(person => this.getPersonsPets(person.id).pipe(
        map(pets => Object.assign(person, {pets}))
      )))
    })
  )
}

first get persons, switchMap into combineLatest of persons mapped into a stream of their pets and assign the pets to the person, and return a list of the people with their pets.

with more assignments:

getPersonsWithPets() {
  return this.getPersons().pipe(
    switchMap(persons => {
      return combineLatest(persons.map(person => 
        combineLatest(
          this.getPersonsPets(person.id),
          this.getPersonsCars(person.id),
          this.getPersonsHouses(person.id)
        ).pipe(
          map(([pets, cars, houses]) => Object.assign(person, {pets, cars, houses}))
        )
      ))
    })
  )
}

Upvotes: 3

AVJT82
AVJT82

Reputation: 73357

You can fetch your both collections using forkJoin, where you get an array of your data, both all persons and pets. Then you can modify for your data, and combine these data in the manner you like, i.e all pets with owner x should be added to the object of the person x. If you are looking for watching real time updates from db, I suggest using combineLatest instead.

Here is a sample, but you need to apply it to your usecase with angularfire and use the doc refs accordingly :)

allData = [];

// added an id property here for this sample, you will use the doc ref
persons = [{ id: 1, name: "Romea" }, { id: 2, name: "Julieto" }];
pets = [
  { name: "Forky", ownerID: 1 },
  { name: "Rasky", ownerID: 1 },
  { name: "Tursy", ownerID: 2 }
];

ngOnInit() {
  // in your usecase, combine your two queries
  forkJoin(of(this.persons).pipe( /** here map accordingly **/), 
           of(this.pets).pipe(/** here map accordingly **/)).pipe(
      // DON'T USE ANY, type your data!
      map((data: [any, any]) => {
        const persons = data[0];
        const pets = data[1];
        // here combine the data
        return persons.map(person => {
          const personPets = pets.filter(p => p.ownerID === person.id);
          return { ...person, pets: [...personPets] };
        });
      })
    )
    .subscribe(d => this.allData = d);
}

here map accordingly just means that in your code you need to map the values based on how you are making the queries, assumingly using snapshotChanges.

STACKBLITZ of the above code

Upvotes: 1

Related Questions