How to handle this javascript asynchronous function - javascript

I have this function in javascript node
exports.getUserEmailByAccess = async (moduleType) => {
let emailList = []
const foundUsers = await Access.find({moduleType:moduleType})
console.log(foundUsers);
await foundUsers.forEach(access => {
User.findById(access.userId)
.then(foundUser => {
console.log(foundUser.workerEmail);
emailList.push(foundUser.workerEmail)
})
});
console.log(emailList);
return emailList
}
What I want is to push into emailList array by looping object array, the above approach results in an empty array,so I tried a different following way
exports.getUserEmailByAccess = async (moduleType) => {
let emailList = []
const foundUsers = await Access.find({moduleType:moduleType})
console.log(foundUsers);
await foundUsers.forEach(access => {
const foundUser = User.findById(access.userId)
console.log(foundUser.workerEmail);
emailList.push(foundUser.workerEmail)
});
console.log(emailList);
return emailList
}
By doing this , the array list is getting filled but with an [undefined]strong text value, I come from a humble python control structure background, Please can I know why I am not able to push data into array even after using async/await

If the User.findById() returns a promise, I'd recommend using Promise.all() instead of individually running all promises using forEach to fetch documents of all foundUsers:
exports.getUserEmailByAccess = async (moduleType) => {
const foundUsers = await Access.find({ moduleType: moduleType });
console.log(foundUsers);
const userDocs = await Promise.all(
foundUsers.map((user) => User.findById(user.userId))
);
const emailList = userDocs.map((user) => user.workerEmail);
// or alternatively
// const emailList = userDocs.map(({workerEmail}) => workerEmail);
console.log(emailList);
return emailList;
};

you can give try this
exports.getUserEmailByAccess = async (moduleType) => {
let emailList = []
const foundUsers = await Access.find({ moduleType: moduleType })
console.log(foundUsers);
await foundUsers.map(async access => {
let result = await User.findById(access.userId);
if (result) {
emailList.push(foundUser.workerEmail)
}
});
console.log(emailList);
return emailList
}
UPDATED
await Promise.all(
foundUsers.map(async access => {
let result = await User.findById(access.userId);
if (result) {
emailList.push(foundUser.workerEmail)
}
})
])
console.log(emailList);
return emailList

How are your Models related? I think you might do a populate here if the Access Model has the ObjectId of the User Model. Something like this:
const foundUsers = await Access.find({ moduleType: moduleType }).populate({
path: 'users', // the name of the field which contains the userId
select: '-__v' // fields to bring from database. Can add a (-) to NOT bring that field
})
The idea is that you specify all the fields that you need. Then when you receive the data, you can do something like:
foundUsers.users.email

Related

How to await admin.auth().getUser() method within a forEach loop?

