How to make a subquery synchronously in Firebase realtime with Cloud Funtions - javascript

I'm using firebase functions with JavaScript to make a query about Firebase realtime, which will be used in an Android application. The main query obtains a collection of values that I then go through, and for each of them I launch another query. The results obtained from the second query stores in an array that is the one that I return as a result.
The problem is that the array that I return as an answer is empty, since the response is returned before the subqueries that add the data to the array end, that is, the problem I think is due to the asynchronous nature of the calls.
I have tried to reorganize the code in several ways and use promises to try that the result is not sent until all the queries have been made but the same problem is still happening.
The structure of the JSON database that I consult is the following:
"users" : {
"uidUser" : {
"friends" : {
"uidUserFriend" : "mail"
},
"name" : "nameUser",
...
},
"uidUser2" : {
"friends" : {
"uidUserFriend" : "mail"
},
"name" : "nameUser",
...
}
}
The functions are:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.searchFriends = functions.https.onCall((data,context) => {
const db = admin.database();
const uidUser = data.uidUser;
var arrayOfResults = new Array();
const refFriends = db.ref('/users/'+uidUser+'/friends');
let user;
return refFriends.once('value').then((snapshot) => {
snapshot.forEach(function(userSnapshot){
user = findUser(userSnapshot.key);
arrayOfResults.push(user);
});
return {
users: arrayOfResults
};
}).catch((error) => {
throw new functions.https.HttpsError('unknown', error.message, error);
});
});
function findUser(uid){
const db = admin.database();
const ref = db.ref('/users/'+uid);
return ref.once('value').then((snapshot) => {
console.log(snapshot.key,snapshot.val());
return snapshot.val();
}).catch((error) => {
console.log("Error in the query - "+error);
});
}
I do not know if the problem is because I do not manage the promises well or because I have to orient the code in another way.
Thank you.

Indeed, as you mentioned, you should "manage the promises" differently. Since you are triggering several asynchronous operations in parallel (with the once() method, which returns a promise) you have to use Promise.all().
The following code should do the trick:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.searchFriends = functions.https.onCall((data,context) => {
const db = admin.database();
const uidUser = data.uidUser;
var arrayOfPromises = new Array();
const refFriends = db.ref('/users/' + uidUser + '/friends');
let user;
return refFriends.once('value').then(snapshot => {
snapshot.forEach(userSnapshot => {
user = findUser(userSnapshot.key);
arrayOfPromises.push(user);
});
return Promise.all(arrayOfPromises);
})
.then(arrayOfResults => {
return {
users: arrayOfResults
};
})
.catch((error) => {
throw new functions.https.HttpsError('unknown', error.message, error);
});
});
function findUser(uid){
const db = admin.database();
const ref = db.ref('/users/' + uid);
return ref.once('value').then(snapshot => {
console.log(snapshot.key,snapshot.val());
return snapshot.val();
});
}
Note that I have modified the name of the first array to arrayOfPromises, which makes more sense IMHO.
Note also that you receive the results of Promise.all() in an array corresponding to the fulfillment values in the same order than the queries array, see: Promise.all: Order of resolved values

Related

How to take a list of IDs from an array, match each item to the ID in the firestore, and return a JSON of the associated data to front-end site?

