Firebase async/await query not working as expected - javascript

Hey guys I'm abit new to this but I'll explain it the best way I can, So I'm using a function to return a promise my code looks something like this
getAccounts(email) {
return new Promise((resolve, reject) => {
usersCollection.where('email', '==', email).where('userType', 'in', ['Admin', 'Superuser'])
.get()
.then(async querySnapshot => {
const accounts = [];
await querySnapshot.forEach(async account => {
let accountData = await account.data();
accountData.id = accountData.userType;
if (accountData.userType === 'Admin') {
const adminObj = new Admin();
const adminData = await adminObj.getAdminDetails();
accountData = { ...accountData, ...adminData };
}
accountData.uid = authId;
await accounts.push(accountData);
});
resolve(accounts);
});
});
}
I currently have two accounts, one Admin, the other Superuser the problem is the promise resolved before adminData can be fetched, what could be the issue?

You are mixing await style with .then(). Get rid of Promise and .then entirely, and stick with async.
You can't use await inside .forEach() or any other Array method (map, filter, etc) but you can inside a for loop.
accounts.push is perfectly synchronous, no need to await it at all.
const getAccounts = async email => {
const querySnapshot = await usersCollection
.where('email', '==', email)
.where('userType', 'in', ['Admin', 'Superuser'])
.get();
const accounts = [];
for( let account of querySnapshot.docs ){
let accountData = await account.data();
accountData.id = accountData.userType;
if (accountData.userType === 'Admin') {
const adminObj = new Admin();
const adminData = await adminObj.getAdminDetails();
accountData = { ...accountData, ...adminData };
}
accountData.uid = authId;
accounts.push(accountData);
}
return accounts;
}
const accounts = await getAccounts("some.email#domain.com");

Related

Issue resolving nested promises

Below is how I generally use promises and I haven't had any issues thus far until now. The problem here is that the console.log(this.recipe) is undefined and when I console.log(JSON.stringify(recipes)) it shows me its an empty array. This leads me to believe that I am not resolving the nested promises correctly.
ngOnInit(): void {
this.recipeService.getAllRecipesByUserId().then((recipes) => {
this.allRecipes = recipes;
this.recipe = this.allRecipes[0];
console.log(this.recipe);
});
}
The getAllRecipesByUserId() function should return a promise of type Recipe[]
async getAllRecipesByUserId(): Promise<Recipe[]> {
let recipes: Recipe[] = [];
await this.userService.userRecipeIds().then((ids) => {
ids.forEach((id) => {
const q = query(this.recipeCollection, where('id', '==', id));
getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
recipes?.push(doc.data() as Recipe);
});
});
});
});
return recipes;
}
userRecipeIds():
async userRecipeIds(): Promise<string[]> {
let user: User = new User();
const q = query(
this.userCollection,
where('userId', '==', this.auth.getCurrentUser()?.uid)
);
return await getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => (user = doc.data() as User));
return user.recipes;
});
}
Am I resolving the promises correctly here ?
EDIT:
I have edited the userRecipeIds() method to the following:
async userRecipeIds(): Promise<string[]> {
const q = query(
this.userCollection,
where('userId', '==', this.auth.getCurrentUser()?.uid)
);
const docs = await getDocs(q);
const user = docs.docs[0].data() as User;
return user.recipes;
}
And then subsequently refactored the getAllRecipesByUserId() to the following:
async getAllRecipesByUserId(): Promise<Recipe[]> {
let recipes: Recipe[] = [];
const userRecipes = await this.userService.userRecipeIds();
userRecipes.forEach(async (recipeId) => {
const q = query(this.recipeCollection, where('id', '==', recipeId));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
recipes.push(doc.data() as Recipe);
});
});
return recipes;
}
I still seem to run into the same problem where the array seems to be empty upon inspection within the ngOnInit().
I don't fully understand your code, but for instance this snippet looks wrong. You are iterating over an array an assign each item to a single field called user, so at the end, after the forEach will take place, field user will have the value of the last item, so the last doc as you call it.
Maybe the whole method should look more like this:
async userRecipeIds(): Promise<Recipe[]> {
const q = query(
this.userCollection,
where('userId', '==', this.auth.getCurrentUser()?.uid)
);
const docs = await getDocs(q);
const recipes = docs.map(doc => doc.data());
return recipes;
}
in case we only want first element:
async userRecipeIds(): Promise<User> {
const q = query(
this.userCollection,
where('userId', '==', this.auth.getCurrentUser()?.uid)
);
const docs = await getDocs(q);
const user = docs[0].data();
return user;
}
Then that's how the other function can maybe look like?:
async getAllRecipesByUserId(): Promise<Recipe[]> {
const recipes: Recipe[] = await this.userService.userRecipeIds();
return recipes;
}
Answer to edited question:
When you need to resolve multiple promises at once, for instance when the promises are created from items of an array. You need to treat the results asynchronously. The foreach method doesn't care about that and will end asap, without waiting for results from your promises inside.
What you should do is use Promise.all method. Store all promises, and resolve them all, making an array of results:
async getAllRecipesByUserId(): Promise<Recipe[]> {
const userRecipes = await this.userService.userRecipeIds();
const recipieQueries = userRecipes.map(recipe => {
const q = query(...));
return getDocs(q);
})
const recipes = await Promise.all(allRecipiesPromises);
return recipes;
}

