Cloud Function for Firebase doesn't push userData to array - node.js

The function tries to retrieve all the users and then loops through each one and finds the ones that are premium and saves it to an array. For some reason the array is returned empty but the console log displays the correct value, i think the prblem might be that the auth user get is a async function but I couldnt figure that out.
Function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.logPremiumUsers = functions.https.onRequest((req, res) => {
const usersRef = admin.firestore().collection('users');
const query = usersRef //.where('time', '>=', 100).where('time', '<=', 1000);
const premiumUsers = [];
query.get()
.then((snapshot) => {
snapshot.forEach(async (doc) => {
const uid = doc.data().uid;
console.log(uid)
const userRecord = await admin.auth().getUser(uid)
const userData = userRecord.toJSON();
if (userData.customClaims.stripeRole === 'premiun') {
console.log(userData)
premiumUsers.push(userData);
}
});
res.status(200).json(premiumUsers);
})
.catch((error) => {
console.error(error);
res.status(500).send('Error getting premium users');
});
});
The console output:
i functions: Beginning execution of "us-central1-logPremiumUsers"
⚠ Google API requested!
- URL: "https://oauth2.googleapis.com/token"
- Be careful, this may be a production service.
> UKjZeASTvYeYm7cA6HvXc5JxqXn1
i functions: Finished "us-central1-logPremiumUsers" in 1459.693751ms
> {
> uid: 'UKjZeASTvYeYm7cA6HvXc5JxqXn1',
> email: 'a.kiselev.private#gmail.com',
> emailVerified: false,
> displayName: 'andrey',
> photoURL: undefined,
> phoneNumber: undefined,
> disabled: false,
> metadata: {
> lastSignInTime: 'Wed, 15 Feb 2023 15:35:21 GMT',
> creationTime: 'Tue, 14 Feb 2023 21:05:06 GMT',
> lastRefreshTime: 'Thu, 16 Feb 2023 08:38:04 GMT'
> },
> passwordHash: undefined,
> passwordSalt: undefined,
> customClaims: { stripeRole: 'premiun' },
> tokensValidAfterTime: 'Tue, 14 Feb 2023 21:05:06 GMT',
> tenantId: undefined,
> providerData: [
> {
> uid: 'a.kiselev.private#gmail.com',
> displayName: 'andrey',
> email: 'a.kiselev.private#gmail.com',
> photoURL: undefined,
> providerId: 'password',
> phoneNumber: undefined
> }
> ]
> }

You should not use async/await within a forEach() loop, see "JavaScript: async/await with forEach()" and "Using async/await with a forEach loop".
You can use Promise.all() as follows:
exports.logPremiumUsers = functions.https.onRequest(async (req, res) => {
try {
const usersRef = admin.firestore().collection('users');
const query = usersRef //.where('time', '>=', 100).where('time', '<=', 1000);
const premiumUsers = [];
const promises = [];
const snapshot = await query.get();
snapshot.forEach((doc) => {
const uid = doc.data().uid;
console.log(uid)
promises.push(admin.auth().getUser(uid));
});
const rawUsersArray = await Promise.all(promises);
rawUsersArray.forEach(user => {
if (userData.customClaims.stripeRole === 'premiun') {
console.log(userData)
premiumUsers.push(userData);
}
})
res.status(200).json(premiumUsers);
} catch (error) {
console.error(error);
res.status(500).send('Error getting premium users');
}
});
UPDATE following your comment:
To return the data from the Firestore user docs we can take advantage on the fact that Promise.all() returns an array that is in the order of the promises passed.
exports.logPremiumUsers = functions.https.onRequest(async (req, res) => {
try {
const usersRef = admin.firestore().collection('users');
const query = usersRef //.where('time', '>=', 100).where('time', '<=', 1000);
const premiumUsers = [];
const promises = [];
const snapshot = await query.get();
snapshot.forEach((doc) => {
const uid = doc.data().uid;
console.log(uid)
promises.push(admin.auth().getUser(uid));
});
const rawUsersArray = await Promise.all(promises);
rawUsersArray.forEach((user, idx) => {
if (userData.customClaims.stripeRole === 'premiun') {
// You want to return the data from the Firestore user docs
// Since Promise.all returns an array that is in the order of the promises passed
// we can use the idx as follows:
premiumUsers.push(snapshot.docs[idx].data());
}
})
res.status(200).json(premiumUsers);
} catch (error) {
console.error(error);
res.status(500).send('Error getting premium users');
}
});