Like the title says, I am trying to take a list of IDs from an array, match each ID to the ID in the firestore, and return an array of JSONs of the associated data to front-end site. The code below is returning an empty array.
const functions = require("firebase-functions");
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.getPlayerDataFromTokens = functions.https.onCall(async (data, context) => {
return new Promise((resolve, reject) => {
var db = admin.firestore();
const tokens = data.tokens;
let playerArray = [];
tokens.forEach((token) => {
const tokensToTokenData = db.collection("Football_Player_Data").where("Token", "==", token);
tokensToTokenData.get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
playerDataToken = doc.data()["Token"];
playerDataJersey = doc.data()["Jersey_Number"];
playerDataMultiplier = doc.data()["Multiplier"];
playerDataPlayerID = doc.data()["PlayerID"];
playerDataPosition = doc.data()["Position"];
playerDataTeam = doc.data()["Team"];
playerData = {
"Token": playerDataToken,
"PlayerID": playerDataPlayerID,
"Jersey_Number": playerDataJersey,
"Position": playerDataPosition,
"Team": playerDataTeam,
"Multiplier": playerDataMultiplier
};
playerArray.push(playerData);
});
})
.catch((error) => {
playerArray = error;
console.log("Error getting documents: ", error)
reject(playerArray);
});
})
resolve(JSON.stringify(playerArray));
});
})
Based on your code, you are building this based on a very outdated tutorial.
In Firebase Functions V1.0.0 (April 2018), functions.config().firebase was removed. It now resolves as undefined so that the modern admin.initializeApp() (with no arguments) functions properly. When invoked this way, the Admin SDK initializes based on the presence of the appropriate environment variables.
Next, you are using the Explicit Promise Construction Antipattern. In short, do not wrap your code inside new Promise((resolve, reject) => {}).
As this functions code is likely to be deployed to Google Cloud Functions, which runs on Node 10/12/14 you can use modern ES2017 syntax like async/await, deconstruction patterns, import, let, const and Promise.all.
So this means the top of your script consists of:
import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
admin.initializeApp();
As you need to fetch a list of players for each token given, you can break this logic out into it's own function:
/**
* For the given token, return an array of players linked with
* that token.
*/
async function getFootballPlayersWithToken(db, token) {
const playersWithTokenQuerySnapshot = await db
.collection("Football_Player_Data")
.where("Token", "==", token)
.get();
if (playersWithTokenQuerySnapshot.empty) {
console.log("Given token did not match any players: ", token);
return []; // or throw error
}
const playerArray = await Promise.all(
playersWithTokenQuerySnapshot.docs
.map((playerDoc) => {
const { Token, Jersey_Number, Multiplier, PlayerID, Position, Team } = playerDoc.data();
return { Token, Jersey_Number, Multiplier, PlayerID, Position, Team };
});
);
return playerArray;
}
In the above code block, it's important to note that playersWithTokenQuerySnapshot is a QuerySnapshot object, not an array. While forEach is available on the QuerySnapshot object, to use normal Array methods like .map and .reduce, you need to use the docs property instead.
This makes your Cloud Function:
exports.getPlayerDataFromTokens = functions.https.onCall(async (data, context) => {
const { tokens } = data;
try {
const db = admin.firestore();
const playersGroupedByTokenArray = await Promise.all(
tokens.map(token => getFootballPlayersWithToken(db, token))
);
// playersGroupedByTokenArray is an array of arrays
// e.g. [[token1Player1, token1Player2], [token2Player1], [], ...]
// flatten this array
// Node 12+:
// const playerArray = playersGroupedByTokenArray.flat()
// Node 10+:
const playerArray = [];
playersGroupedByTokenArray.forEach(
groupOfPlayers => playerArray.push(...groupOfPlayers)
);
return playerArray;
} catch (error) {
console.error("Error finding players for one or more of these tokens: ", tokens.join(", "))
throw new functions.https.HttpsError(
"unknown",
"Could not get players for one or more of the tokens provided",
error.code || error.message
);
}
);
As shown in the above code block, for errors to be handled properly, you must wrap them in a HttpsError as documented here.
If a token would return only one player, you can tweak the above get players function to just the following and then edit your Cloud Function as needed:
/**
* For the given token, return the matching player.
*/
async function getFootballPlayerByToken(db, token) {
const playersWithTokenQuerySnapshot = await db
.collection("Football_Player_Data")
.where("Token", "==", token)
.get();
if (playersWithTokenQuerySnapshot.empty) {
console.log("Given token did not match any players: ", token);
return null; // or throw error
}
const playerDoc = playersWithTokenQuerySnapshot.docs[0];
const { Token, Jersey_Number, Multiplier, PlayerID, Position, Team } = playerDoc.data();
return { Token, Jersey_Number, Multiplier, PlayerID, Position, Team };
}

How can I access Firestore data from within a Google Cloud Function?

This is my first time using Cloud Functions. I'm trying to make a simple call to access all the businesses stored in my Firestore collection, but when I try to log the results, I always get an empty array.
All things w/ Firebase/store are set up properly, collection name is listed properly, and have confirmed access to the database by logging db. Is there something obviously wrong with my code here? Thanks!
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access Firestore.
const admin = require('firebase-admin');
admin.initializeApp();
exports.updateBusinessData = functions.https.onRequest((request, response) => {
const db = admin.firestore()
const businessesReference = db.collection('businesses')
var businesses = []
const getBusinesses = async () => {
const businessData = await businessesReference.get()
businesses = [...businessData.docs.map(doc => ({...doc.data()}))]
for (let business in businesses) {
console.log(business)
}
response.send("Businesses Updated")
}
getBusinesses()
});
I tweaked the way you were processing the docs and I'm getting proper data from firestore.
exports.updateBusinessData = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const businessesReference = db.collection("businesses");
const businesses = [];
const getBusinesses = async () => {
const businessData = await businessesReference.get();
businessData.forEach((item)=> {
businesses.push({
id: item.id,
...item.data(),
});
});
// used for of instead of in
for (const business of businesses) {
console.log(business);
}
response.send(businesses);
};
getBusinesses();
});
Your getBusinesses function is async: you then need to call it with await. Then, since you use await in the Cloud Function you need to declare it async.
The following should do the trick (untested):
exports.updateBusinessData = functions.https.onRequest(async (request, response) => {
try {
const db = admin.firestore()
const businessesReference = db.collection('businesses')
var businesses = []
const getBusinesses = async () => {
const businessData = await businessesReference.get()
businesses = businessData.docs.map(doc => doc.data());
for (let business in businesses) {
console.log(business)
}
}
await getBusinesses();
// Send back the response only when all the asynchronous
// work is done => This is why we use await above
response.send("Businesses Updated")
} catch (error) {
response.status(500).send(error);
}
});
You are probably going to update docs in the for (let business in businesses) loop to use await. Change it to a for … of loop instead as follows:
for (const business of businesses) {
await db.collection('businesses').doc(...business...).update(...);
}
Update following the comments
Can you try with this one and share what you get from the console.logs?
exports.updateBusinessData = functions.https.onRequest(async (request, response) => {
try {
console.log("Function started");
const db = admin.firestore()
const businessesReference = db.collection('businesses');
const businessData = await businessesReference.get();
console.log("Snapshot size = " + businessData.size);
const businesses = businessData.docs.map(doc => doc.data());
for (const business of businesses) {
console.log(business);
}
response.send("Businesses Updated")
} catch (error) {
console.log(error);
response.status(500).send(error);
}
});

