How to access multiple documents from Firestore in a cloud function - javascript

I have a cloud function that is triggered on a document write. The cloud function needs to check multiple documents based on the trigger and execute if/else statements.
I've created a function that accesses all documents with a Promise.all, but this errors when trying to access all the document information if not yet available.
export function onTriggered(change, context) {
const userPrevData = change.before.data();
const userNewData = change.after.data();
const promises = [];
// Get the uid
const uid = context.params.userId;
// User DocRef
const userDoc = firestoreInstance.collection('users').doc(uid).get();
// User Session DocRef
const userSessionDoc = firestoreInstance.collection('sessions').doc(uid).get();
// Solution DocRef
const solutionDoc = firestoreInstance.collection('solution').doc('solutionId').get();
promises.push(userDoc, userSessionDoc, solutionDoc);
return Promise.all(promises).then((snapshots) => {
// Use Promise.all with snapshot.docs.map to combine+return Promise context
return Promise.all(snapshots.map((doc) => {
// At this point, each document and their Ids print to the console.
console.log('docId:::', doc.id);
console.log('docData:::', doc.data());
const solutionDocData = getSolutionDocData(doc);
// This will print as 'undefined' until the correct document data is processed
console.log('solutionDocData:::', solutionDocData);
// This will print as 'undefined' until the correct document data is processed
const clientSeed = doc.get('clientSeed');
// Check to see if the Users' Guess is the same as the solution
if (userNewData.guess.color === solutionDocData.guess.color && userNewData.guess.number === userNewData.guess.number) {
console.log('User solution is correct');
}
}));
})
}
function getSolutionDocData(doc) {
if (doc.id === 'solutionId') { return doc.data(); }
}
I expect 'User solution is correct' if the condition is satisfied. But, I get an error because data is undefined.

The solution was to move most of the logic a .then()
return Promise.all(promises).then((snapshots) => {
// Use Promise.all with snapshot.docs.map to combine+return Promise context
return Promise.all(snapshots.map((doc) => {
// At this point, each document and their Ids print to the console.
console.log('docId:::', doc.id);
console.log('docData:::', doc.data());
return doc.data();
})).then(data => {
console.log('data:::', data);
let userDocDetails = {};
let userSessionDocDetails = {};
let solutionDocDetails = {};
data.forEach(document => {
if (document.uid === uid) { userDocDetails = document }
if (document.serverSeedEncrypted) { userSessionDocDetails = document }
if (document.solutionId) { solutionDocDetails = document }
});
});
})
I am unsure if the data will always be returned in the order of the original promise array, so I used a forEach statement to identify unique properties and assign them accordingly.

Related

Firebase Functions: Why do they sometimes fail? Why do they often complete without error but don't fulfill all tasks?

This perplexes me. I'm six months into a firebase project and have been using Javascript for firebase-functions. I've learned a lot along the way by adding transactions, promises, batch writes and neat tricks. However, it seems like complete luck for a function to execute correctly. More often than not, the functions do execute correctly, but there are strange periods when bursts of consecutive function calls where functions half complete with no errors in the logs.
For example. I have a function for when a new user joins my app. It does a little bit of server data construction and also notifies the two admins that a new user has joined. Last night I did a test run with two new users and got no notification, but their user profiles constructed correctly on the server database. I checked the function logs and there were no errors.
Am I not handling Promises in the correct way? If a firebase function hangs, does it mess up the next few function calls?
exports.onNewUser = functions.firestore
.document('/users/{userId}')
.onCreate(async (snapshot, context) => {
user = snapshot.data().username;
//Notification payload
const payload = {
notification: {
title: `New user!`,
body: `${user} has joined [AppName]`
}
};
var promises = [];
//Check if usename unique
var passed = true;
promises.push(db.runTransaction(async t => {
const docRef = db.collection('users').doc('index');
const doc = await t.get(docRef);
var newIndex = doc.data().usernames;
if (newIndex[user.toUpperCase()] == true) {
t.delete(snapshot.ref);
passed = false;
return null;
} else {
newIndex[user.toUpperCase()] = true;
t.set(docRef, { 'usernames': newIndex });
}
}));
if (!passed) return Promise.all(promises);
//add new user to Algolia database
const algoliasearch = require('algoliasearch');
const algoliaClient = algoliasearch(functions.config().algolia.appid, functions.config().algolia.apikey);
const collectionIndex = algoliaClient.initIndex(collectionIndexName);
await saveDocumentInAlgolia(snapshot, collectionIndex);
//Notify Admins
db.collection('notificationTokens')
.doc(admin1)
.get().then((doc) => {
if (doc.exists && doc.data().notificationToken != null)
promises.push(pushNotification(doc.data().notificationToken, payload));
});
db.collection('notificationTokens')
.doc(admin2)
.get().then((doc) => {
if (doc.exists && doc.data().notificationToken != null)
promises.push(pushNotification(doc.data().notificationToken, payload));
});
return Promise.all(promises);
});
Just change
return Promise.all(promises);
to
return await Promise.all(promises);
You have to wait till the promises resolve before you return the function, as that would stop the instance of the cloud function.

