Param didn't pass to local function in firebase cloud functions - javascript

I have a firebase cloud function as follows:
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const promises = [];
jobs.forEach(job => {
promises.push(job.val());
});
return Promise.all(promises);
})
.then(jobs => {
return jobs.forEach(job => {
var percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
admin
.database()
.ref("feeds")
.child(job.feedId)
.child("upcomingWeek")
.push(candidate); // add to team's feed
}
});
})
.catch(err => {
console.log("firebase got an error: ", err);
});
});
In function foo, I call a local non-cloud function getMatchedPercent which is defined as below:
const getMatchedPercent = (candidate, job) => {
console.log("In get percent: ", candidate, job);
// do something
};
The problem is when I checked job.val() in foo before calling getMatchedPercent, I can see valid data got printed from console for job.val(). When once get in getMatchedPercent, I tried to print job, it complains it's undefined.
Is there anything I missed? Why the information of job can be lost during calling a function? Thanks!

Your problem is caused by these lines:
const promises = [];
jobs.forEach(job => {
promises.push(job.val());
});
return Promise.all(promises);
job.val() returns an object (of the data) not a promise, so Promise.all() incorrectly interprets it as a resolved promise with no value. In your next block of code, the array jobs is an array of undefined values rather than the data you were expecting.
To fix this, you would instead return the array of values rather than using Promise.all().
const jobValues = [];
jobs.forEach(job => {
jobValues.push(job.val());
});
return jobValues;
But because no asyncronous work is taking place here you can flatten your Promise chain. By doing so, you will use less memory because you won't need an array containing of all of your job.val() objects at once.
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const promises = []; // will contain any team feed update promises
jobs.forEach(jobSnapshot => { // This is DataSnapshot#forEach
const job = jobSnapshot.val();
const percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
promises.push(
admin
.database()
.ref("feeds")
.child(job.feedId)
.child("upcomingWeek")
.push(candidate) // add to team's feed
);
}
});
return Promise.all(promises);
})
.catch(err => {
console.log("Failed to update team feeds: ", err);
});
});
However, this still has another problem where some of the feed updates may succeed and others may fail which leaves your database in an unknown state. So instead you might want to consider writing to the database atomically (all data is written, or nothing at all).
This could be achieved using:
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const pendingUpdates = {}; // "path: value" pairs to be applied to the database
const feedsRef = admin.database().ref("feeds");
jobs.forEach(jobSnapshot => { // This is DataSnapshot#forEach
const job = jobSnapshot.val();
const percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
const pushId = feedsRef.push().key; // push() without arguments doesn't write anything to the database, it just generates a new reference with a push ID we can use.
const path = job.feedId + "/upcomingWeek/" + pushId;
pendingUpdates[path] = candidate; // queue add to team's feed
}
});
// apply all updates in pendingUpdates object,
// relative to feedsRef as an all-or-nothing operation.
// e.g. pendingUpdates["feed001/upcomingWeek/9jksdfghsdjhn"] = "someUserId"
// will be written to "feeds/feed001/upcomingWeek/9jksdfghsdjhn"
return feedsRef.update(pendingUpdates); // commit changes
})
.catch(err => {
console.log("Failed to apply all feed updates: ", err);
});
});

Related

Firestore slow queries are causing the entire logic to crash

