1

I'm still struggling to understand how to extract values from a Firestore Query and put them into a global variable. I (now) understand that the asynchronous nature means that code isn't executed in the order that it is written. But, why is user still undefined in the following code block despite the await keyword inside of an async function? I understand (based on this question) that await should be applied only up to get(), otherwise there may not be values to iterate over.

My question is similar to this, but I run into the same issue as the comment and it looks like the question was never marked as answered. It is also similar to this, but pushing all gets to a Promise and then resolving that promise still didn't assign the data to a variable. It just printed an empty array.

I can print the variable, so my question is about assignment, I think. This function is a handler for an Intent in DialogFlow. I strongly prefer to have the data available outside of the call to the db. Adding to agent responses in a call to firestore doesn't always add the text to the agent, so I'd like to avoid that.

async function loginHandler(agent){     
     username = agent.parameters.username.name;      
     password = agent.parameters.password;

        const user = await db.collection("users")
                    .where("name","==",username)
                     .where("password","==",password)
                     .limit(1)
                     .get()
                     .then(querySnapshot =>{
                        querySnapshot.forEach(docSnapShot =>{
                            return docSnapShot.data();
                            //console.log.(docSnapShot.data()); //prints correct contents, so error is in programming logic
                            agent.add("Response"); // Would prefer to avoid this, but could refactor if I am fundamentally misunderstanding how to access Firebase data
                        })
                     })
                     .catch(error => console.log);
                                 

        console.log(user);      
       console.log("bort");  
}

Picture of Firestore to demonstrate that the correct data do exist:

enter image description here

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
mac389
  • 3,004
  • 5
  • 38
  • 62

3 Answers3

2

forEach iterates the results but doesn't return anything, so your return inside there isn't doing what you'd expect (forEach returns void so you're returning the snapshot to a function that is returning void). You can create a local variable to hold the results you iterate and then return that:

const user = await db.collection("users")
                    .where("name","==",username)
                    .where("password","==",password)
                    .limit(1)
                    .get()
                    .then(querySnapshot =>{
                        // Set up an empty array to return later
                        let data = []
                        querySnapshot.forEach(docSnapShot =>{
                            // Add each snapshot's data object to the array
                            data = [...data, ...docSnapShot.data()]
                        })
                        // Return `data` which will populate `user`
                        return data
                     })
                     .catch(error => console.log);
I'm Joe Too
  • 5,468
  • 1
  • 17
  • 29
  • Thanks. When I use this method `console.log(user)` is `undefined` if I have two `console.log` statements, otherwise it prints `[Function: log]`, which suggests `get` is throwing an error? – mac389 Aug 04 '21 at 14:03
2

You might be able to split the code up. Return the data from the database first, then map over the data to extract the details, and then assign that result to the user variable.

async function loginHandler(agent) {

  username = agent.parameters.username.name;
  password = agent.parameters.password;

  // `await` the promise and assign it to
  // `querySnapshot`
  const querySnapshot = await db.collection('users')
    .where('name', '==', username)
    .where('password', '==', password)
    .limit(1)
    .get()
    .catch(error => console.log);
  
  const user = querySnapshot.docs.map(docSnapShot => {
    agent.add('Response');
    return docSnapShot.data();
  });

  console.log(user);

}
Andy
  • 61,948
  • 13
  • 68
  • 95
  • Thanks. I get the error `TypeError: data.map is not a function`. I removed the `;` after get. – mac389 Aug 04 '21 at 13:54
  • What's the value of `data` ? Is it an object? @mac389 – Andy Aug 04 '21 at 13:58
  • {"username":"Bob", "password":"123123"} Both are strings. – mac389 Aug 04 '21 at 14:04
  • `querySnapshot.forEach` in your code doesn't make sense in that context then because `querySnapshot` will also be an object. They're functionally the same. – Andy Aug 04 '21 at 14:08
  • Wouldn't the `querySnapshot.forEach` yield `DocumentSnapshot`? I replaced `data.map` with `data.then`, which didn't help either. – mac389 Aug 04 '21 at 14:17
  • 1
    I got it to work by replacing `querySnapshot.map` with `querySnapshot.docs.map` in the definition of `const user`. – mac389 Aug 04 '21 at 14:30
  • I've amended my answer with that information. – Andy Aug 04 '21 at 14:32
1

Have you checked what the correct answer is?

Please clarify whether you mean undefined as you say in the title, or empty array as you say in the text.

Your code looks correct to me. I would not expect the output of console.log(user) to be undefined. However, an empty list [ ] would be a perfectly reasonable answer.

Perhaps there is nobody in that collection with that username and password?

Have you tried removing the condition of equality of username and password? That should get you an element of the collection, if it is not entirely empty.

ProfDFrancis
  • 8,816
  • 1
  • 17
  • 26
  • I mean `[ ]`, an empty array. I have updated the title. However, I am running it on a test collection where I am sure that this username/password combination exists (and is returned where the `console` statement indicates.) – mac389 Aug 04 '21 at 13:50