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

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);
}
});

Related

Including the results of an async function in my 'return' while mapping an array

Problem
I am trying to call a database function while mapping an array. I created some sample code to demonstrate my issue on a smaller scale. I am using the 'mysql2' and 'sequelize' dependencies, as well as MySQL Workbench.
Goal in Sample Code
I have two tables in my database - one called 'boxes' and one called 'items'. Each item will be in a box (it will have a property of 'box_id'). Each box has a location. I want to get an array of objects that simply shows the name of the item and the location of the box it's in (not just the ID). I want this to be run in the cleanest way possible. I am aware there's a way to relate the two databases together, and an answer on how to do that would be appreciated, but I feel I would learn more important concepts using a different method - so ideally, I would like an alternative solution.
Code
mainFunction() is the starting point
// Gets all items currently in my database and returning the dataValues
const getAllItems = async () => {
const findAllItems = await Item.findAll()
const items = findAllItems.map(item => item.dataValues)
return items
}
// Gets a box from my database, by ID, and returning its dataValues
const getOneBox = async (id) => {
const findOneBox = await Box.findOne({ where: {id}})
return findOneBox.dataValues
}
// Starting point
const mainFunction = async () => {
// Get all items in database
const items = await getAllItems()
// Go through each item, and everytime, get the box that corresponds to the item's box_id
const myArray = items.map(async item => {
const getBox = await getOneBox(item.box_id)
// Return an object with my custom formatting, and including the box's location
return {
itemID: item.id,
itemName: item.name,
itemLocation: getBox.location
}
})
// The function will only work if I manually give my function enough time to finish
console.log('myArray before delay => ', myArray)
await new Promise(response => setTimeout(response, 500))
console.log('myArray after delay => ', myArray)
}
Here is the result in my terminal:
Setup
Here is my setup if it matters. I populated my tables manually to simplify things:
items =>
boxes =>
// Populating the existing 'test' schema with relevant tables and running main function after connecting
const Sequelize = require('sequelize')
const database = new Sequelize ('test', 'root', [REDACTED], {
host: 'localhost',
dialect: 'mysql',
define: {
timestamps: false
}
})
const connect = async () => {
await database.authenticate()
.then(async () => {
await database.sync()
console.log('Connected...')
mainFunction()
}).catch(error => {
console.log('Failed to connect => ', error)
})
}
connect()
// Defining my models
const Box = database.define('box', {
name: Sequelize.STRING,
location: Sequelize.STRING
})
const Item = database.define('item', {
name: Sequelize.STRING,
box_id: Sequelize.INTEGER
})
Turns out the issue was in my approach; it's actually very easy with for loops. I'll leave my solution in case it helps anyone.
This one just adds another property to my items array
const mainFunction = async () => {
const items = await getAllItems()
for(i = 0; i < items.length; i++) {
const getBox = await getOneBox(items[i].box_id)
items[i]['location'] = getBox.location
}
console.log(items)
}
This one is for if I wanted to format it in my own way
const mainFunction = async () => {
const items = await getAllItems()
const itemsFormatted = []
for(i = 0; i < items.length; i++) {
const getBox = await getOneBox(items[i].box_id)
itemsFormatted.push({
itemID: items[i].id,
itemName: items[i].name,
itemLocation: getBox.location
})
}
console.log(itemsFormatted)
}

How to use mongoose updateMany middleware to increase performance?

SOLVED: SOLUTION AT THE BOTTOM
I have the following Code where I am updating element by element:
//registerCustomers.js
const CustomerRegistrationCode = require("../models/CustomerRegistrationCode");
const setRegCodesToUsed = async (regCodes) => {
for (let regCode of regCodes) {
await setRegCodeToUsed(regCode._id);
}
};
const setRegCodeToUsed = async (id) => {
await CustomerRegistrationCode.findByIdAndUpdate(id, { used: true });
};
The Code works fine but is to slow and i want to update many (1000) CustomerRegistrationCodes at once.
I had a look at the updateMany middleware function but found not much info online and on the official docs. I changed my code to the following but don't know how further.
//registerCustomers.js
const setRegCodesToUsed = async (regCodes) => {
await CustomerRegistrationCode.updateMany(regCodes);
}
//CustomerRegistrationCode.js
CustomerRegistrationCodeSchema.pre('updateMany', async function (next, a) {
console.log('amount arguments: ', arguments.length); //is 2
console.log(arguments); //both parameters are functions.
next();
});
What would be the best way to update many CustomerRegistrationCodes with 1000 different id's?
SOLUTION, thanks to Murat Colyaran
const setRegCodesToUsed = async (regCodes) => {
const ids = [];
regCodes.map(code => ids.push(code._id));
await setRegCodeToUsed(ids);
};
const setRegCodeToUsed = async (ids) => {
await CustomerRegistrationCode.updateMany(
{ _id: { $in: ids } },
{ used: true }
);
};
This should work:
//registerCustomers.js
const CustomerRegistrationCode = require("../models/CustomerRegistrationCode");
const setRegCodesToUsed = async (regCodes) => {
let ids = [];
regCodes.map((code) => ids.push(code._id.toString()));
await setRegCodeToUsed(ids);
};
const setRegCodeToUsed = async (ids) => {
await CustomerRegistrationCode.updateMany(
{
id : { $in: ids }
},
{
used: true
}
);
};
Instead of sending a query for every records, we just parse the id and send a bulk request with $in

