0

I have a food app with a star system. Every time a user selects a rating/star I send their rating to the database and then run a firebase transaction to update the total amount of users who left a rating.

let theirRating: Double = // whatEverStarRatingTheySelected
let dict = [currentUserId: theirRating]

let reviewsRef = Database.database().reference().child("reviews").child(postId)
reviewsRef.updateChildValues(dict) { (error, ref) in

    let postsRef = Database.database().reference().child("posts").child(postId).child("totalUsersCount")
    postsRef.runTransactionBlock({ (mutableData: MutableData) -> TransactionResult in
        // ...
    })
}

When displaying the actual rating I pull the the post which has the totalUsersCount as a property, I pull all of the users actual ratings, add them all together, and then feed both numbers into an algo to spit out the actual rating. I do all of this on the client. How can I do the same thing "I pull all of the users actual rating, add them all together" with a Cloud Function, that is similar to this answer?

Database.database().reference().child("posts").child(postId).observeSingleEvent(of: .value, with: { (snapshot) in
 
     guard let dict = snapshot.value as? [String:Any] else { return }

     let post = Post(dict: dict)
 
     let totalUsers = post.totalUsersCount

     Database.database().reference().child("reviews").child(postId).observeSingleEvent(of: .value, with: { (snapshot) in
      
          var ratingSum = 0.0
      
          // *** how can I do this part using a Cloud Function so that way I can just read this number as a property on the post instead of doing all of this on the client ***
          for review in (snapshot.children.allObjects as! [DataSnapshot]) {
              guard let rating = review.value as? Double else { continue }
              ratingSum += rating
          }
      
          let starRatingToDisplay = self.algoToComputeStarRating(totalUsersCount: totalUsersCount, ratingsSum: ratingSum)
     })
 })

This isn't a question about the algoToComputeStarRating, that I can do on the client. I want to add all of the user's ratings together using a Cloud Function, then just add that result as a property to the post. That way when I pull the post all I have to do is:

Database.database().reference().child("posts").child(postId).observeSingleEvent(of: .value, with: { (snapshot) in

    guard let dict = snapshot.value as? [String:Any] else { return }

    let post = Post(dict: dict)
         
    let totalUsersCount = post.totalUsersCount
    let ratingsSum = post.ratingsSum
    
    let starRatingToDisplay = self.algoToComputeStarRating(totalUsersCount: totalUsersCount, ratingsSum: ratingSum)
})

Database structure:

@posts
   @postId
      -postId: "..."
      -userId: "..."
      -totalUsersCount: 22
      -ratingsSum: 75 // this should be the result from the cloud function

@reviews
    @postId
       -uid_1: theirRating
       -uid_2: theirRating
       // ...
       -uid_22: theirRating

This is what I tried so far:

exports.calculateTotalRating = functions.https.onRequest((data, response) => {

    const postId = data.postId;
    
    const totalUsersCtRef = admin.database().ref('/posts/' + postId + '/' + 'totalUsersCt');
    const postsRef = admin.database().ref('/posts/' + postId);

    admin.database().ref('reviews').child(postId).once('value', snapshot => {

        if (snapshot.exists()) {

            var ratingsSum = 0.0;

            snapshot.forEach(function(child) {

                ratingsSum += child().val()
            })
            .then(() => { 
            
                return postsRef.set({ "ratingsSum": ratingsSum})          
            })
            .then(() => { 
            
                return totalUsersCtRef.set(admin.database.ServerValue.increment(1));                
            }) 
            .catch((error) => {
                console.log('ERROR - calculateTotalRating() Failed: ', error);
            });
        }
    });
});
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • This is so common case, that firebase docs even has a manual for it: https://firebase.google.com/docs/firestore/solutions/counters – Dmytro Rostopira Feb 02 '21 at 14:02
  • I'm not a native Javascript/Node developer. If this was Swift I could figure this out in breeze, but I have to do a ton of research to figure this out in Javascript, that's why I asked the question. – Lance Samaria Feb 02 '21 at 14:05

2 Answers2

1

I got it to work using this answer:

exports.calculateTotalRating = functions.https.onRequest((data, response) => {

    const postId = data.postId;
    
    const postsRef = admin.database().ref('/posts/' + postId);
    const totalUsersCtRef = admin.database().ref('/posts/' + postId + '/' + 'totalUsersCt');

    var ratingsSum = 0.0;

    admin.database().ref('reviews').child(postId).once('value', snapshot => {

        if (snapshot.exists()) {

            snapshot.forEach((child) => {

                ratingsSum += child().val()
            })
            console.log('ratingsSum: ', ratingsSum);

            return postsRef.update({ "ratingsSum": ratingsSum }).then(() => { 
                return totalUsersCtRef.set(admin.database.ServerValue.increment(1));                
            })
            .catch((error) => {
                console.log('ERROR - calculateTotalRating() Failed: ', error);
            });   
        }
    });
});
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
0

I think this should provide you a good start. I have used a trigger so all the updates will happen automatically and your swift listener will immediately get the new ratings. :) Though I am not sure how your let post = Post(dict: dict) is going to parse the [String: Any] dictionary.