How to catch Firebase promise in React?

I have a simple function that checks if the user has Premium access or not:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
if (doc.id === auth.currentUser.uid) {
return doc.data().userSettings.hasPremium
}
});
})
}
else{
return false
}
}
I tried to catch this in various ways, but no luck, it always returns an "undefined" object.
const getPremium = async => {
checkPremium.then((response) => console.log(response))
}
const getPremium = async => {
let hasPremium = await checkPremium()
}
let hasPremium = checkPremium()
What is the correct way to get the returned Boolean value?
onSnapshot is meant for listening to a collection continuously, getting repeatedly notified as its value changes. It does not create a promise, so the promise returned by getPremium is unrelated to the data you will eventually get in onSnapshot. If you just want to get the value once, you should use getDocs:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
const querySnapshot = await getDocs(q);
const match = querySnapshot.docs.find(doc => doc.id === auth.currentUser.uid);
if (match) {
return doc.data().userSettings.hasPremium);
} else {
return false;
}
}
else{
return false
}
}
Also, instead of getting all the users and then using client side code to find the one with the right id, you could just fetch that individual doc directly:
const ref = doc(db_firestore, 'users', auth.currentUser.uid)
const snapshot = await getDoc(ref);
const data = snapshot.data();
if (data) {
return data.userSettings.hasPremium
} else {
return false
}

Promise inside a loop inside an async function

