0

I have a Firebase Realtime Database containing the following:

"user": 
1: {
  "id": 1,
  "name": "Jason"
}

"receipts":
1: {
  "id": 1,
  "store_id": 1,
  "user_id": 1,
  "product": "apples"
},

2: {
   "id" : 2,
   "store_id": 2,
   "user_id": 1,
   "product": "oranges"
}

I simplified it but it has a similar structure to this. I am trying to list in a mat-table the Receipts with a given store_id using a query from AngularFireDatabase equalTo("given store_id") in a MatTable in Angular, but I do not want to show the user_id in the table, I want to show the user_name that I can get by querying "user" with a user_id to get the name.

I am doing something like this:

receiptDetails = new MatTableDataSource();

...

this.storeService.getReceiptsByStoreId(store_id).subscribe(result => {
  let tempArray:any = []
  result.forEach((element) => {
    let object:any = element.payload.val()
    let user_name:any;
    
    this.storeService.getUserNameByUserId(object.user_id).subscribe(result => user_name = result)

    let receipt:any = {
      id: object.id,
      product: object.product,
      user_id: user_name
    }

    tempArray.push(receipt)
  });

  this.receiptDetails.data = tempArray
})

In the .html, everything in the dataSource shows correctly but the user_name, which is undefined. If i console.log(user_name) inside the second subscription, it shows correctly. But I see the console.log(user_name) inside the second subscription executes after the object "receipt" I create is pushed into the tempArray, therefore it is undefined when it gets pushed into the array.

The methods in the storeService I use look like this:

public getReceiptsByStoreId(store_id: number) {
  return this.db.list('/receipts', ref => ref.orderByChild('store_id').equalTo(store_id)).snapshotChanges()
}

public getUserNameByUserId(user_id: number) {
  return this.db.object('/user/'+ user_id).snapshotChanges().pipe(map(user=>{
    let object:any=user.payload.val();
    let name=object.name;

    return name;
  }))
}

I do not think mergeMap is working here, I've tried it and I had no success with it. Any ideas on what approach I should have in order to not show user_id in the table, but the user_name provided by the query?

keesiah
  • 1
  • 1
  • https://stackoverflow.com/questions/65344100/is-there-a-more-efficient-way-to-have-a-subscription-in-a-subscription-in-typesc – Eliseo May 27 '21 at 16:55
  • @Eliseo thank you very much for your answer. Unfortunately, I adapted all the answers to my project to no avail, none of them get any data for me to show in the table. – keesiah May 27 '21 at 18:01
  • I wrote an answer. Really is a few different that the link i indicated, I hope the comments help you to understand the code. NOTE: Don't worry about the rxjs operators. In this example are the most common you need: switchMap, forkJoin and map (really there're much more but the most used are only these three) – Eliseo May 27 '21 at 20:32

2 Answers2

0

When you need to do a foreach on the result of the outer observable and use an inner observable inside the foreach, this is usually a good use case for mergemap.

You could try something like:

this.storeService.getReceiptsByStoreId(store_id)
  .pipe(
    mergemap(receipt => addUsername(receipt))
  )
  .subscribe(val => this.receiptDetails.data = val)

const addUsername = (receipt) => {
  return this.storeService.getUserNameByUserId(receipt.user_id)
           .pipe(
             map(username => { id: receipt.id, product: receipt.product, user_id: username })
           )
}

See example here - https://stackblitz.com/edit/rxjs-gxktnh?file=index.ts

garethb
  • 3,951
  • 6
  • 32
  • 52
  • I tried to integrate your example within my project. Unfortunately, the mergeMap(receipt => addUsername(receipt)) does not pass a receipt to the addUsername function, but the array of receipts. I can access the array inside addUsername with receipt[0].payload.val() for example for the first receipt in the array of receipts. So I think mergeMap does not work as it is supposed to here? – keesiah May 27 '21 at 15:14
  • I have now realized that I think mergeMap is not working because getReceiptsByStoreId returns an array of receipts with length 0 but with receipts inside of it. – keesiah May 27 '21 at 16:52
  • I think you need to find out first why your firebase queries aren't returning the data you expect. Get the data you expect in the way you expect it first then worry about the rest. – garethb May 28 '21 at 03:52
0

In this case is a bit more complex that the indicate because we don't want to make so many calls to get the data users, else only as many calls as different users we get