Retrieve multiple users info from firebase auth using Node js

I am using Firebase authentication to store users. I have two types of users: Manager and Employee. I am storing the manager's UID in Firestore employee along with the employee's UID. The structure is shown below.
Firestore structure
Company
|
> Document's ID
|
> mng_uid: Manager's UID
> emp_uid: Employee's UID
Now I want to perform a query like "Retrieve employees' info which is under the specific manager." To do that I tried to run the below code.
module.exports = {
get_users: async (mng_uid, emp_uid) => {
return await db.collection("Company").where("manager_uid", "==", mng_uid).get().then(snaps => {
if (!snaps.empty) {
let resp = {};
let i = 0;
snaps.forEach(async (snap) => {
resp[i] = await admin.auth().getUser(emp_uid).then(userRecord => {
return userRecord;
}).catch(err => {
return err;
});
i++;
});
return resp;
}
else return "Oops! Not found.";
}).catch(() => {
return "Error in retrieving employees.";
});
}
}
Above code returns {}. I tried to debug by returning data from specific lines. I got to know that the issue is in retrieving the user's info using firebase auth function which I used in forEach loop. But it is not returning any error.
Thank you.
There are several points to be corrected in your code:
You use async/await with then() which is not recommended. Only use one of these approaches.
If I understand correctly your goal ("Retrieve employees' info which is under the specific manager"), you do not need to pass a emp_uid parameter to your function, but for each snap you need to read the value of the emp_uid field with snap.data().emp_uid
Finally, you need to use Promise.all() to execute all the asynchronous getUser() method calls in parallel.
So the following should do the trick:
module.exports = {
get_users: async (mng_uid) => {
try {
const snaps = await db
.collection('Company')
.where('manager_uid', '==', mng_uid)
.get();
if (!snaps.empty) {
const promises = [];
snaps.forEach(snap => {
promises.push(admin.auth().getUser(snap.data().emp_uid));
});
return Promise.all(promises); //This will return an Array of UserRecords
} else return 'Oops! Not found.';
} catch (error) {
//...
}
},
};

JavaScript Google Cloud Function: write Stripe values to Firebase