Accessing method vars from inside arrow function

In my vuetify/vue.js appliction, i'm trying to read a firestore collection I've stored to calculate an average rating from all ratings stored there:
function updateRating() {
let firestoreQuery = firebase
.firestore()
.collection('ratings')
.where('qrCodeId', '==', this.qrCode.id);
let ratingsSum = 0;
let ratingsAmountCounter = 0;
firestoreQuery.get().then(querySnapshot => {
querySnapshot.forEach(doc => {
ratingsSum += doc.rating;
ratingsAmountCounter++;
});
});
}
However, I can't access the ratingsSum and ratingsAmountCounter variables from inside my forEach arrow function. I've already tried using this or the var _this = this workaround, but both don't seem to work and eslint still reports:
'ratingsSum' is assigned a value but never used (no-unused-vars)
What am I doing wrong?
As the comments suggest, the problem here is that you have no code that actually reads ratingsSum and ratingsAmountCounter. The error message is telling you that ESLint has triggered on the rule called "no-unused-vars" to tell you about this. Variables that are never read are probably a bug, or at least unnecessary code.
What you should probably do is return a promise to the caller so that they can get a hold of the values, whenever the asynchronous get() is complete and the values are computed. For example:
function updateRating() {
let firestoreQuery = firebase
.firestore()
.collection('ratings')
.where('qrCodeId', '==', this.qrCode.id);
let ratingsSum = 0;
let ratingsAmountCounter = 0;
return firestoreQuery.get().then(querySnapshot => {
querySnapshot.forEach(doc => {
ratingsSum += doc.rating;
ratingsAmountCounter++;
});
return { ratingsSum, ratingsAmountCounter }
});
}
The caller of this function now receives a promise that resolves when the values are known, and they can be fetched out of the resolved object:
updateRating().then(result => {
const { ratingsSum, ratingsAmountCounter } = result;
// Now you can use ratingsSum and ratingsAmountCounter...
})
this might work, I have no way to test it since I don't know how to use vue but here goes
async function getData(){
return await firebase
.firestore()
.collection('ratings')
.where('qrCodeId', '==', this.qrCode.id)
.get()
}
})
you can then do this:
getData().then(_querySnapshot => {
var ratingsSum = 0
var ratingsAmountCounter = 0
_querySnapshot.forEach(doc => {
ratingsSum += doc.rating;
ratingsAmountCounter++;
});
});

Broken promise cloud firestore function

