How to make a Firebase messaging function asynchronous - javascript

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(...).

Related

How to get all documents in Firestore (v9) sub collection

I'm trying to get all documents in all sub-collection with Firebase V9, so I used collectionGroup to retrieve the data.
Here is my Firestore architecture :
bots (collection)
| id (doc ID)
| order_history (sub collection)
| id (doc ID)
createdBy: uid (user uid)
And this is how I try to get documents :
const [orders, setOrders] = useState([]);
const { user } = useAuth();
const getOrders = async () => {
const orders = await getDocs(query(collectionGroup(db, 'order_history'), where('createdBy', '==', user.uid)));
setOrders(orders.docs.map((doc) => ({
...doc.data()
})))
}
useEffect(() => {
getOrders();
console.log('orders ', orders); // return []
}, []);
This code returns an empty array.
Did I do something wrong?
I think your getOrders function is asynchronus function.
If you want debug log I think you should waiting for getOrders completed then orders had been updated.
Ex:
useEffect(() => {
console.log('orders ', orders);
}, [orders]);
Your getOrders anonymous method execution requires an explicit return statement if there's more than 1 statement. Implicit returns work when only a single statement exists (and after some testing, return await X doesn't appear to work either).
const getOrders = async () => {
const orders = await getDocs(...);
setOrders(orders.docs.map(...))
}
Needs to be
const getOrders = async () => {
const orders = await getDocs(...);
return setOrders(orders.docs.map(...))
}

Trouble returning the customer object with stripe queries

I am playing around with stripe queries and in the documentation this is the example that is given:
const stripe = require('stripe')('some-api-key');
const customer = await stripe.customers.search({
query: 'metadata[\'foo\']:\'bar\'',
});
so naturally I tried this:
const stripe = require('stripe')('some-api-key');
const customer = async () => await stripe.customers.search({
query: 'metadata[\'foo\']:\'bar\'',
});
console.log(customer)
but all I get back is this in the console:
customer() {
return _ref.apply(this, arguments);
}
and when I try this I just get promise pending:
console.log(customer().then(data => console.log(data)));
what am I doing wrong here?
If you are doing this:
const customer = async () => await stripe.customers.search({
query: 'metadata[\'foo\']:\'bar\'',
});
Then customer is an async function. So you have two ways to log the result: console.log(await customer()) or customer().then(res => console.log(res)).

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 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 access date in next .then() - fetch api

I wonder if there is a way to access data from then on the next one? Code looks like this:
fetch(`http://localhost:3003/users/${userObject.id}`)
.then(res => res.json())
.then(user => {
...
})
.then(???)
and in second then I have all the needed info about the user. When I put everything in second then it works, but code is very messy and repetitive and I would like to put some things into function. Unfortunately, in that case, I don't have access to some data...
Any ideas? Thanks!
You can pass an argument to the next promise by returning it from the previous one.
fetch(`http://localhost:3003/users/${userObject.id}`)
.then(res => res.json())
.then(user => {
return user.id;
})
.then(userId => {
console.log('user id is:', userId);
});
Also, you can accomplish same result by using async programming like bellow:
async function fetchSomeData() {
var res = await fetch(`http://localhost:3003/users/${userObject.id}`);
var user = await res.json();
return user;
}
fetchSomeData().then(user => {
console.log('user id is:', user.id)
});
but code is very messy and repetitive and I would like to put some
things into function
Just use async/await syntax, it's much more understandable.
(async() => {
const request = await fetch(`http://localhost:3003/users/${userObject.id}`);
const result = await request.json();
// proceed to do whatever you want with the object....
})();
This is a self-executing function, but you can also declare this in the following way:
const myFunc = async() => {
const request = await fetch(`http://localhost:3003/users/${userObject.id}`);
const result = await request.json();
// your logic here or you can return the result object instead;
};
or even without arrow functions:
const myFunc = async function() {
const request = await fetch(`http://localhost:3003/users/${userObject.id}`);
const result = await request.json();
console.log(result.id);
};

Categories