I'm new to JavaScript and I have written the following JS Google Cloud Function with the help of various resources.
This function handles a Stripe invoice.payment_succeeded event and instead of writing the entire data I am trying to save just both the sent period_start and period_end values back to the correct location in my Firebase DB (see structure below).
How can I write these two values in the same function call?
exports.reocurringPaymentWebhook = functions.https.onRequest((req, res) => {
const hook = req.body.type;
const data = req.body.data.object;
const status = req.body.data.object.status;
const customer = req.body.data.object.customer;
const period_start = req.body.data.object.period_start;
const period_end = req.body.data.object.period_end;
console.log('customer', customer);
console.log('hook:', hook);
console.log('status', status);
console.log('data:', data);
console.log('period_start:', period_start);
console.log('period_end:', period_end);
return admin.database().ref(`/stripe_ids/${customer}`).once('value').then(snapshot => snapshot.val()).then((userId) => {
const ref = admin.database().ref(`/stripe_customers/${userId}/subscription/response`)
return ref.set(data);
})
.then(() => res.status(200).send(`(200 OK) - successfully handled ${hook}`))
.catch((error) => {
// We want to capture errors and render them in a user-friendly way, while
// still logging an exception with StackDriver
return snap.ref.child('error').set(userFacingMessage(error));
})
.then((error) => {
return reportError(error, {user: context.params.userId});
});
});//End
HTTP type functions are terminated immediately after the response is sent. In your code, you're sending the response, then attempting to do more work after that. You will have to do all the work before the response is sent, otherwise it may get cut off.
If you just want to save the period_start and period_end values, instead of the entire data object, you can use the update() method (see https://firebase.google.com/docs/database/web/read-and-write#update_specific_fields).
You should then modify your code as follows. (Just note that it is not clear from where you receive the userId value, since you don't show the stripe_ids database node in your question. I make the assumption that it is the value at /stripe_ids/${customer}. You may adapt that.)
exports.reocurringPaymentWebhook = functions.https.onRequest((req, res) => {
const hook = req.body.type;
const data = req.body.data.object;
const status = req.body.data.object.status;
const customer = req.body.data.object.customer;
const period_start = req.body.data.object.period_start;
const period_end = req.body.data.object.period_end;
admin.database().ref(`/stripe_ids/${customer}`).once('value')
.then(snapshot => {
const userId = snapshot.val();
let updates = {};
updates[`/stripe_customers/${userId}/subscription/response/period_start`] = period_start;
updates[`/stripe_customers/${userId}/subscription/response/period_end`] = period_end;
return admin.database().ref().update(updates);
})
.then(() => res.status(200).send(`(200 OK) - successfully handled ${hook}`))
.catch((error) => {...});
});

How to fix Cloud Function error admin.database.ref is not a function at exports

I'm currently trying to modify my Cloud Functions and move in under https.onRequest so that i can call use it to schedule a cron job. How it i'm getting the following error in the logs.
TypeError: admin.database.ref is not a function
at exports.scheduleSendNotificationMessageJob.functions.https.onRequest (/user_code/index.js:30:20)
at cloudFunction (/user_code/node_modules/firebase-functions/lib/providers/https.js:57:9)
exports.scheduleSendNotificationMessageJob = functions.https.onRequest((req, res) => {
admin.database.ref('/notifications/{studentId}/notifications/{notificationCode}')
.onCreate((dataSnapshot, context) => {
const dbPath = '/notifications/' + context.params.pHumanId + '/fcmCode';
const promise = admin.database().ref(dbPath).once('value').then(function(tokenSnapshot) {
const theToken = tokenSnapshot.val();
res.status(200).send(theToken);
const notificationCode = context.params.pNotificationCode;
const messageData = {notificationCode: notificationCode};
const theMessage = { data: messageData,
notification: { title: 'You have a new job reminder' }
};
const options = { contentAvailable: true,
collapseKey: notificationCode };
const notificationPath = '/notifications/' + context.params.pHumanId + '/notifications/' + notificationCode;
admin.database().ref(notificationPath).remove();
return admin.messaging().sendToDevice(theToken, theMessage, options);
});
return null;
});
});
You cannot use the definition of an onCreate() Realtime Database trigger within the definition of an HTTP Cloud Function.
If you switch to an HTTP Cloud Function "so that (you) can call use it to schedule a cron job" it means the trigger will be the call to the HTTP Cloud Function. In other words you will not be anymore able to trigger an action (or the Cloud Function) when new data is created in the Realtime Database.
What you can very well do is to read the data of the Realtime Database, as follows, for example (simplified scenario of sending a notification):
exports.scheduleSendNotificationMessageJob = functions.https.onRequest((req, res) => {
//get the desired values from the request
const studentId = req.body.studentId;
const notificationCode = req.body.notificationCode;
//Read data with the once() method
admin.database.ref('/notifications/' + studentId + '/notifications/' + notificationCode)
.once('value')
.then(snapshot => {
//Here just an example on how you would get the desired values
//for your notification
const theToken = snapshot.val();
const theMessage = ....
//......
// return the promise returned by the sendToDevice() asynchronous task
return admin.messaging().sendToDevice(theToken, theMessage, options)
})
.then(() => {
//And then send back the result (see video referred to below)
res.send("{ result : 'message sent'}") ;
})
.catch(err => {
//........
});
});
You may watch the following official Firebase video about HTTP Cloud Functions: https://www.youtube.com/watch?v=7IkUgCLr5oA&t=1s&list=PLl-K7zZEsYLkPZHe41m4jfAxUi0JjLgSM&index=3. It shows how to read data from Firestore but the concept of reading and sending back the response (or an error) is the same for the Realtime Database. Together with the 2 other videos of the series (https://firebase.google.com/docs/functions/video-series/?authuser=0), it also explains how it is important to correctly chain promises and to indicate to the platform that the work of the Cloud Function is finished.
For me, this error happened when writing admin.database instead of admin.database().

Categories

Resources