I am trying to iterate over an array of comments and need to grab the commenter's uid for each comment. I am a beginner to JavaScript and need a little bit of help with the following use case:
I need to grab the uid for each comment and then run a .getUser() method which will return the user's email address that is associated with the user's uid. Since .getUser() returns a promise (method reference link), I need to await somewhere in this loop. How to do so? Is this even a good approach?
(Note: My end goal is to eventually attach the email addresses to a to property in a msg object where I will then send out email notifications.)
Example data for comments:
[
{
id: 1,
uid: 'RGaBbiui'
},
{
id: 2,
uid: 'ladfasdflal'
},
{
id: 3,
uid: 'RGaBbiui'
},
{
id: 4,
uid: 'RGaBbiui'
},
{
id: 5,
uid: 'ladfasdflal'
},
{
id: 6,
uid: 'ladfasdflal'
}
]
Cloud function example:
export const sendCommentNotification = functions.firestore
.document('users/{uid}/posts/{postId}/comments/{commentId}')
.onCreate(async (snapshot, context) => {
try {
const commentsQuery = await admin
.firestore()
.collection(
`users/${context.params.uid}/posts/${context.params.postId}/comments`
)
.get()
const commentsArr = []
commentsQuery.forEach((documentSnapshot) =>
commentsArr.push(documentSnapshot.data())
)
const commentsArrUids = new Set(commentsArr.map((c) => c.uid))
console.log(commentsArrUids)
const emailAddresses = []
commentsArrUids.forEach((uid) =>
emailAddresses.push(admin.auth().getUser(uid)) // how to use await here?
)
...
const msg = {
to: //TO DO..put email addresses here..
...
You cannot use await in a forEach loop. You could use await in a for-loop but they won't run simultaneously as in Promise.all().
You can just await all the promises at once using Promise.all():
Returned values will be in order of the Promises passed, regardless of completion order.
const emailAddresses = []
commentsArrUids.forEach((uid) => {
emailAddresses.push(admin.auth().getUser(uid))
})
const data = await Promise.all(emailAddresses)
Data will be an array of UserRecord.
Then you can use the .map() method to get an array of all the emails.
const emails = data.map((user) => user.email)
The code can be written like this to make it easier:
const commentsDocs = await admin.firestore().collection(`users/${context.params.uid}/posts/${context.params.postId}/comments`).get()
const userIds = commentsDocs.docs.map(comment => comment.userId)
const usersReq = userIds.map(u => admin.auth().getUser(u.uid))
const emails = (await Promise.all(usersReq))).map((user) => user.email)
Use for loop instead.
for (let i = 0; i < commentsArrUids.length; i++) {
let user = await new Promise((resolve, reject) => {
admin.auth().getUser(commentsArrUids[i]).then((user) => {
resolve(user);
});
};
emailAddresses.push(user);
}
I will replace forEach with for of and the promises is in series. Also, I rewrite some of your codes as they are redundant.
export const sendCommentNotification = functions.firestore
.document("users/{uid}/posts/{postId}/comments/{commentId}")
.onCreate(async (snapshot, context) => {
try {
const comments = await admin
.firestore()
.collection(
`users/${context.params.uid}/posts/${context.params.postId}/comments`
)
.get();
const uids = new Set();
for (const comment of comments) {
uids.add(comment.data().uid);
}
const emailAddresses = [];
for (const uid of uids) {
const res = await admin.auth().getUser(uid);
emailAddresses.push(res);
}
} catch (err) {
console.log(err);
}
});

How to wait for a forEach to finish before returning from my promise / function

Angular, firestore
I have an angular function to get products from one firestore collection, then I am looping over the results of that query to lookup prices from another collection.
How can I wait until the prices forEach is done before returning from the outer promise and the outer function itself?
The returned result contains a products array, but the prices array for each product is empty.
const products = await this.billingService.getProducts();
async getProducts() {
let result = [];
let product = {};
return this.db.collection(
'products',
ref => { ref
let query: Query = ref;
return query.where('active', '==', true)
})
.ref
.get()
.then(function (querySnapshot) {
querySnapshot.forEach(async function (doc) {
product = doc.data();
product['prices'] = [];
await doc.ref
.collection('prices')
.orderBy('unit_amount')
.get()
.then(function (docs) {
// Prices dropdown
docs.forEach(function (doc) {
const priceId = doc.id;
const priceData = doc.data();
product['prices'].push(priceData);
});
});
});
result.push(product);
return result;
});
}
I also tried this approach, but not sure how to access the results
await this.billingService.getProducts().then(results =>
getProducts() {
const dbRef =
this.db.collection(
'products',
ref => { ref
let query: Query = ref; return query.where('active', '==', true)
});
const dbPromise = dbRef.ref.get();
return dbPromise
.then(function(querySnapshot) {
let results = [];
let product = {};
querySnapshot.forEach(function(doc) {
let docRef = doc.ref
.collection('prices')
.orderBy('unit_amount')
results.push(docRef.get())
});
return Promise.all(results)
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
}
Posting as a Community Wiki answer, based in the comments.
For this case, using a forEach() is not the correct choice. As clarified in this case here, forEach() doesn't work properly with await functions, this way, not working correctly with your promises. Considering that and the fact that you want to read the data in sequence - as the results from one query will impact in the second one - you need to use a normal for, to loop through the data and arrays. This specific answer here should help you with code samples.
This version of the code works:
getProducts(): Promise<any> {
return new Promise((resolve,reject)=> {
let result = [];
let product = {};
this.db.collection(
'products',
ref => { ref
let query: Query = ref;
return query.where('active', '==', true)
})
.ref
.get()
.then(async function (querySnapshot:firebase.firestore.QuerySnapshot) {
for(const doc of querySnapshot.docs) {
const priceSnap = await doc.ref
.collection('prices')
.orderBy('unit_amount')
.get()
product = doc.data();
product['prices'] = [];
// Prices dropdown
for(const doc of priceSnap.docs) {
const priceId = doc.id;
let priceData = doc.data();
priceData['price_id'] = priceId;
product['prices'].push(priceData);
resolve(result);// returns when it reaches here
};
result.push(product);
};
});
})
}
Make the getProducts() function a promise. Thus it will only return when you resolve it ( or reject it).
getProducts() {
return new Promise((resolve,reject)=> {
let result = [];
let product = {};
this.db.collection(
'products',
ref => { ref
let query: Query = ref;
return query.where('active', '==', true)
})
.ref
.get()
.then(function (querySnapshot) {
querySnapshot.forEach(async function (doc) {
product = doc.data();
product['prices'] = [];
doc.ref
.collection('prices')
.orderBy('unit_amount')
.get()
.then(function (docs) {
// Prices dropdown
docs.forEach(function (doc) {
const priceId = doc.id;
const priceData = doc.data();
product['prices'].push(priceData);
});
resolve(result);// returns when it reaches here
});
});
result.push(product);
});
})
}
Then you can call the promise using then or await
this.billingService.getProducts().then( res => {
const products = res;
})
Using await
const products = await this.billingService.getProducts();

Async reduce returning promises

I have an array of objects and I have to add one property on each of the objects coming from and async function
I am doing an Array.reduce to iterate on each of the elements and return just one result: One array of objects with the new property.
I have this
const res = await resultOne.reduce(async (users = [], user, i) => {
let userName;
try {
let { name } = await names.getNames(user.id);
userName = name;
} catch (error) {
throw error;
}
delete user.id;
users.push({ ...user, userName });
return users;
}, []);
But I get the message
Push is not a function of users
And this is because I think is a promise.
How can I handle async requests in a reduce or a map
Yes, users is a promise. Don't use reduce with async functions. You could do something like await users in the first line, but that would be pretty inefficient and unidiomatic.
Use a plain loop:
const users = [];
for (const user of resultOne) {
const { name } = await names.getNames(user.id);
delete user.id;
users.push({ ...user, userName: user });
}
or, in your case where you can do everything concurrently and create an array anyway, the map function together with Promise.all:
const users = await Promise.all(resultOne.map(async user => {
const { name } = await names.getNames(user.id);
delete user.id;
return { ...user, userName: user };
}));
Because it's an async function, every time you return something it gets wrapped in a promise. To fix this you need to set the starting array as a promise and then await for the accumulator on each iteration.
const res = await resultOne.reduce(async (users, user, i) => {
try {
return [
...await users,
{ ...user, userName: await names.getNames(user.id.name) }
]
} catch (error) {
console.log(error)
}
}, Promise.resolve([]));

async Array.map() inside another map call

I have a method that receives a profiles array and I have to map for each profile and inside this map I have to map again in the photos propriety, which contains the pictures ids for requesting to an API for getting this picture.
The question is, where can I safely access this profiles array with their loaded photos for each respective profile?
profiles.map((profile, i) => {
let photos = []
Promise.all(profile.photos.map(async idPhoto => {
const res = await fetch(...)
const img = await res.blob()
photos.push(img)
}))
.then(() => profiles[i].photos = [...photos])
})
With the outer map function the way it currently is, the Promise.all() calls are discarded, so there is no way for your code to detect when they are complete.
However, since you also do not appear to be using the return value of the outer map, we can make it return an array of Promises that resolve when the inner their array of Promises is all resolved. And then we can use the same Promise.all(array.map()) pattern as we use for the inner map.
const photoRequests = profiles.map(async (profile, i) => {
let photos = []
await Promise.all(profile.photos.map(async idPhoto => {
const res = await fetch(...)
const img = await res.blob()
photos.push(img)
}));
profiles[i].photos = [...photos];
})
// And now...
await Promise.all(photoRequests);
// After this it is safe to access.
// Or, if the outer map is not in an async method:
Promise.all(photoRequests).then(() => {
// It is safe to access profiles here
});
I've refactored the outer map to be an async function (aids readability IMO), but you can put it back if you prefer. Just have the outer map function return the result of the Promise.all call.
As to what else could be improved here, the having variables photos and profile.photos is a little confusing, so consider renaming photos. Also make it const while you're at it, as it's never reassigned.
Unless there's some other code that manipulates the photos array, the array spread syntax is not needed. Same for the index variable. Final code might look something like:
const photoRequests = profiles.map(async profile => {
const loadedPhotos = []
await Promise.all(profile.photos.map(async idPhoto => {
const res = await fetch(...)
const img = await res.blob()
loadedPhotos.push(img)
}));
profile.photos = loadedPhotos;
})
await Promise.all(photoRequests);
Or you could use the fact that Promise.all resolves to an array containing the resolve values of the individual promises it received:
const photoRequests = profiles.map(async profile => {
profile.photos = await Promise.all(
profile.photos.map(async idPhoto => {
const res = await fetch(...)
return res.blob()
})
);
})
await Promise.all(photoRequests);
I think it would be better to separate each mapping into its own function, it makes it easier to read. I refactored your code into this:
let fetchPhoto = async (photoId) => {
// const res = await fetch(...);
// return res.blob();
return { imageData: photoId } // mock blob result
};
let mapPhotoIdToImage = async (profile) => {
let photos = profile.photos.map(fetchPhoto)
photos = await Promise.all(photos);
profile.photos = photos;
return profile;
};
let profileList = [{photos: ['id1', 'id2']}];
let result = await profileList.map(mapPhotoIdToImage);
result:
[{ photos: [ { imageData: 'id1' }, { imageData: 'id2' } ] }]

"Exit promise" with multiples fetch requests

I need to merge data from API. I do a first call to an endpoint that gives me a list of ids, then I do a request for each id. My goal is to return a list with the responses of all requests but I lost myself in promises ...
My code runs on NodeJS. Here is the code :
const fetch = require('node-fetch')
const main = (req, res) => {
fetch('ENDPOINT_THAT_GIVES_LIST_OF_IDS')
.then(response => response.json())
.then(response => {
parseIds(response)
.then(data => {
console.log(data)
res.json(data)
// I want data contains the list of responses
})
})
.catch(error => console.error(error))
}
const getAdditionalInformations = async function(id) {
let response = await fetch('CUSTOM_URL&q='+id, {
method: 'GET',
});
response = await response.json();
return response
}
const parseIds = (async raw_ids=> {
let ids= []
raw_ids.forEach(function(raw_id) {
let informations = {
// Object with data from the first request
}
let additionalInformations = await
getAdditionalInformations(raw_id['id'])
let merged = {...informations, ...additionalInformations}
ids.push(merged)
})
return ids
})
main()
I get this error : "await is only valid in async function" for this line :
let additionalInformations = await getAdditionalInformations(raw_id['id'])
Help me with promise and async/await please.
You're almost there, just a slight bit of error here with your parentheses:
// notice the parentheses'
const parseIds = async (raw_ids) => {
let ids= []
raw_ids.forEach(function(raw_id) {
let informations = {
// Object with data from the first request
}
let additionalInformations = await getAdditionalInformations(raw_id['id'])
let merged = {...informations, ...additionalInformations}
ids.push(merged)
})
return ids
}
You are missing an async after forEach
const parseIds = (async raw_ids=> {
let ids= []
raw_ids.forEach(async function(raw_id) {
let informations = {
// Object with data from the first request
}
let additionalInformations = await
getAdditionalInformations(raw_id['id'])
let merged = {...informations, ...additionalInformations}
ids.push(merged)
})
return ids
})
One suggestion: you are mixing promises (.then()) with async/await. Prefer async/await is more readable.
Note that getAdditionalInformations inside forEach doesn't wait for it to be done before going to the next entry of the array.
You can use plain old for(var i=0; .... instead

Categories