0

Is this a limitation to Firebase or am I doing this all wrong? Everything works until I add the db.collection('users').doc(friendId).get()... in the middle of the code. Thanks in advance.

const db = admin.firestore();
const friendRef = db.collection('users').doc(id).collection('friends');

friendsList = [];

friendRef.get().then((onSnapshot) => {
    if (!onSnapshot.empty) {
        onSnapshot.forEach((friend) => {

            const friendId = String(friend.data().person_id);

            db.collection('users').doc(friendId).get().then((result) => {
                const firstName = String(result.data().name.first);
                const lastName = String(result.data().name.last);
            })

            const data = {
                personId: friendId,
                firstName: firstName,
                lastName: lastName,
            }

            friendsList.push(data);
        })

        res.send(friendsList);
    } else {
        res.send({
            'message': 'no friends'
        });
    }
}).catch((e) => {
    res.send({
        'error': e
    });
})
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807

1 Answers1

3

Data is loaded from Firestore asynchronously. This means that by the time you're sending the response back to the client, the data hasn't loaded from Firestore yet.

The easiest way to see this is with some well placed logging statements:

console.log("Before getting friend");
db.collection('users').doc(friendId).get().then((result) => {
  console.log("Got friend");
})
console.log("After getting friend");

When you run just this code it'll print:

Before getting friend

After getting friend

Got friend

That is probably not the order you expected the logs to be in. The reason is that the data may take some time to come back from Firestore. So instead of blocking the thread, it continues running the thread and then calls your callback function when the data is available. And that unfortunately means that your res.send(friendsList) ends up sending an empty list back to the client, since the data hasn't loaded yet.

The solution to this is to use a bunch of nested callbacks, to use Promise.all(), or ES6's new async/await keyword. With promises the code looks like this:

const db = admin.firestore();
const friendRef = db.collection('users').doc(id).collection('friends');

friendRef.get().then((onSnapshot) => {
  var promises = [];

  onSnapshot.forEach((friend) => {
    const friendId = String(friend.data().person_id);    
    promises.push(db.collection('users').doc(friendId).get());
  });

  Promise.all(promises).then((snapshots) => {
    friendsList = [];
    snapshots.forEach((result) => {
      friendsList.push({
        personId: result.id,
        firstName: result.data().name.first,
        lastName: result.data().name.last,
      });
    });
    res.send(friendsList);
  });
}).catch((e) => {
  res.send({
    'error': e
  });
})

So we first build a list of all friend read operations, then once all of those are done, we build the response and send it back.

Community
  • 1
  • 1
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Awesome, it worked. I understand now with your explanation. Thanks again. I would love to use ES6 for this. If you have time and don't mind, can you send it using ES6 async/await, so I get a sense of it. I will start learning ES6 with Google Functions. Thanks in advance. –  Dec 29 '17 at 01:16
  • Hi Frank. Everything works except I am not getting output for `personId: snapshot.id` for some reason. I tried a few things: `snapshots.id` and `result.id`, but that didn't work either. This is the id for `onSnapshot.forEach((friend)...` and should be `friend.id`. Thanks again. –  Dec 29 '17 at 04:21
  • Your code didn't show how you initialized `friendId`, so I assumed it's the document ID: https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot#~id. If that's not the case, you'll need to modify that part of the solution to suit your needs. – Frank van Puffelen Dec 29 '17 at 16:00
  • Thank you again @FrankvanPuffelen for help. I still can't figure out how to push the document id from Friends collection to the friendsList array. I asked a new question because I thought it might be hard to explain it in a comment. [firestore-with-promises-and-push](https://stackoverflow.com/questions/48038490/firestore-with-promises-and-push). –  Dec 30 '17 at 23:01
  • Ah... it should've been `result.id`, I just updated the code. The `result.id` is the ID of the document that you loaded. And since you load the documents based on `friendId`, it'll be the same value as `friendId`. – Frank van Puffelen Dec 31 '17 at 00:38