I am trying to retrieve the document for specific ids in the following cloud function. The loop (see "Block Get Major") for retrieving that information is running, but the logs show that it is executing AFTER the function returns data.
I am certain this is because this creates a new promise, and that I need to tie them together.
I've looked through a lot of the posts on here and elsewhere and am just not getting it.
It looks like I am creating a broken/dropped promise. I get the necessary data, with the exception of menteeMajor, which is undefined until later in the log files).
exports.getAllMatchesMCR = functions.https.onCall((data, context) => {
var dbRef = db.collection("matches");
var dbPromise = dbRef.get();
// return the main promise
return dbPromise.then(function(querySnapshot) {
var results = [];
var idx = 0;
querySnapshot.forEach(function(doc) {
// push promise from get into results
var matchObj = {
mentorName: "",
mentorEmployer: "",
mentees: []
}
var mentor = doc.data();
mentor.mentorID = doc.id;
matchObj.mentorName = mentor.mentorID;
matchObj.mentees = mentor.mentees;
for (var curIDX in matchObj.mentees) {
matchInfoObj = {};
matchInfoObj.mentorID = matchObj.mentorID;
matchInfoObj.mentorName = matchObj.mentorName;
matchInfoObj.menteeName = matchObj.mentees[curIDX];
// Block Get Major --->
var menteeRef = db.collection('users').doc(matchObj.mentees[curIDX]);
var getDoc = menteeRef.get()
.then(doc => {
if (!doc.exists) {
console.log('No such document!');
} else {
var userInfo = {};
userInfo = doc.data();
matchInfoObj.menteeMajor = userInfo.major;
// console.log('user Major:', matchInfoObj.menteeMajor);
return userInfo.major;
}
})
.catch(err => {
// console.log('Error getting document', err);
return ("No Mentee Doc")
});
console.log("in menteeInfo: ", getDoc.data());
matchInfoObj.menteeMajor = getDoc.data(); // Block Get Major <---
if (typeof something === "undefined") {
console.log('After BLOCK Major is UNDEFINED:', matchInfoObj.menteeName);
} else {
console.log('After BLOCK :', matchInfoObj.menteeMajor);
}
results.push(matchInfoObj)
}
});
// dbPromise.then() resolves to a single promise that resolves
// once all results have resolved
return Promise.all(results)
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
});
Here are two screen shots for the logs that show the order based on console.log output. Keep in mind that the sort of these log records is NEWEST at the top. Result 1 shows the start, from deployment of function and it's start, Result 2 shows the messages from the broken promise almost two minutes later.
The plain text version of what I'm trying to do is:
1. collection "Users" contains information on Mentees and Mentors
2. collection "Matches" is a doc of Mentees (array) for each Mentor, or One to Many).
3. Display one row for EACH mentor/mentee connection. I have the ids, but I need to get the names and other information for both mentees and mentors.
Plain old Result file shows that I am getting the rows I need with the Mentor ID, and the Mentee ID (labeled Mentees, that will change)
Can someone please show me the way to chain this promise?

Pass array in promise chain

I'm trying to send notifications for all guests who are invited to an event.
The below code works up to the console.log(guestPlayerIds); which returns undefined. What is the best way to pass guestPlayerIds?
exports.sendNewEventFcm = functions.database.ref('/fcm/{pushID}').onWrite(event => {
const eventID = event.params.pushID;
const dsnap = event.data.val();
// Exit when the data is deleted.
if (!event.data.exists()) {
return;
}
let guests = Object.keys(dsnap);
const promises = guests.map((data) => admin.database().ref('playerIds').child(data).once('value'));
return Promise.all(promises).then(data => {
let guestPlayerIds = [];
let returns = data.map(item => {
let itemVal = Object.keys(item.val())[0];
console.log(Object.keys(item.val())[0])
guestPlayerIds.push(itemVal);
});
}).then(guestPlayerIds => {
console.log(guestPlayerIds);
})
});
Using map is good, but you should return something inside the callback, and then also return the result of the overall map:
return data.map(item => {
let itemVal = Object.keys(item.val())[0];
console.log(Object.keys(item.val())[0])
return itemVal;
});
You actually don't need the array variable guestPlayerIds at that point. Only in the subsequent then you can use it like you already had it.

Chained and Nested promises with For Loop

I am trying to get each property of my games within chained promises (Each of the property is coming from a different async calls).
Logic of my algorithm:
Check the Network and Get the smart contract address
Register the contract containing the addresses of all the Games
Get the number of Games
For each game, perform one aSync call
per property
Print all the games and details (here I am not able
to get the updated object)
Code:
var games = [];
window.addEventListener('load', function() {
// Check the Network and assign the smart contract address
web3.eth.net.getId()
.then(function(networkId) {
let contractAddressRegistry;
if (networkId == 1) {
contractAddressRegistry = "0xQWERTYUIOPQWERTYUIOPQWERTY"
} else {
contractAddressRegistry = "0x12345678901234567890123456"
}
return contractAddressRegistry;
})
.then(function(contractAddressRegistry) {
let contractRegistry = new web3.eth.Contract(contractAbiRegistry, contractAddressRegistry);
contractRegistry.methods.numberOfGames().call()
.then(function(numberOfGames) {
for (let i = 0; i < numberOfGames; i++) {
let game = {};
game.propertyA = aSyncCallGetPropertyA(i); // Promise
game.propertyB = aSyncCallGetPropertyB(i); // Promise
game.propertyC = aSyncCallGetPropertyC(i); // Promise
}
games.push(game);
})
})
.then(function() {
console.log(games) // Empty
})
})
I tried used Promises.all() but I am not able to sync it properly as some async calls are within a then().
How can I make sure to get the object Games filled with all its properties?
You should use Promise.all like this. Basically, you need to wrap all three aSyncCallGetProperty async calls in Promise.all for waiting until they really finish then assign the results to object game.
whatever
.then(function(contractAddressRegistry) {
let contractRegistry = new web3.eth.Contract(contractAbiRegistry, contractAddressRegistry);
return contractRegistry.methods.numberOfGames().call();
})
.then(function(numberOfGames) {
return Promise.all(numberOfGames.map(() => {
return Promise.all([
aSyncCallGetPropertyA(),
aSyncCallGetPropertyB(),
aSyncCallGetPropertyC()
]).then(results => {
let game = {};
game.propertyA = results[0];
game.propertyB = results[1];
game.propertyC = results[2];
return game;
});
}));
})
.then(function(games) {
console.log(JSON.stringify(games));
})
#Lewis' code seems right but I can not make sure what numberOfGames is. Assuming that it's an integer as used in your question (not an array as treated in the other answer) here is a further rephrased version without nested .then()s.
window.addEventListener('load', function() {
web3.eth.net.getId()
.then(networkId => networkId === 1 ? "0xQWERTYUIOPQWERTYUIOPQWERTY"
: "0x12345678901234567890123456")
.then(contractAddressRegistry => new web3.eth.Contract(contractAbiRegistry, contractAddressRegistry).methods.numberOfGames().call())
.then(numberOfGames => Promise.all(Array(numberOfGames).fill()
.map(_ => Promise.all([aSyncCallGetPropertyA(),
aSyncCallGetPropertyB(),
aSyncCallGetPropertyC()]))))
.then(function(games){
games = games.map(game => ({propertyA: game[0],
propertyB: game[1],
propertyC: game[2]}));
doSomethingWith(games);
});
});

Categories

Resources