2

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)))))))
}
4
  • Can you update your question with the code for getPersonsWithPets()? What does your initial query look like, You'll be working with that to get your result. Do you make one request or two requests? Commented Nov 20, 2019 at 19:32
  • @DamianC I've no service mothods, the question is how to do it Commented Nov 20, 2019 at 19:36
  • what does getting a persons pets look like on it's own and what does getting a person look like on it's own, and when do you want to do either? Commented Nov 20, 2019 at 19:39
  • One last thought here, being that this is a document DB, you should consider storing your data based on how you are going to be accessing it. This puts less pressure on the app to do it EVERY time you request it. Commented Nov 20, 2019 at 19:54

2 Answers 2

3

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}))
        )
      ))
    })
  )
}
Sign up to request clarification or add additional context in comments.

6 Comments

thanks, do you know how to do that with multiple observable references? let's say there's many collections with ownerID (pets, cars, houses) and I want to get a list of persons with their pets, cars and houses
nested combineLatest
got it: .pipe(switchMap(persons => combineLatest(/* get person with pets /).pipe(switchMap(persons => combineLatest(/ get person with cars */))))
could do it cleaner tbh, combineLatest already is a higher order observable so you don't need to switchMap.
updated my answer, just be aware you're building a very large stream here with a lot of requests in it if this list is long... 1 + (nPeople * nAssignments) http requests
|
1

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

Comments

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.