1

I have to get different values from different children at the same ref. In Swift I would do:

var sum = 0
var totalPosts = 0
var starRating = 0 // run some calculations on totalPosts + sum

let sneakersRef = Database.database().reference().child("sneakers").child(adidas_ABC) // postId
sneakersRef.observeSingleEvent(of: .value, with: { (snapshot) in

    if !snapshot.exists() { return }

    if snapshot.hasChild("postCount") {
        let childSnapshot = snapshot.childSnapshot(forPath: "postCount")
            if let postCount = childSnapshot.value as? Int {
                
               self.totalPosts = postCount
            }
        }
    }

    if snapshot.hasChild("fans") {

        let childSnapshot = snapshot.childSnapshot(forPath: "fans")

        for child in childSnapshot.children {

            let userId = child as! DataSnapshot

            for snap in userId.children {

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

                let price = dict["price"] as? Int ?? 0

                self.sum += price
            }

            // update ref with value obtained from sum and create/update a starRating
        }
    }
})

How would I do the same thing in Cloud Functions? The code inside the childSnapshot.forEach((child) => { } below is what I need help with.

exports.updateValues = functions.https.onCall((data, context) => {

    const postId = data.postId; // adidas_ABC

    var sum = 0;
    var totalPosts = 0;
    var starRating = 0;

    const sneakersRef = admin.database().ref('sneakers').child(postId);

    sneakersRef.once('value', snapshot => {

        if (snapshot.exists()) {

            const postCount = snapshot.child("postCount").val();

            totalPosts = postCount

            const childSnapshot = snapshot.child("fans").val().userID;

            // the code in this loop is questionable
            childSnapshot.forEach((snap) => {

                const price = snap.child("price").val();

                sum += price
            })

            // run calculations to get starRating

            return sneakersRef.update({ "sum": sum, "starRating": starRating }).then(() => {            
            })
            .catch((error) => {
                console.log('something went wrong: ', error);
            });
        } else {
            console.log('doesn't exist');
        }
    });
});

Here is my db layout:

@sneakers
   @adidas_ABC // same postId for anything dealing with adidas
      @fans // this might not exist in which case postCount would be zero
         @userId_XYZ
            @postId_123
               -condition: "used"
               -price: 100
               -type: shell_top
            @postId_456
               -condition: "new"
               -price: 500
               -type: yeezy
      -fanCount: 1
      -postCount: 2
      -sum: 600
      -starRating: 4 
Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • It is not clear to me what you expect to get with `snapshot.child("fans").val().userID`. It does not seem you have a node named `userID`. – Renaud Tarnec Mar 14 '21 at 09:45
  • Ahhhh, maybe that's the issue. I followed this answer and I thought that it was some sort of wildcard for a userID https://stackoverflow.com/a/43498731/4833705 – Lance Samaria Mar 14 '21 at 09:52
  • I thought that that would return anything under fans for example I thought that `snapshot.child("fans").val().userID` would get me `/fans/userId_XYZ` – Lance Samaria Mar 14 '21 at 09:54
  • I'll write an answer, as there is another key point that needs to be adapted. – Renaud Tarnec Mar 14 '21 at 09:54

1 Answers1

3

There are two things to adapt in your code:

  1. You need to loop over the user objects and then you need to loop over each post of each user object. See this SO answer for more details.
  2. You need to correctly manage the life cycle of your Cloud Function by returning the Promises chain. As you will see here in the doc, it is key to terminate Cloud Functions that perform asynchronous processing by returning a JavaScript promise. In your case we return the Promise chain, which is a Promise, and which is composed of the Promises returned by the asynchronous Firebase methods (once() and update()) and by then(). For that we need to use the "promisified" version of the once() method (i.e. once('value').then((snapshot) => {..})) and not the callback version, which you used in your code.

So the following should do the trick:

exports.updateValues = functions.https.onCall((data, context) => {
    const postId = data.postId; // adidas_ABC

    var sum = 0;
    var totalPosts = 0;
    var starRating = 0;

    const sneakersRef = admin.database().ref('sneakers').child(postId);

    return sneakersRef   // Note the return here!! => We return the Promise chain
        .once('value')
        .then((snapshot) => {

            if (snapshot.exists()) {
                const postCount = snapshot.child('postCount').val();

                totalPosts = postCount;

                const childSnapshot = snapshot.child('fans').val();

                Object.keys(childSnapshot).forEach((user) => {
                    const obj = childSnapshot[user];
                    Object.keys(obj).forEach((e) => {
                        const price = obj[e].price;
                        sum += price;
                    });
                });

                // I let you dealing with the calculations to get starRating

                return sneakersRef.update({ sum: sum, starRating: starRating });
            } else {
                console.log("doesn't exist");
                return null; // Note the return here
            }
        })
        .catch((error) => {
            console.log('something went wrong: ', error);
            return null; // Note the return here
        });

});
Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
  • I based my code off of this answer https://stackoverflow.com/a/66202744/4833705 (I also wrote the question). The code works but is much simpler because for that question I did not need to get any childSnapshots/grandChildsnapshots. The `promise` is constructed different from yours but it does work as I haven't had any issues. My question to you is why does the `promise` from that answer work even though you said that the way i initially did it in my question above was incorrect? – Lance Samaria Mar 14 '21 at 10:35
  • I will answer later, not in front of my computer the moment! – Renaud Tarnec Mar 14 '21 at 10:49
  • ok thanks again, and extra thanks for pointing out `return null;`, I've looked at a ton of answers and I don't recall seeing anyone do that. – Lance Samaria Mar 14 '21 at 10:51
  • 1
    In your previous [code](https://stackoverflow.com/questions/66202383/firebase-cloud-functions-snapshot-foreach-then-is-not-a-function/66202744#66202744) you are also incorrectly managing the life cycle of your CF, because you don't return a Promise. Have a look at this [answer](https://stackoverflow.com/a/66425014/3371862) (or [this one](https://stackoverflow.com/questions/63197863/firebase-cloud-transactions-not-always-finishing/63197988#63197988)) where I explain that **sometimes**, the CF platform does not terminate the Function immediately and the asynchronous work can be completed... – Renaud Tarnec Mar 14 '21 at 12:51
  • Ahhhh, ok, thanks for letting me know that. I almost placed that code into a production app, luckily I'm still testing :) – Lance Samaria Mar 14 '21 at 12:55
  • We do `return null;` to, again, return a Promise in the CF. Actually the `catch()` function returns a Promise, like the `then()` one. You could very well do `return 1;` or `return 'abcd';`, or any other constant. You will find some examples of doing that in the **official** repository of CF samples, for example [here](https://github.com/firebase/functions-samples/blob/5614dd71cddbe27512b41d662cc51d8d25f2a562/remote-config-diff/functions/index.js). – Renaud Tarnec Mar 14 '21 at 12:59
  • 1
    BTW, you may consider using a Transaction to update the prices sum. It depends on the exact function of your app, but if several users can concurrently modify a node, it would be better to use a Transaction. – Renaud Tarnec Mar 14 '21 at 13:02
  • 1
    cool, this is good stuff to know. It’s going take a while digest it all. I’m a Swift developer and Promises are very foreign to me. I have to learn though, one way or another. – Lance Samaria Mar 14 '21 at 13:03
  • You're right about a transaction. I forgot about that. I was so busy trying to figure out where I was going wrong with this. I'm going to adjust the code for it now! – Lance Samaria Mar 14 '21 at 13:04
  • Hi, I've been trying to go over Promises but I can't get a good grasp of it. I've been looking at the code that you posted and one thing that has me perplexed is this line `const price = obj[e].price;` The `obj` is the user, the `e` represents a `post`, and `price` is property, but what I can't understand is why did you have to add the `obj` in front of the brackets. Wouldn't this achieve the same exact thing: `const price = e.price;`? Notice I excluded the `obj[]` – Lance Samaria Mar 19 '21 at 13:54
  • "Wouldn't this achieve the same exact thing" => No, we are looping over the **keys** of the Object `obj`, so if we want to get the value, we need to do `obj[e]` (`e` holding the key value). On that we call the `price` property. – Renaud Tarnec Mar 19 '21 at 14:04
  • This is one of the places where I'm lost. If `e` is each individual post, we have the actual post itself. As the loop runs why can't we just pull the property on the `post` itself? Unless `e` isn't a post – Lance Samaria Mar 19 '21 at 14:07
  • Taking it a step back `const obj = childSnapshot[user];` must be the `post` then? I though it was the `user`. So `e` must be each key and we're looking for the price key? Basically `const price = obj[e].price;` is the same thing as saying `const price = post[key].price` Correct? – Lance Samaria Mar 19 '21 at 14:13
  • The **direct** properties (child nodes) of `fans` are the users (userId_XYZ, userId_ABC, userId_DEF, ...) For each if these users you may have several posts. So you need two loops (over users, and for each user, over posts). This is what I derived from the data model in your question. If I misunderstood the data model, you shoudl adapt this part of the asnwer! :-) – Renaud Tarnec Mar 19 '21 at 14:20
  • No no no, everything is fine on your part, it's my part that's having the issue. I'm trying to get a grasp on this. It's much different then firebase on the iOS/ Swift client. The problem here is I thought that `obj` was a user. – Lance Samaria Mar 19 '21 at 14:22
  • No worries :-) I do understand that you "are trying to get a grasp on this" and I'm happy to help you! I just explained what is the rationale of the proposed solution, based on my understanding. But my understanding could also very well be wrong! – Renaud Tarnec Mar 19 '21 at 14:27
  • I got it now. You loop over the users, then you loop over the keys of each post. See I thought it was you loop over the user, then you loop over each post, not each `key`. That's where I was lost => looping over each key .Thanks for the answer! :) – Lance Samaria Mar 19 '21 at 14:29