I am currently designing a todo app with react and firebase without any node js server code. Everything was fine when inserting data and authenticating, but when I tried to query all the tasks where uid equals a certain value(that means all the tasks that belongs to a particular user), somehow the query gets skipped and returns null. But then, after a second or so, the array got retrieved and printed out.
The code:
function retrieveTasksOfUser(uid) {
// get all tasks where the uid equals the given value
return db.collection("tasks").where("uid", "==", uid).get();
}
function retrieveTasks() {
let res = [];
retrieveTasksOfUser(currentUser["uid"])
.then((snapshot) => {
snapshot.forEach((doc) => {
// include the doc id in each returned object
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
console.log(res);
return res;
})
.catch((err) => {
console.error("Error retrieving tasks: ", err);
return res;
});
}
let tasks = retrieveTasks();
console.log({ tasks });
EDIT:
Inspired by Frank's answer, I modified my code and got:
async function retrieveTasks() {
let res = [];
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
snapshot.forEach((doc) => {
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
return res;
}
let tasks = [];
let promise = retrieveTasks();
promise.then((res) => {
console.log("Tasks are");
tasks = res;
console.log(res);
});
console.log(tasks);
But the result turns out to be an empty array
Thanks a bunch in advance.
You're not returning anything from the top-level code in retrieveTasks, so that means that tasks will always be undefined.
The simplest fix is to return the result of the query, which then means that your return res will be bubbled up:
return retrieveTasksOfUser(currentUser["uid"])
...
But this means you're returning a promise, as the data is loaded asynchronously. So in the calling code, you have to wait for that promise to resolve with then:
retrieveTasks().then((tasks) => {
console.log({ tasks });
})
You can make all of this a lot more readable (although it'll function exactly the same) by using the async and await keywords.
With those, the code would become:
async function retrieveTasks() {
let res = [];
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
snapshot.forEach((doc) => {
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
return res;
}
let tasks = await retrieveTasks();
console.log({ tasks });
Or with a bit more modern JavaScript magic, we can make it even shorter:
async function retrieveTasks() {
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
return snapshot.docs.map((doc) => { ...doc.data, id: doc.id });
}
let tasks = await retrieveTasks();
console.log({ tasks });

How to call a function after the promise has been resolved?

The following code is for my FCM function where I am listening to firestore the getting tokens before constructing payload to send. Every time its sent the system logs that the tokens are empty. How can I make sure its not empty when sending the fcm?
let functions = require('firebase-functions');
let admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotification =functions.firestore.document('chatrooms/{chatRoomId}/messages/{messageId}')
.onWrite((snap, context) => {
let message = snap.after.data().messageBody;
let messageSender = snap.after.data().senderName;
let messageUserId = snap.after.data().userId;
let chatRoomId = context.params.chatRoomId;
let tokens = [];
let chatRoomRef = admin.firestore().collection("chatrooms").doc(chatRoomId);
return admin.firestore().runTransaction(t => {
return t.get(chatRoomRef)
.then(chatroom => {
let usersArray = chatroom.data().chatMembers;
usersArray.forEach(user_id => {
let userIdRef = admin.firestore().collection("tokens").doc(user_id);
return t.get(userIdRef).then(doc => {
if (doc.exists) {
let user_token = doc.data().token;
functions.logger.log('token: ', token);
tokens.push(user_token);
}
}).catch(err => {
functions.logger.error(err);
})
});
});
}).then(() => {
//The transaction has run successfully, we expect tokens array to not be empty
functions.logger.log("Construction the notification message.");
const payload = {
data: {
data_type: "data_type_chat_message",
title: "Tuchat",
message: message,
sender_id: messageUserId,
sender_name: messageSender,
chatRoom_id: chatRoomId
}
};
const options = {
priority: "high",
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToDevice(tokens, payload).catch(err => {
functions.logger.error(err);
});
}).catch(err => {
functions.logger.error('Transaction error: ', err);
})
});
Also before trying transactions it was returning empty tokens.
The problem is because of the way you're dealing with promises inside the forEach loop. The loop will not wait for promises to be resolved for the code inside it. It is currently just iterating as fast as possible over each user ID and not waiting for the queries to complete inside it. That means the code will continue before tokens can get populated.
You should instead collect each of the promises inside the loop and use Promise.all() to wait for the entire batch, then return the promise from that to indicate when all the work is complete. That will ensure that tokens contains everything you want before the next then in the chain is executed.
The general form of the code will be like this:
.then(() => {
const promises = []
collection.forEach(item => {
const promise = doSomeWork()
promises.push(promise)
})
return Promise.all(promises)
})
.then(() => {
// this will continue only after all promises are resolved
})
See also:
How to use promise in forEach loop of array to populate an object
Node JS Promise.all and forEach

For loop with nested Firebase queries Javascript

I have a node with the users list and a second node with the rating information for each user.
I would like to loop within the users of the first node and for each one get the rating from the second node. The problem is that the loop doesn't wait for the second node to give the answer, therefore the array returned is always empty.
I know I need to use await/async method but it's not working.
Thanks for help
return admin.database().ref('/user').once('value').then(async(snap) => {
for(const userId of snap) {
admin.database().ref('/rating/' + userId.key).once('value').then(await (snap) =>{
var rating = snap.val();
array.push([userId.key, rating]);
}).catch(error => {
console.log("failed 1 " + error.code);
});
}
return array;
}).catch(error => {
console.log("failed 2 " + error.code);
});
You need to use Promise.all(), as follows (untested):
const array = [];
const userIds = [];
return admin
.database()
.ref('/user')
.once('value')
.then((snap1) => {
const promises = [];
snap1.forEach((childSnapshot1) => {
promises.push(
admin
.database()
.ref('/rating/' + childSnapshot1.key)
.once('value')
);
userIds.push(childSnapshot1.key);
});
return Promise.all(promises);
})
.then((snap2) => {
snap2.forEach((childSnapshot2, idx) => {
const rating = childSnapshot2.val();
array.push([userIds[idx], rating]);
});
return array;
})
.catch((error) => {
//...
});
As explained in the Promise.all() doc, "returned values will be in order of the Promises passed, regardless of completion order". So for the userIds and snap2 arrays, their elements orders are corresponding: you can therefore do array.push([userIds[idx], rating]);

How to avoid nesting promises with firebase transaction?

In an onDelete trigger I'm running a transaction to update some object. I now need to do some cleanup and delete some other objects before running that transaction. After adding the cleanup code I'm getting a warning about nesting promises which I don't know how to get rid of. Here is a snippet:
exports.onDeleteAccount = functions.firestore
.document('accounts/{accountID}')
.onDelete((account, context) => {
// First do the cleanup and delete addresses of the account
const query = admin.firestore().collection('account_addresses').where('accountID', '==', account.id);
return query.get().then(addresses => {
var promises = [];
addresses.forEach(address=>{
promises.push(address.ref.delete());
})
return Promise.all(promises);
}).then(()=> {
// Then run the transaction to update the account_type object
return runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
const acc_type = account.data().type;
const accountTypeRef = admin.firestore().doc("account_types/"+acc_type);
return transaction.get(accountTypeRef).then(accTypeDoc => {
// Do some stuff and update an object called users
transaction.update(accountTypeRef, {users: users});
return;
})
})
})
.catch(error => {
console.log("AccountType delete transaction failed. Error: "+error);
});
})
I don't think the problem comes from the Transaction but from the forEach loop where you call delete(). You should use Promise.all() in order to return a single Promise that fulfills when all of the promises (returned by delete()) passed to the promises array have been fulfilled, see below.
In addition, you do runTransaction(transaction => {...}) but runTransaction is a method of Firestore. You should do admin.firestore().runTransaction(...).
Therefore, the following should do the trick:
exports.onDeleteAccount = functions.firestore
.document('accounts/{accountID}')
.onDelete((account, context) => {
// First do the cleanup and delete addresses of the account
const query = admin.firestore().collection('account_addresses').where('accountID', '==', account.id);
return query.get()
.then(addresses => {
const promises = [];
addresses.forEach(address => {
promises.push(address.ref.delete());
})
return Promise.all(promises);
}).then(() => {
// Then run the transaction to update the account_type object
return admin.firestore().runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
const acc_type = account.data().type;
const accountTypeRef = admin.firestore().doc("account_types/" + acc_type);
return transaction.get(accountTypeRef).then(accTypeDoc => {
// Do some stuff and update an object called users
transaction.update(accountTypeRef, { users: users });
})
})
})
.catch(error => {
console.log("AccountType delete transaction failed. Error: " + error);
});
})

Cannot get data from class method taken from Firebase

I have a difficulties to solve my problem. I am connected to Firebase, and trying to set a connection and check if the name is in the database.
class Db {
connect (path) {
const db = firebase.firestore();
const docRef = db.doc(path);
return docRef;
}
exist (name, path) {
this.connect(path).get()
.then(querySnapshot => {
console.log(querySnapshot.data().Users);
const users = querySnapshot.data().Users;
// return users;
if (users.indexOf(name) > -1) {
console.log('yes');
return true
} else {
console.log('no');
return false
}
})
.catch(e => {
console.log(e);
})
}
}
let databaseurl = '2048/database';
let database = new Db();
console.log(database.exist('kytek', databaseurl)); //undefined
from console log I am getting undefined
but console log return an array, I am not sure why...
part with return before if:
console.log(querySnapshot.data().Users);
const users = querySnapshot.data().Users;
return users;
and consol.log returns array
but return returning undefined
any ideas?
You're not getting your true/false values because the exist() function you've written is asynchronous, and you aren't returning the interior function. If you want to return data from the interior this.connect().get() function, you need to return that outer function... what I mean is:
exist (name, path) {
return this.connect(path).get()
.then(querySnapshot => {
// etc... rest of function here
If that doesn't work for you, you may also have to mark the function as async and use await to wait to receive the result:
async exist (name, path) {
return this.connect(path).get()
.then(querySnapshot => {
// etc... rest of function here
let databaseurl = '2048/database';
let database = new Db();
let result = await database.exist('kytek', databaseurl);
console.log(result);

Categories

Resources