How to handle this javascript asynchronous function

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

How do I use a document's field from one collection to retrieve another document field from a different collection?

Here is how my database is structured:
challenges table &
users table
Here is the error I'm getting: error image
I want to use the "created_by" field which is also a document id for the users table, where I want to retrieve both the display name and the photo URL.
I'm not all the way sure how promises work and I have a feeling that is why I'm struggling, but the code I have thus far is below:
Data Retrieval:
UsersDao.getUserData(ChallengesDao.getChallenges().then(result => {return result['author'][0]})).then(result => {console.log(result)})
Challenges DAO:
export default class ChallengesDao {
static async getChallenges() {
const db = require('firebase').firestore();
// const challenges = db.collection('challenges').limit(number_of_challenges)
// challenges.get().then(())
const snapshot = await db.collection('challenges').get()
const names = snapshot.docs.map(doc => doc.data().name)
const createdBy = snapshot.docs.map(doc => doc.data().created_by)
const highScores = snapshot.docs.map(doc => doc.data().high_score.score)
return {challengeName: names, author: createdBy, score: highScores}
}
Users DAO:
const db = require('firebase').firestore();
export default class UsersDao {
static async getUserData(uid: string) {
let userData = {};
try {
const doc = await db
.collection('users')
.doc(uid)
.get();
if (doc.exists) {
userData = doc.data();
} else {
console.log('User document not found!');
}
} catch (err) {}
return userData;
}
}
You're getting close. All that's left to do is to call getUserData for each UID you get back from getChallenges.
Combining these two would look something like this:
let challenges = await getChallenges();
let users = await Promise.all(challenges.author.map((uid) => getUserData(uid));
console.log(challenges.challengeName, users);
The new thing here is Promise.all(), which combines a number of asynchronous calls and returns a promise that completes when all of them complete.
Your code looks a bit odd to me at first, because of the way you return the data from getChallenges. Instead of returning three arrays with simple values, I'd recommend returning a single array where each object has three values:
static async getChallenges() {
const db = require('firebase').firestore();
const snapshot = await db.collection('challenges').get();
const challenges = snapshot.docs.map(doc => { name: doc.data().name, author: doc.data().created_by, score: doc.data().high_score.score });
return challenges;
}
If you then want to add the user name to each object in this array, in addition to the UID that's already there, you could do:
let challenges = await getChallenges();
await Promise.all(challenges.forEach(async(challenge) => {
challenge.user = await getUserData(challenge.author);
});
console.log(challenges);

How to make a Firebase messaging function asynchronous

I have a firebase messaging function, but the return function seems to execute before the rest of the functions. Here is the code (sorry its long):
exports.newMessageNotification = functions.firestore
.document(`messages/{messageId}`) // wildcard for the msg id
.onCreate(async (change, context) => {
const db = admin.firestore();
const messageId: string = context.params.messageId;
const messageRef = db.collection('messages').doc(messageId);
const tokens = [];
// get the message
const message: Message = await messageRef
.get()
.then(q => q.data() as Message);
const recipients: any = await message.recipients;
const user: User = await db
.collection('users')
.doc(message.senderId)
.get()
.then(q => {
return q.data() as User;
});
// Notification content
const payload = await {
notification: {
title: `${user.name}`,
body: `${message.message}`,
},
};
console.log(payload);
// loop though each recipient, get their devices and push to tokens
Object.keys(recipients).forEach(async recipient => {
const devicesRef = db
.collection('devices')
.where('userId', '==', recipient);
const devices = await devicesRef.get();
devices.forEach(res => {
const token: string = res.data().token;
console.log(token);
tokens.push(token);
});
});
console.log(tokens); // logs empty
return await sendToCloud(tokens, payload);
});
I think the issue is that the forEach is not asynchronous so the final line of code is executing before waiting for forEach to finish?
Ugh. I had this problem somewhere recently. You are correct, at least in my experience: forEach does not seem to obey the async directive. I had to use the for ... in ... syntax to:
Get it to obey async (from the parent scope)
Process sequentially, as order was important to me at the time
In your case, it would probably look like for (const recipient in recipients) { ... }
Apparently, this is caused by forEach calling the callback on each item without awaiting a response. Even if the callback is asynchronous, forEach doesn't know to await its response on each loop.
Source: https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404
A solution from the comments on the blog linked above:
await Promise.all(
Object.keys(recipients).map(async recipient => {
const devicesRef = db
.collection('devices')
.where('userId', '==', recipient);
const devices = await devicesRef.get();
devices.forEach(res => {
const token: string = res.data().token;
console.log(token);
tokens.push(token);
});
});
)
Note the forEach has been replaced by a map, and it's within Promise.all(...).

Categories