//We are adding a event listner that will be triggered when any child under the reviews key is modified. Since the key for updated review can be anything we use wildcards `{autoId}`.
exports.calculateTotalRating = functions.database
  .ref("reviews/{autoId}")
  .onUpdate((snapshot, context) => {


    if (snapshot.exists()) { //For safety check that the snapshot has a value
      const ratingsDict = snapshot.val(); //The updated reviews object you set using `reviewsRef.updateChildValues(dict)`

      const autoId = context.params.autoId; //Getting the wildcard postId so we can use it later 

      //Added due to error in understanding the structure of review object. Can be ignored. 
      //delete ratingsDict("totalUserCount");
      //delete ratingsDict("ratingsSum");

      const totalUsers = Object.keys(ratingsDict).length; //Counting how many reviews are there in the ratingsDict 

      //Sum over all the values in ratingsDict. This approach is supposedly slower, but was oneliner so used it. Feel free to modify this with a more efficient way to sum values once you are more comfortable with cloud functions.
      const ratingsSum = Object.keys(ratingsDict).reduce(
        (sum, key) => sum + parseFloat(ratingsDict[key] || 0),
        0
      );  

      //Saving the computed properties.
      ratingsDict["totalUserCount"] = totalUsers;
      ratingsDict["ratingsSum"] = ratingsSum;

      //Updating posts with new reviews and calculated properties
      return functions.database.ref("posts").child(autoId).set(ratingsDict);
    }
  });

I feel like you are new to this, so you can follow this guide to create cloud functions. In your app's top level directory, just run firebase init functions and selecting your project from Use an existing project. This should create a functions folder for you with an index.js file where you can add this code. Then just run firebase deploy --only functions:calculateTotalRating.

Parth
  • 2,682
  • 1
  • 20
  • 39
  • thanks, let me add that Post to the question. Gimma a sec – Lance Samaria Feb 02 '21 at 14:23
  • I don't think that's needed. Do you follow the logic though? I may have slightly misunderstood your reviews object. And feel these lines are not needed, ` delete ratingsDict("totalUserCount"); delete ratingsDict("ratingsSum");`. But the rest should work. – Parth Feb 02 '21 at 14:25
  • thanks for the help. I added the `exports` that I came up with so far. Does this make it clearer for you? I'm not native Javascript so it's going to take me a minute to go through your answer. – Lance Samaria Feb 02 '21 at 14:31
  • I'm definitely new to Cloud Functions, I've only used them once before. I asked a question then later added an answer on how to set them up https://stackoverflow.com/a/65875956/4833705. The Javascript part on how to iterate through the reviews then update that result to the `posts/postId/ratingsSum` property is where I'm lost – Lance Samaria Feb 02 '21 at 14:36
  • Ah, no worries! Hope it's clear now? I have not tested it but let me know if you run into any issues. – Parth Feb 02 '21 at 14:42
  • I think you have the structure confused. Look at my question under **Database structure:** and **This is what I tried so far:**. In your answer, the reviewsRef uses an `autoId` but I have the actual `postId`. If I'm not mistaken, your `exports.` grabs the data from `totalUsers` from the `reviewRef`, but `totalUsers` is at the `postsRef`. I can't understand everything else that's going on in there, but the reviewsRef need to iterate through all of the `theirRating`, add them all together, then update that result to `posts/postId/ratingsSum`. `totalUsers` and `theirRating` are at different refs. – Lance Samaria Feb 02 '21 at 14:50
  • Okay so, `\{autoId}` is a wild card. This is done so that if post with postId post1 or post with postId post2 is modified, we will still be able to catch that event. Because it will be impossible to write functions for each postId, since we will be creating a new reference for each postId. – Parth Feb 02 '21 at 15:27
  • Next, I read all reviews for an updated postId which I get from `context.params.autoId`, and then add the review for each userID using `Object.keys(ratingsDict).reduce((sum, key) => sum + parseFloat(ratingsDict[key] || 0),0); `. This gives me `ratingsSum`. Then since I have the dictionary `[userId: String: rating: Int]` (from snapshot.val()), I count number of keys in this dictionary using `Object.keys(ratingsDict).length`, which gives me `totalUsersCount`. – Parth Feb 02 '21 at 15:30
  • Then I update the previous ratings dictionary with 2 new entires totalUsersCount, ratingsSum. And I save this new dictionary at `posts/{autoId}` (here again autoId is the postId which was modified.). I believe this is what you want. The only thing that confuses me is, in your question what does post/postId/ have only one userId key. Which user's id is this, and what is it's value? I think you want to have all the user ratings under post/postId along with 2 new fields totalUsersCount, ratingsSum, right? – Parth Feb 02 '21 at 15:34
  • 1
    When a user makes a post, I need to know which user posted it. The userID refers to that user. I’ll try your code in a few hours. Something just popped up that’s really important for me to take care of. You are correct about the 2 fields although I want to keep the actual ratings at a separate ref. The ratingsSum is just a total of all of the actual ratings added together. – Lance Samaria Feb 02 '21 at 15:40
  • Which means the postId under @ posts should be different from that under @ reviews, since you have 10 user reviews under @ reviews/postID but @ posts/postID has only 1 userId but a sum of all those 10 reviews. Will @ posts/postID/userID only have the id of the user that made the latest update? – Parth Feb 02 '21 at 15:43
  • The reviews are for the same exact postId, why would the reviews use a different id. How would I know which post that they are for? posts/postId/userId is only for the person who made the post. For example if I’m filtering posts to see if userA blocked the person who made the post, their post wouldn’t appear on userA’s device. – Lance Samaria Feb 02 '21 at 15:56
  • I think I'm looking for something along the lines of this iteration: https://stackoverflow.com/a/43444392/4833705 without the null value. – Lance Samaria Feb 02 '21 at 16:06
  • I am still confused, if you have 4 users and 4 ratings under reviews/postid. Then shouldn't there be 4 entries under post/postId? Which userId is at post/postId/userID? – Parth Feb 02 '21 at 16:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/228169/discussion-between-lance-samaria-and-parth-tamane). – Lance Samaria Feb 02 '21 at 16:14