Related

How to call an async firebase function from other async function

Using three functions (getViews, getViewCount and updateCount) I want to retrieve a youtube view count and then store it in a firestore database. Both functions work asynchronously, but when I call getViews() inside updateCount() I get the following error within updateCount:
TypeError: Cannot read properties of undefined (reading 'on') which refers to a promise.
Please let me know am I doing wrong here! Code below:
getViews:
exports.getViews = functions
.runWith({
secrets: ["YOUTUBE_API"]
})
.https.onCall(async (data, context) => {
const count = await getViewCount({});
return count;
});
updateCount:
exports.updateCount = functions.https.onRequest(async (req, res) => {
const viewData = await this.getViews({ "h": "j" }); //Error occurs here
const addData = await admin
.firestore()
.collection("viewCount")
.doc("Count")
.set(viewData)
.then(() => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
});
getViewCount:
const getViewCount = async (arg) => {
const youtube = google.youtube({
version: "v3",
auth: process.env.YOUTUBE_API,
});
const count = await youtube.channels.list({
id: process.env.YOUTUBE_CHANNEL_ID,
part: "statistics",
});
const countData = count.data.items[0].statistics.viewCount;
return countData;
}
If you want to use the code in getViews() Cloud Function as well, then it might be better to move that to a different function. Try refactoring the code as shown below:
exports.getViews = functions
.runWith({
secrets: ["YOUTUBE_API"]
})
.https.onCall(async (data, context) => {
const count = await getViewCount({}); // <-- pass required arguments
return count;
})
exports.updateCount = functions.https.onRequest(async (req, res) => {
const viewData = await getViewCount({ "h": "j" });
const addData = await admin
.firestore()
.collection("viewCount")
.doc("Count")
.set(viewData)
.then(() => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
});
// not a Cloud Function
const getViewCount = async (arg) => {
const youtube = google.youtube({
version: "v3",
auth: process.env.YOUTUBE_API,
});
const count = await youtube.channels.list({
id: process.env.YOUTUBE_CHANNEL_ID,
part: "statistics",
});
const countData = count.data.items[0].statistics.viewCount;
return countData;
}

Firebase nodejs doesn't execute return function properly

We are trying to get timeslots from our database by pushing them into an array and then returning it. The array does get filled properly according to the firebase logs, however the function never returns the data properly at all, even though we see the data to be returned.
Basically, the execution does not reach the return statement.
Our goal is to get all of the timeslots in this photo. Is there any neat way to do this?
exports.getTimeslots = functions.region('europe-west2').https.onCall((data, context) => {
const uid = context.auth.uid;
let array = [];
if (!uid)
throw new functions.https.HttpsError('no-userid', 'The requested user was not found');
else
return admin.firestore().collection('users').doc(uid).collection('modules').where('name', '!=', '').get().then(snapshot => {
snapshot.forEach(async doc => {
await admin.firestore().collection('users').doc(uid).collection('modules').doc(doc.id).collection('timeslots').where('length', '!=', -1).get().then(snapshot2 => {
snapshot2.forEach(doc2 => {
array.push(Object.assign(doc2.data(), {id: doc2.id, modID: doc.id}))
console.log("identifier #1", array)
})
console.log("Got outside");
})
console.log("Got more outside");
})
console.log("Got the most outside")
return ({ data: array });
});
//console.log("I have escaped!")
})
As #Ragesh Ramesh also said: "The solution is to make everything async await.", I tried replicating your code using the data structure, and code below:
Data Structure:
Code:
// firebase db
const db = firebase.firestore();
exports.getTimeslots = functions.region('europe-west2').https.onCall((data, context) => {
const getData = async () => {
const uid = context.auth.uid;
let array = [];
if (!uid) {
throw new functions.https.HttpsError('no-userid', 'The requested user was not found');
} else {
const modulesRef = db.collection('users').doc(uid).collection('modules');
const modulesQuery = modulesRef.where('name', '!=', '');
const modulesQuerySnap = await modulesQuery.get();
const moduleDocuments = modulesQuerySnap.docs.map((doc) => ({ id: doc.id }));
for (const moduleDocument of moduleDocuments) {
const timeslotsRef = modulesRef.doc(moduleDocument.id).collection('timeslots');
const timeslotsQuery = timeslotsRef.where('length', '!=', -1);
const timeslotsQuerySnap = await timeslotsQuery.get();
const timeslotDocuments = timeslotsQuerySnap.docs.map((doc) => ({ id: doc.id, data: doc.data() }));
for (const timeslotDocument of timeslotDocuments) {
array.push(Object.assign(timeslotDocument.data, {id: timeslotDocument.id, modID: moduleDocument.id}))
}
}
return ({ data: array });
}
}
return getData()
.then((response) => {
// console.log(response);
return response;
});
}
The Reference page for Firestore reveals the docs property on the snapshot.
Upon running the code, here's the output:
{
data: [
{
length: 1,
id: '8UIlspnvelEkCUauZtWv',
modID: 'RmL5BWhKswEuMWytTIvZ'
},
{
title: 'Modules',
length: 120,
day: 1,
startTime: 720,
id: 'E5fjoGPyMswOeq8mDjz2',
modID: 'qa15lWTJMjkEvOU74N1j'
},
{
startTime: 360,
title: 'English',
day: 2,
length: 240,
id: '7JHtPSO83flO3nFOc0aE',
modID: 'qa15lWTJMjkEvOU74N1j'
}
]
}
This is a issue with how your function is written. Instead of
return ({ data: array });
Your function sometimes returns.
admin.firestore().collection('users').doc(uid).collection('modules').where('name', '!=', '').get()
Which is a promise by itself. You are chaining async inside then function. The solution is to make everything async await.
const data = await admin.firestore().collection('users').doc(uid).collection('modules').where('name', '!=', '').get()

Problem to use a Map in Firebase Functions

I am trying to get the length of a Map and I keep getting "undefined". Could please someone tell me what am I doing wrong?
This is the part of the code that gives me problems.
const GYMdetail: { [key: string]: number} = {};
GYMdetail[`${doc.data().name} (${doc.data().personalID})`] = 650;
const subtotal = 650 * GYMdetail.size;
This is the complete function code
export const addGymMonthlyExpense =
functions.https.onRequest((request, response) => {
const query1 = admin.firestore().collection("users");
const query = query1.where("subscriptions.gym.active", "==", true);
query.get()
.then(async (allUsers) => {
allUsers.docs.forEach(async (doc) => {
if (doc != undefined) {
const houseForGym = doc.data().subscriptions.gym.house;
await admin.firestore()
.doc(`houses/${houseForGym}/expenses/2022-04`)
.get().then((snapshot) => {
if (snapshot.data() == undefined) {
console.log(`${houseForGym}-${doc.data().name}: CREAR!!`);
} else if (snapshot.data()!.issued == false) {
let detail: { [key: string]: any} = {};
const GYMdetail: { [key: string]: number} = {};
detail = snapshot.data()!.detail;
GYMdetail[
`${doc.data().name} (${doc.data().personalID})`
] = 650;
const subtotal = 650 * GYMdetail.size;
detail["GYM"] = {"total": subtotal, "detail": GYMdetail};
snapshot.ref.set({"detail": detail}, {merge: true});
}
return null;
})
.catch((error) => {
console.log(
`${houseForGym} - ${doc.data().name}: ${error}`);
response.status(500).send(error);
return null;
});
}
});
response.send("i");
})
.catch((error) => {
console.log(error);
response.status(500).send(error);
});
});
Since you are executing an asynchronous call to the database in your code, you need to return a promise from the top-level code; otherwise Cloud Functions may kill the container when the final } executes and by that time the database load won't be done yet.
So:
export const addGymMonthlyExpense =
functions.https.onRequest((request, response) => {
const query1 = admin.firestore().collection("users");
const query = query1.where("subscriptions.gym.active", "==", true);
return query.get()
...
Next you'll need to ensure that all the nested get() calls also get a chance to finish before the Functions container gets terminated. For that I recommend not using await for each nested get call, but a single Promise.all for all of them:
query.get()
.then(async (allUsers) => {
const promises = [];
allUsers.docs.forEach((doc) => {
const houseForGym = doc.data().subscriptions.gym.house;
promises.push(admin.firestore()
.doc(`houses/${houseForGym}/expenses/2022-04`)
.get().then((snapshot) => {
...
});
});
response.send("i");
return Promise.all(promises);
})
.catch((error) => {
console.log(error);
response.status(500).send(error);
});

How to properly make a promise.all function with a .map?

I attempted to ask a simplified version of this here but I realized that it probably doesn't contain enough information. So unfortunately I'm just going to post the whole thing and hope that it doesn't offend anyone.
Basically I have 4 functions, with their stated purposes below:
1.InitializeDrive() takes a user email and uses Google's JWT user impersonation method to return the appropriate authorization for that user.
2.listfiles() calls on InitializeDrive() for authorization and retrieves a list of the documents associated with the associated user auth.
3.singleUserData() expects a list of the files such as that from listfiles() and refines them.
4.all_user_data() is intended to be the Promise.all async function that combines all of the aforementioned functions, and maps to an array of users getListUsers(), creating a master array containing all of the refined file data for each user.
Basically my poorly formed and ignorant question is 'how can make the all_user_data() in such a way that it will return the aforementioned master array?'. I am relatively new to async programming, and have a persistent problem getting confused with nested functions.
// loads credentials from .env file
require('dotenv').config();
const util = require('util');
const { google } = require('googleapis');
const { logger } = require('handlebars');
const { getListUsers } = require('./get_users');
const target_users = getListUsers();
function initializeDrive(version, user) {
return new Promise((resolve, reject) => {
const client_email = process.env.GOOGLE_CLIENT_EMAIL;
console.log(client_email);
// add some necessary escaping so to avoid errors when parsing the private key.
const private_key = process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n');
// impersonate an account with rights to create team drives
const emailToImpersonate = user;
const jwtClient = new google.auth.JWT(
client_email,
null,
private_key,
['https://www.googleapis.com/auth/drive'],
emailToImpersonate,
);
return google.drive({
version: version,
auth: jwtClient,
});
});
}
const listfiles = async (pagetokenObj, user) => {
let pageToken = '';
let version = 'v3';
if (pagetokenObj !== undefined) {
pageToken = pagetokenObj.pageToken;
}
const drive = await initializeDrive(version, user);
//const drive = initializeDrive(version,user);
return new Promise((resolve, reject) => {
drive.files.list(
{
pageSize: 100,
fields:
'nextPageToken, files(id, name, owners(emailAddress),
sharingUser(emailAddress), permissions)',
...(pageToken ? { pageToken } : {}),
},
function (err, { data: { nextPageToken = '', files = [] } = {} }) {
if (err) {
return reject(err);
}
if (!nextPageToken) {
return resolve(files);
}
// if page token is present we'll recursively call ourselves until
// we have a complete file list.
return listfiles({ pageToken: nextPageToken }).then((otherfiles) => {
resolve(files.concat(otherfiles));
});
},
);
});
};
//Function returns formatted
const singleUserData = async (files) => {
return new Promise((resolve, reject) => {
const single_user_json = await listfiles();
const res = single_user_json
.filter((doc) => Boolean(doc.permissions))
.map((doc) => {
return {
id: doc.id,
sharedWith: doc.permissions
.filter((permission) => permission.type === 'user')
.map((permission) => {
return {
emailAddress: permission.emailAddress,
role: permission.role || null,
};
}), // convert the perm object into a string (email address)
};
});
// this is how you get nicer console.logs instead of just [Object] and [Array] BS
// https://stackoverflow.com/a/10729284/9200245
console.log(util.inspect(res, { showHidden: false, depth: null, colors: true }));
if (err) {
return reject(err);
}
if (!nextPageToken) {
return resolve(files);
};
})
};
const all_user_data = async() => {
const users = await getListUsers();
const pagetokenObj = "{ pageToken = '' } = {}";
Promise.all(
users.map(async (pagetokenObj,user) => {
const files = await listfiles(pagetokenObj, user);
console.log(files);
const singleUserData = await singleUserData(files)
console.log(singleUserData);
}),
);
console.log(users)
}
//returnJSON();
//getListUsers();
//singleUserData();
all_user_data();
I finally figured it out. Basically needed to await a lot more and properly declare async.
// loads credentials from .env file
require('dotenv').config();
const util = require('util');
const { google } = require('googleapis');
const { logger } = require('handlebars');
const { getListUsers } = require('./get_users');
const target_users = getListUsers();
async function initializeDrive(user) {
// return new Promise((resolve, reject) => {
const client_email = process.env.GOOGLE_CLIENT_EMAIL;
console.log(user + ': ' + client_email);
// add some necessary escaping so to avoid errors when parsing the private key.
const private_key = process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n');
// impersonate an account with rights to create team drives
const emailToImpersonate = await user;
const jwtClient = new google.auth.JWT(
client_email,
null,
private_key,
['https://www.googleapis.com/auth/drive'],
emailToImpersonate,
);
return google.drive({
version: "v3",
auth: jwtClient,
});
//});
}
//
async function listfiles(pagetokenObj, user) {
let pageToken = '';
if (pagetokenObj !== undefined) {
pageToken = pagetokenObj.pageToken;
}
const drive = await initializeDrive(user);
//console.log(drive)
return new Promise((resolve, reject) => {
drive.files.list(
{
pageSize: 100,
fields:
'nextPageToken, files(parents, id, name, properties, owners(emailAddress), sharingUser(emailAddress), permissions)',
...(pageToken ? { pageToken } : {}),
},
function (err, { data: { nextPageToken = '', files = [] } = {} }) {
if (err) {
return reject(err);
}
if (!nextPageToken) {
return resolve(files);
}
// if page token is present we'll recursively call ourselves until
// we have a complete file list.
return listfiles({ pageToken: nextPageToken }).then((otherfiles) => {
resolve(files.concat(otherfiles));
});
},
);
});
};
async function singleUserData(user) {
const pagetokenObj = "{ pageToken = '' } = {}"
const single_user_json = await listfiles(pagetokenObj, user);
// return new Promise ((resolve, reject) => {
console.log(user+ ":" + JSON.stringify(single_user_json));
const res = await single_user_json
.filter((doc) => Boolean(doc.permissions))
.map((doc) => {
return {
id: doc.id,
sharedWith: doc.permissions
.filter((permission) => permission.type === 'user')
.map((permission) => {
return {
emailAddress: permission.emailAddress,
role: permission.role || null,
};
}) // convert the perm object into a string (email address)
};
});
return JSON.stringify(res);
//})
}
async function getAllUserData() {
try {
const users = await getListUsers();
// const userString = JSON.parse(users);
console.log("test22222222222222: " + users)
const usersData = await Promise.all(
users.map((user) => {
return singleUserData(user);
})
);
console.log("[" + usersData + "]");
return usersData;
} catch (error) {
console.log(error);
}
}
getAllUserData();
You're almost there.
Remove listfiles call in all_user_data
Pass user to singleUserData
Remove pagetokenObj in all_user_data
Remove async and await inside users.map and return promise directly
Await Promise.all, assign it to a variable and return that variable
Remove unnecessary Promise wrapping in singleUserData
Change function signature of singleUserData to take in a user
Pass user to listfiles in singleUserData
Return res instead of files in singleUserData
Here are the changes I made to all_user_data and singleUserData:
// Function returns formatted
async function singleUserData(user) {
const single_user_json = await listfiles(user);
const res = single_user_json
.filter((doc) => Boolean(doc.permissions))
.map((doc) => {
return {
id: doc.id,
sharedWith: doc.permissions
.filter((permission) => permission.type === "user")
.map((permission) => {
return {
emailAddress: permission.emailAddress,
role: permission.role || null,
};
}), // convert the perm object into a string (email address)
};
});
return res;
};
async function getAllUserData() {
const users = await getListUsers();
const usersData = await Promise.all(
users.map((user) => {
return singleUserData(user);
})
);
return usersData;
};
const usersData = await getAllUserData();

Async-Await & Bottleneck Rate Limiting using Promise.all

I'm using an API which has a rate limit of 500 requests / min.
Therefore I decided to use bottleneck. But I need to execute array of async functions which generates a Promise to make that API call. I'm not sure I'm on the right way. Because API responses me with "Exceeded rate limit of 83 in 10_seconds" where I just only send 70 requests in 10 seconds.
Here is how I call the main function:
const result = await Helper.updateUsers(request.query.where);
..
..
Here is the helper.js
const Boom = require("boom");
const mongoose = require("mongoose");
const Bottleneck = require("bottleneck");
const Intercom = require("intercom-client");
const config = require("../../config/config");
const client = new Intercom.Client({
token: config.intercom.access_token
});
const User = mongoose.model("User");
const Shop = mongoose.model("Shop");
// create a rate limiter that allows up to 70 API calls per 10 seconds,
// with max concurrency of 70
const limiter = new Bottleneck({
maxConcurrent: 70,
minTime: 10000
});
// Helpers
// This function prepares a valid Intercom User Object.
// user -> User Object
// returns <Promise>
const prepareAndUpdateUser = async user => {
try {
let userData = {
email: user.email,
user_id: user._id,
companies: []
};
Shop.find({ _id: { $in: user.account.shops } })
.exec((err, shops) => {
if (err) console.log("INTERCOM UPDATE USER", err);
shops.forEach(shop => {
let shopData = {
company_id: shop._id,
name: shop.name[shop.defaultLanguage.code]
};
userData.companies.push(shopData);
});
// Update Intercom Promise
return client.users.create(userData);
});
} catch (e) {
return Boom.boomify(err);
}
};
module.exports.updateUsers = async query => {
try {
const users = await User.find(query)
.populate("account")
.limit(700);
if (users && users.length > 0) {
limiter.schedule(() => {
const allTasks = users.map(
async user => await prepareAndUpdateUser(user)
);
return Promise.all(allTasks);
});
return users.length;
} else {
return 0;
}
} catch (err) {
return Boom.boomify(err);
}
};
Am I using Bottleneck & Async-Await correct?
The first thing to point out is your use of callbacks in an async method instead of awaiting a promise. You should use the promise returning version of Shops.find() and await the results.
async function prepareAndUpdateUser(user) {
try {
const shops = await Shop.find({ _id: { $in: user.account.shops } }).exec();
return client.users.create({
email: user.email,
user_id: user._id,
companies: shops.map(shop => {
return {
company_id: shop._id,
name: shop.name[shop.defaultLanguage.code]
};
})
});
} catch (e) {
return Boom.boomify(err);
}
}
In your updateUsers method you're using the rate limiter backwards. You want to map the users into the rate limiter so that it can control when prepareAndUpdateUser is called, currently you'll be requesting everything in parallel. You also want to wait for promise returned by the rate limiter to resolve. Essentially you'll want to move limiter.scehdule(...) into user.map(...).
async function updateUsers(query) {
try {
const users = await User.find(query)
.populate("account")
.limit(700);
if (users && users.length > 0) {
// Schedule an update for each user
const allTasks = users.map(user => {
// Schedule returns a promise that resolves when the operation is complete
return limiter.schedule(() => {
// This method is called when the scheduler is ready for it
return prepareAndUpdateUser(user)
})
});
// Wait for all the scheduled tasks to complete
await Promise.all(allTasks);
return users.length;
} else {
return 0;
}
} catch (err) {
return Boom.boomify(err);
}
}

Resources