Firebase cloud function promises - javascript

I'm having a hard time getting a promise chain to flow correctly in a firebase cloud function. It loops through a ref and returns an array of emails for sending out notifications. It has some nested children, and I think that's where I'm going wrong, but I can't seem to find the error.
/courses structure
{
123456id: {
..
members: {
uidKey: {
email: some#email.com
},
uidKey2: {
email: another#email.com
}
}
},
some12345string: {
// no members here, so skip
}
}
function.js
exports.reminderEmail = functions.https.onRequest((req, res) => {
const currentTime = new Date().getTime();
const future = currentTime + 172800000;
const emails = [];
// get the main ref and grab a snapshot
ref.child('courses/').once('value').then(snap => {
snap.forEach(child => {
var el = child.val();
// check for the 'members' child, skip if absent
if(el.hasOwnProperty('members')) {
// open at the ref and get a snapshot
var membersRef = admin.database().ref('courses/' + child.key + '/members/').once('value').then(childSnap => {
console.log(childSnap.val())
childSnap.forEach(member => {
var email = member.val().email;
console.log(email); // logs fine, but because it's async, after the .then() below for some reason
emails.push(email);
})
})
}
})
return emails // the array
})
.then(emails => {
console.log('Sending to: ' + emails.join());
const mailOpts = {
from: 'me#email.com',
bcc: emails.join(),
subject: 'Upcoming Registrations',
text: 'Something about registering here.'
}
return mailTransport.sendMail(mailOpts).then(() => {
res.send('Email sent')
}).catch(error => {
res.send(error)
})
})
})

The following should do the trick.
As explained by Doug Stevenson in his answer, the Promise.all() method returns a single promise that resolves when all of the promises, returned by the once() method and pushed to the promises array, have resolved.
exports.reminderEmail = functions.https.onRequest((req, res) => {
const currentTime = new Date().getTime();
const future = currentTime + 172800000;
const emails = [];
// get the main ref and grab a snapshot
return ref.child('courses/').once('value') //Note the return here
.then(snap => {
const promises = [];
snap.forEach(child => {
var el = child.val();
// check for the 'members' child, skip if absent
if(el.hasOwnProperty('members')) {
promises.push(admin.database().ref('courses/' + child.key + '/members/').once('value'));
}
});
return Promise.all(promises);
})
.then(results => {
const emails = [];
results.forEach(dataSnapshot => {
var email = dataSnapshot.val().email;
emails.push(email);
});
console.log('Sending to: ' + emails.join());
const mailOpts = {
from: 'me#email.com',
bcc: emails.join(),
subject: 'Upcoming Registrations',
text: 'Something about registering here.'
}
return mailTransport.sendMail(mailOpts);
})
.then(() => {
res.send('Email sent')
})
.catch(error => { //Move the catch at the end of the promise chain
res.status(500).send(error)
});
})

Instead of dealing with all the promises from the inner query inline and pushing the emails into an array, you should be pushing the promises into an array and using Promise.all() to wait until they're all done. Then, iterate the array of snapshots to build the array of emails.
You may find my tutorials on working with promises in Cloud Functions to be helpful in learning some of the techniques: https://firebase.google.com/docs/functions/video-series/

Related

React & Firebase - making multiple Firebase calls and waiting for all promises to be complete

I would like to change the following code as it reaches Firebases 10 item in an array limit. I want to loop through all teamIds and make a Firebase query for each individual teamId. The issue is, I'm not sure how to do this in a way that it waits until all promises are complete before continuing.
This is the current code;
const unsubscribe = Firebase.firestore().collection('invites').where('teamId', 'in', teamIds).onSnapshot(snapshot => {
const invites = [];
snapshot.docs.forEach(doc => {
const data = doc.data();
if (!invites[data.teamId]) {
invites[data.teamId] = [];
}
invites[data.teamId].push(Object.assign({}, { id: doc.id }, doc.data()));
});
setTeamInvites(invites);
setLoading(false);
setError(false);
});
I've like to change it to something like this;
teamIds.forEach(teamId => {
Firebase.firestore().collection('invites').where('teamId', '==', teamId).onSnapshot(snapshot => {
// Map the results to an array that will be stored in the pageState when all promises are complete
});
});
How can I do this?
I figured out I can do this with Promise.all, this is what I ended up with.
const promises = [];
teamIds.forEach(teamId => {
promises.push(new Promise((resolve, reject) => {
Firebase.firestore().collection('invites').where('teamId', '==', teamId).onSnapshot(snapshot => {
let invites = [];
invites[teamId] = [];
snapshot.docs.forEach(doc => {
const data = doc.data();
invites[data.teamId].push(Object.assign({}, { id: doc.id }, data));
resolve(invites);
});
});
}));
});
Promise.all(promises).then(allInvites => {
// Do what I needed to do with all of the invites here
});