I wrote several comments. The steps are

  1. Call to get all the "ReceptsByStoreId"
  2. Get an array with the uniq user_Id
  3. Create an array of observables and use forkJoin
  4. use map to return the ReceptsByStore replace the property user_id by the name of the user

  ngOnInit()
  {
    const result$=this.dataService.getReceiptsByStoreId(1).pipe(
       switchMap((res:any[])=>{
         //in res we has the getReceiptsByStore

         //we whan an unique users
         //I use reduce, you can use others ways
         const uniqUsers:number[]=res.reduce((a:number[],b:any)=>a.indexOf(b.user_id)<0?[...a,b.user_id]:a,[])

         //uniqUsersnumber is an array with the name of the users

         //we create an array of observables
         const obs=uniqUsers.map(x=>this.dataService.getUserNameByUserId(x))

         return forkJoin(obs).pipe(map((users:any[])=>{
            //en users We has all the users "implicated"

            //but we are going to return the getReceiptsByStore
            //replacing the property user_id by the name of the user
            return res.map(x=>({
              ...x,
              user_id:users.find(u=>u.id==x.user_id).name
            }))
         }))
       })
    )
    //we can use 
        this.dataSource=result$
    //or we can subscribe
    //  result$.subscribe(res=>{
            this.dataSource=res
        })
   }

In this stackblitz I simulate the call to an API using "of" in the service

NOTES about the code 1.-the reduce function to get uniques values, reduce is the way

 myarray.reduce(valueAnterior,elementOf theArray,function(),valueInitial)

As valueInitial, I wrote an empty array, the function, add the element.user_id if not is in the valueAnterior (to add the element I use the "spread" operator

2.-when return res, we return res.map -is a map of array, not the rxjs- and return an object with all the properties of each element of res but I change the value of user_id by the name of the user. we can also create a new variable

NOTE:I imagine a service that return data in a determinate way. we can use console.log(res) to know what do you received. The code I wrote return and array of objects. Is possible we received an object like

{data:[here-the array]}

So the code don't work. We can then change our code like

   const result$=this.dataService.getReceiptsByStoreId(1).pipe(
       switchMap((res:any)=>{
         //see that we use res.data
         const uniqUsers:number[]=res.data.reduce(...)
        ....
   )

Or we can change the data that received form our services. For me is the best option because if, imagine, you change your dbs and know is not a Firebase Realtime else a MySQL with an API in .NET, the only you need change is the service, not the component. So

Update ::glups:: Your'e using FireBase, so if you makes a simple call you don't received an array

Really I'm not expert in FireBase, but reading tutorials, You has two services like

public getReceiptsByStoreId(store_id: number) {
  return this.db.list('/receipts', ref => ref.orderByChild('store_id').equalTo(store_id)).snapshotChanges()
}

public getUserNameByUserId(user_id: number) {
  return this.db.object('/user/'+ user_id).snapshotChanges().pipe(map(user=>{
    let object:any=user.payload.val();
    let name=object.name;

    return name;
  }))
}

We are going to change your services to return and array of "receipt" and a whole "user". It's better transform the service that use the component. This allow a more "scalable" application -Imagine you decided change the dbs, it's better change one service that several components

public getReceiptsByStoreId2(id: number) {
    return this.db.list('/receipts', ref => ref.orderByChild('store_id').equalTo(store_id)).snapshotChanges()
      .pipe(
        map((recepSnapshot: any[]) => {
          return recepSnapshot.map((recepData: any) => {
            const data = recepData.payload.val();
            return {
              id: recepData.payload.key,
              ...data
            };
          });
        })
      );
  }

And

public getUserNameByUserId(user_id: number) {
  return this.db.object('/user/'+ user_id).snapshotChanges().pipe(map(user=>{
    return user.payload.val(); //<--return the whole user
  }))
}

(*)Disclamer, I don't know about fireBase (I only just read a simple tutorial and I don't check the code -I only hope the code help to understand how works with FireBase-)

Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • On `getReceiptsByStoreId`, I was able to retrieve the actual values from `res:any[]` by calling a `forEach (element)` on it and adding `element.payload.val()` to a local array, then calling `reduce` on that one. Afterwards, when doing the `forkJoin`, the `users:any[]` is empty, so I think the `getUserNameByUserId` method is not working as intended. `getUserNameByUserId` does retrieve the Firebase object and it is returning a DataSnapshot, on which i would have to call `payload.val()` to see the actual value. But it is empty, so I cannot get any value from `users:any[]` . – keesiah May 27 '21 at 21:50
  • Also, on `console.log(obs)` I see in console an Observable Array that shows Array length 0 but it has items in it! I think because it gets populated later, that is why `users:any[]` is empty. – keesiah May 27 '21 at 22:10
  • @keesiah, Really I don't know about FireBase, I imagine the problem is that the services don't return an array of "recepit" and a "whole user" object. I think that the better bet is use `pipe(map)` in the service to return the desired object. I updated the answer but be carefully, I'd never use FireBase, so the answer can be plenty of errors (apologies) – Eliseo May 28 '21 at 09:53