I am working on a project using react and firebase and redux and I have some items that did created by a user. I'm storing the id of the user in the item object so i can populate the user later when i get the item to display.
Now I'm trying to get the items and modify them by replacing the user id with the actual info about the user but I have a promises problem. In my code I just get an empty array which mean the modification didn't get resolved before I return the final result.
export const getItems = () => {
return (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
const items = [];
const dbRef = firestore.collection('items').orderBy('createdAt', 'desc').limit(2);
return dbRef
.get()
.then((res) => {
const firstVisible = res.docs[0];
const lastVisible = res.docs[res.docs.length - 1];
async function getData(res) {
/////////////////////////////////////////////// how to finish this code befor jumping to the return line
await res.forEach((doc) => {
firestore
.collection('users')
.doc(doc.data().owner)
.get()
.then((res) => {
items.push({ ...doc.data(), owner: res.data() });
});
});
////////////////////////////////////////////////
return { docs: items, lastVisible, firstVisible };
}
return getData(res);
})
.catch((err) => {
console.log(err);
});
};
};
I don't get exactly what you are trying to do, but I would suggest putting some order to make your code easy to read and work with.
You can use for of to manage async looping. I suggest something like this, disclaimer, I did it at the eye, problably there are some errors, but you can get the idea.
const getAllDocs = function (data) {
let temp = [];
data.forEach(function (doc) {
temp.push(doc.data());
});
return { data: temp };
};
const getDoc = snap => (snap.exists ? { data: snap.data() } : {});
export const getItems = () => {
return async (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
const dbRef = firestore.collection('items').orderBy('createdAt', 'desc').limit(2);
const usersRef = firestore.collection('users');
let temps = [];
const { data: items } = await dbRef.get().then(getAllDocs);
const firstVisible = items[0];
const lastVisible = items[items.length - 1];
for (const item of items) {
const { data: user } = await usersRef.doc(item.owner).get().then(getDoc);
const owner = {
/* whatever this means*/
};
temps.push({ ...user, owner });
}
return { docs: temps, lastVisible, firstVisible };
};
};
The problem is that an array of Promises is not itself a Promise -- so awaiting it will be a no-op.
You can solve this using Promise.all if you want to load them all asynchronously.
const items = await Promise.all(res.map(async (doc) => {
const res = await firestore.collection('users').doc(doc.data().owner).get();
return { ...doc.data(), owner: res.data() };
});
Otherwise you can await in a for loop as suggested in other answers.

firestore get data from two tables one after the other return empty array

I'm using firebase - firestore. I have courses and tasks collection.
I want to get all the courses of user from courses collection and for each course to get days data from tasks collection and then save all this data in one array.
getData = () => {
var arr = []
f.auth().onAuthStateChanged(async (user) => {
db.collection("courses")
.where("uid", "==", user.uid)
.get()
.then((snapshot) => {
var a = {};
snapshot.forEach((doc) => {
let coursesData = doc.data()
let courseName = coursesData.name;
let kita = coursesData.kita;
a = { name: courseName, id: doc.data().code, k: kita };
let snapshotData = await db
.collection("tasks")
.where("uid", "==", user.uid)
.where("name", "==", courseName)
.where("kita", "==", kita)
.get();
let numActiveCourse = 0;
snapshotData.forEach((dc) => {
let taskData = dc.data()
console.log('taskData',taskData)
let days = taskData.days;
if (days > 0) {
numActiveCourse = 1;
}
});
a = { ...a, numActiveCourse };
arr.push(a);
console.log("arr2 is", arr);
});
})
.catch((e) => {
console.log("error is courses", e);
});
this.setState({data:arr})
});
};
the problem is that the arr is always empty (I guess I have asyncornize issue)
and the snapshot not await after it will finish.
I found solution.
the issue it's because I tried to make async await into forEach and it not wait to answer.
the solution is
readCourses = async()=>{
f.auth().onAuthStateChanged(async (user) => {
let loadedPosts = {};
let docSnaps = await db.collection("courses").where("uid", "==", user.uid).get();
for (let doc of docSnaps.docs){
let courseName = doc.data().name;
let kita = doc.data().kita
loadedPosts[doc.id] = {
...doc.data(),
k:kita,
id:doc.data().code
}
const taskSnap = await db
.collection("tasks")
.where("uid", "==", user.uid)
.where("name", "==", courseName)
.where("kita", "==", kita)
.get()
let numActiveCourse = 0
for(let task of taskSnap.docs){
let taskData = task.data()
if(taskData.days>0){
numActiveCourse =numActiveCourse+1
}
}
loadedPosts[doc.id].numActiveCourse = numActiveCourse
}
console.log('loadedPosts',loadedPosts)
this.setState({data:loadedPosts})
})
}
if you have any other solution I would like to see.
It's not a good idea to mix await and then as it was in your original code. There was your first mistake. Not only you didn't wait for results of forEach, but this.setState({data:arr}) was outside of then and executed even before you've reached the forEach call.
Another issue with your initial version of code is as you said - not waiting for results of forEach. But I'm not sure that you fully understand it. Because you didn't have to change your code so much (readability aside). All your had to do is:
// change db.collection("courses")...then(...) to
const snapshot = await db.collection("courses")... // only now onAuthStateChanged callback becomes async
...
// then change forEach() to map() and wait for result
const promises = snapshot.map(async (doc) => { ... })
await Promise.all(promises)
...
As for your new version: in each iteration of the for loop you await. That means that requests for every taskSnap will be executed one after another. That's bad. Especially on slow connections. Check out the snippet (I've simplified it to the bare minimum): getData with map completes in 1 second, version with for - in 4 seconds. (And you also removed catch from your new code - not a great idea.)
let i = 0
const get_courses = () => new Promise((resolve) => setTimeout(() => resolve(["a","b","c","d"]), 10))
const get_tasks = () => new Promise((resolve) => setTimeout(() => resolve(++i), 1000))
const f_auth_onAuthStateChanged = fn => fn()
const getData = () => {
const data = []
f_auth_onAuthStateChanged(async (user) => {
try {
const courses = await get_courses()
const promises = courses.map(async (course) => {
const tasks = await get_tasks()
data.push({ course, tasks })
})
await Promise.all(promises)
console.log(data) // this.setState({ data })
console.timeEnd("map")
} catch(e) { console.error(e) }
})
}
console.time("map")
getData()
const getData2 = () => {
const data = []
f_auth_onAuthStateChanged(async (user) => {
try {
const courses = await get_courses()
for (const course of courses) {
const tasks = await get_tasks()
data.push({ course, tasks })
}
console.log(data) // this.setState({ data })
console.timeEnd("for")
} catch(e) { console.error(e) }
})
}
console.time("for")
getData2()
The readCourses function from your own answer doesn't return a Promise. So formally it's not async. That won't change anything, except for a small code readability improvement. Same goes for onAuthStateChanged callback from your original code.

What am I doing wrong with this async function?

I am trying to learn/implement async functions, and I'm unsure why I am not able to get it to work. The 'users' in my async function are coming back undefined. However, the function that makes the database call works just fine.
const getUsers = () => {
const database = firebase.database()
const users = database.ref('users');
users.on('value', function(snapshot) {
const users = []
snapshot.forEach(function(childSnapshot) {
let user = childSnapshot.val();
users.push(user)
});
return users
});
}
async function generateRandomPotentialPartner() {
try {
const users = await getUsers();
console.log(users)
} catch (error) {
console.log(error)
}
}
Use the promise version of on() instead of using the callback version and return that promise from getUsers() function.
Using the callback approach you are currently using, nothing is being returned at all since a return in a callback does not return to the outer function
const getUsers = () => {
const database = firebase.database()
const users = database.ref('users');
// return the on() promise instead of using callback
return users.on('value').then(snapshot => {
const users = []
snapshot.forEach(childSnapshot => {
users.push(childSnapshot.val())
});
return users
});
}
const getUsers = () => {
const database = firebase.database()
const users = database.ref('users');
users.on('value', function(snapshot) {
const users = []
snapshot.forEach(function(childSnapshot) {
let user = childSnapshot.val();
users.push(user)
});
return new Promise((resolve,reject) => {
if(users.length == 0){
reject("There are no users"); //--- Handle rejection
}else {
resolve(users);
}
});
});
}
Your problem is that you are not returning Promises. Asynchronous Javascript is only about promises and chaining.

Categories

Resources