Microsoft Graph API Javascript SDK Promises

I just got rid of the promise chaining as it was very confusing and went ahead with async await. I'm still not able to get the required results. How do I get the result of finalResult() from the below code. It keeps returning promise pending. I tried doing the following
let sampleData = await finalResult()
Here how do I get the data from sampleData? I also tried attaching a then call to finalResult which didn't work either. Any help pls? All I need is the channelData in the code below
app.get("/graph/getChannelEvents", (req, res) => {
var idToken = req.query.idToken;
var teamId = req.query.teamId;
var channelData = req.query.channelData;
var tenantId = process.env.TENANT_ID;
if (!idToken) {
res.status(500).send("Could not find id token");
return;
}
request(
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
form: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
client_id: process.env.APP_ID,
client_secret: process.env.APP_Secret,
scope: "Calendars.Read",
requested_token_use: "on_behalf_of",
assertion: idToken,
},
},
async (error, response, body) => {
const accessToken = JSON.parse(body)["access_token"];
var client = MicrosoftGraph.Client.init({
defaultVersion: "v1.0",
debugLogging: true,
authProvider: (done) => {
done(null, accessToken);
},
});
async function finalResult() {
let cdata = await client
.api(`/groups/${teamId}/events`)
.header("Prefer", 'outlook.timezone="Pacific Standard Time"')
.select("subject,onlinemeeting,start,end")
.get();
let channelData = await cdata.value.map(async (org) => {
var channelId = url
.parse(decodeURIComponent(org.onlineMeeting.joinUrl))
.pathname.split("/")[3];
var sessionData = await client
.api(`/teams/${teamId}/channels`)
.filter(`startswith(id, '${channelId}')`)
.select("displayName")
.get();
let myData = await sessionData.value.map(async (u) => {
return {
sessionName: org.subject,
channelName: u.displayName,
channelId: channelId,
startDate: org.start.dateTime.split("T")[0],
endDate: org.end.dateTime.split("T")[0],
startTime: org.start.dateTime.split("T")[1],
endTime: org.end.dateTime.split("T")[1],
};
});
});
console.log(channelData);
}
finalResult();
}
);
});
A good solution for this would be to make it asynchronously with a promise wrapping the whole shown code and returning that promise with a function.
After that you would want to make an array of promises to be able to call them all at once and wait for all the results before continuing. you use Promise.all() to call all the promises in the array and use it as you would use a single promise with a .then(). The received value from that then() would be an array containing all of the results from the promiseArray's promises.
var getStuff = () => {
//First and big promise starts here
return new Promise( (resolve, reject) => {
client.api(`/groups/${teamId}/events`)
.header("Prefer", 'outlook.timezone="Pacific Standard Time"')
.select("subject,onlinemeeting,start,end")
.get()
.then((result) => {
// Create an array for your promises
var promisesArray = [];
result.value.map((org) => {
// Fill your array with every promise you need, each one containing just one call to your api
promisesArray.push(new Promise((arrayResolve, arrayReject) => {
console.log("top: " + org.subject);
var channelId = url
.parse(decodeURIComponent(org.onlineMeeting.joinUrl))
.pathname.split("/")[3];
client
.api(`/teams/${teamId}/channels`)
.filter(`startswith(id, '${channelId}')`)
.select("displayName")
.get()
.then((result) => {
const cdata = result.value.map((u) => {
console.log("down " + org.subject);
return {
sessionName: org.subject,
channelName: u.displayName,
channelId: channelId,
startDate: org.start.dateTime.split("T")[0],
endDate: org.end.dateTime.split("T")[0],
startTime: org.start.dateTime.split("T")[1],
endTime: org.end.dateTime.split("T")[1],
};
});
})
.then((result) => {
// Resolve individual promises
arrayResolve(result);
}).catch((err) => {
reject(err);
})
}));
})
// This executes all the promises at the same time
Promise.all(promisesArray).then( (finalResult) => {
// Resolve big and initial promise
resolve(finalResult);
}).catch((err) => {
reject(err);
})
})
})
}
(NOTE: I just wrapped your existing code in a function and completed the braces and parenthesis needed. Then I added the promises that would make this work as intended but some code needs to be added to make it work, as i noticed you called some variables that weren't defined inside your shown code)
After this, you could just call this function and use a '.then()' to receive the value you want.
Hope this helps!

javascript promise after foreach loop with multiple mongoose find

I'm trying to have a loop with some db calls, and once their all done ill send the result. - Using a promise, but if i have my promise after the callback it dosent work.
let notuser = [];
let promise = new Promise((resolve, reject) => {
users.forEach((x) => {
User.find({
/* query here */
}, function(err, results) {
if(err) throw err
if(results.length) {
notuser.push(x);
/* resolve(notuser) works here - but were not done yet*/
}
})
});
resolve(notuser); /*not giving me the array */
}).then((notuser) => {
return res.json(notuser)
})
how can i handle this ?
Below is a function called findManyUsers which does what you're looking for. Mongo find will return a promise to you, so just collect those promises in a loop and run them together with Promise.all(). So you can see it in action, I've added a mock User class with a promise-returning find method...
// User class pretends to be the mongo user. The find() method
// returns a promise to 'find" a user with a given id
class User {
static find(id) {
return new Promise(r => {
setTimeout(() => r({ id: `user-${id}` }), 500);
});
}
}
// return a promise to find all of the users with the given ids
async function findManyUsers(ids) {
let promises = ids.map(id => User.find(id));
return Promise.all(promises);
}
findManyUsers(['A', 'B', 'C']).then(result => console.log(result));
I suggest you take a look at async it's a great library for this sort of things and more, I really think you should get used to implement it.
I would solve your problem using the following
const async = require('async')
let notuser = [];
async.forEach(users, (user, callback)=>{
User.find({}, (err, results) => {
if (err) callback(err)
if(results.length) {
notUser.push(x)
callback(null)
}
})
}, (err) => {
err ? throw err : return(notuser)
})
However, if you don't want to use a 3rd party library, you are better off using promise.all and await for it to finish.
EDIT: Remember to install async using npm or yarn something similar to yarn add async -- npm install async
I used #danh solution for the basis of fixing in my scenario (so credit goes there), but thought my code may be relevant to someone else, looking to use standard mongoose without async. I want to gets a summary of how many reports for a certain status and return the last 5 for each, combined into one response.
const { Report } = require('../../models/report');
const Workspace = require('../../models/workspace');
// GET request to return page of items from users report
module.exports = (req, res, next) => {
const workspaceId = req.params.workspaceId || req.workspaceId;
let summary = [];
// returns a mongoose like promise
function addStatusSummary(status) {
let totalItems;
let $regex = `^${status}$`;
let query = {
$and: [{ workspace: workspaceId }, { status: { $regex, $options: 'i' } }],
};
return Report.find(query)
.countDocuments()
.then((numberOfItems) => {
totalItems = numberOfItems;
return Report.find(query)
.sort({ updatedAt: -1 })
.skip(0)
.limit(5);
})
.then((reports) => {
const items = reports.map((r) => r.displayForMember());
summary.push({
status,
items,
totalItems,
});
})
.catch((err) => {
if (!err.statusCode) {
err.statusCode = 500;
}
next(err);
});
}
Workspace.findById(workspaceId)
.then((workspace) => {
let promises = workspace.custom.statusList.map((status) =>
addStatusSummary(status)
);
return Promise.all(promises);
})
.then(() => {
res.status(200).json({
summary,
});
})
.catch((err) => {
if (!err.statusCode) {
err.statusCode = 500;
}
next(err);
});
};

Nested Promise Function Using Firebase Database

I have the following function, which accepts an argument (an array of "names") and then checks data from my firebase database for each user against that array.
It uses that to compile an array of "settings" for each user with their email, and the names that the user shared with the list fed in as an argument. The function looks like this.
fbDaemon = ({ folderNames }) => {
const settings = [];
ref.once("value")
.then((snapshot) => {
snapshot.forEach(user => {
auth.getUser(user.key)
.then(function(userRecord) {
let email = userRecord.toJSON().email;
let zips = [];
user.forEach(setting => {
let dep = setting.val().department;
if(folderNames.includes(dep)){
zips.push(dep);
}
});
settings.push({ email, zips });
})
.catch(function(error) {
console.log("Error fetching user data:", error);
});
});
});
});
Essentially, it's going through my entire database and compiling a list of the settings that I will pass onto the next function. The end result should look something like this:
[ { email: 'example#example1.com',
zips: [ 'Drug Enforcement Administration', 'Executive Branch' ] },
{ email: 'example#example2.com',
zips: [ 'FEMA', 'Congress' ] },
];
The problem that I'm having right now is I'm not able to return the "settings" array at the appropriate time.
How can I reconfigure this function so that the settings array is only returned when the entire function has run?
In other words, I'd like to return a resolved promise with the settings array. How can I do this?
Perhaps you could use Promise.all() here, to resolve an array of promises (where each item in the array corresponds to a call to getUser for that item/user) ?
So, something along these lines:
fbDaemon = ({ folderNames, folderPaths }) => {
const settings = [];
return ref.once("value")
.then((snapshot) => {
// Collect all users from snapshot into an array
const users = []
snapshot.forEach(user => { users.push(user) })
// Create promise for each user, and use Promise.all to
// resolve when each "user promise" is complete
return Promise.all(users.map(user => {
// Be sure to add "return" here
return auth.getUser(user.key)
.then(function(userRecord) {
let email = userRecord.toJSON().email;
let zips = [];
user.forEach(setting => {
let dep = setting.val().department;
if(folderNames.includes(dep)){
zips.push(dep);
}
});
settings.push({ email, zips });
})
.catch(function(error) {
console.log("Error fetching user data:", error);
})
}));
}).then(function() {
return settings;
});
};

Using Promise.All with Firebase Cloud Fxs to fill array with FCM tokens before sendToDevice called

I was wondering if someone could help enlighten me as to how I can successfully execute this code where it waits until the entire array is filled with FCM tokens before firing sendToDevice().
I've been using these links (listed below) as references to try and resolve this but I still can not figure it out so alas here I am on SO seeking guidance. I just need to pass the tokens array once it is filled completely. I've gotten it where it fired multiple times on each push but never where it asynchronously loads and then fires ><
Firebase Real Time Database Structure for File Upload
Promise.all with Firebase DataSnapshot.forEach
https://aaronczichon.de/2017/03/13/firebase-cloud-functions/
exports.sendVenueAnnouncement = functions.database.ref(`/venueAnnouncements/{venueUid}/announcement`).onCreate(event => {
const venueUid = event.params.venueUid;
const announcement = event.data.val();
const getVenueDisplaynamePromise = admin.database().ref(`verifiedVenues/${venueUid}/displayname`).once('value');
return getVenueDisplaynamePromise.then(snapshot => {
const displayname = snapshot.val();
const payload = {
notification: {
title: `${displayname}`,
body: `${announcement}`
}
};
const getSubscriberTokensPromise = admin.database().ref(`subscribers/${venueUid}`).once('value');
return getSubscriberTokensPromise.then(snapshot => {
const tokens = [];
snapshot.forEach(function(childSnapshot) {
const key = childSnapshot.key;
const token = admin.database().ref(`pushTokens/` + key).once('value');
tokens.push(token);
});
return Promise.all(tokens);
}, function(error) {
console.log(error.toString())
}).then(function(values) {
return admin.messaging().sendToDevice(values, payload).then(response => {
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
if (error.code === 'messaging/invalid-registration-token' || error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
}
}
});
return Promise.all(tokensToRemove)
});
})
})
})
You almost have an understanding of Promises. It looks like you're also mixing callbacks with Promises. Firebase and Cloud Functions for Firebase are completely Promise based so there is no need.
With that said, you're code should look something like the following:
exports.sendVenueAnnouncement = functions.database
.ref(`/venueAnnouncements/${venueUid}/announcement`)
.onCreate(event => {
const venueUid = event.params.venueUid
const announcement = event.data.val()
let payload
let tokens = []
return admin.database()
.ref(`verifiedVenues/${venueUid}/displayname`)
.once('value')
.then(snapshot => {
const displayname = snapshot.val()
payload = {
notification: {
title: `${displayname}`,
body: `${announcement}`
}
}
return admin.database().ref(`subscribers/${venueUid}`).once('value')
})
.then(snapshot => {
snapshot.forEach((childSnapshot) => {
const key = childSnapshot.key
const token = admin.database().ref(`pushTokens/` + key).once('value')
tokens.push(token)
})
return Promise.all(tokens)
})
.then(values => {
return admin.messaging().sendToDevice(values, payload)
})
.then(response => {
const tokensToRemove = []
response.results.forEach((result, index) => {
const error = result.error
if (error) {
if (error.code === 'messaging/invalid-registration-token' || error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove())
}
}
})
return Promise.all(tokensToRemove)
})
})
Notice I don't assign the Promise to a variable. Just return it and chain a then. A Promise can return another Promise.
I suggest watching this Firecast for a better understanding of Promises.

Categories