firebase Nodejs async cloud function not finishing execution - node.js

I am building a firebase cloud function that when I change a value in my db, I get UIDs of all users in db then check a node called wishlists to see if the user has products in one of his wishlists. if the wishlists of the users do not have products the cloud function sends a notification to the user with a msg to fill his wishlist. The function works for one user but when I iterate the user's nodes and call the work that I do for one user, it finishes before completing all the work. I believe that the probléme comes from not handling properly the async functions. Any help is appreciated. Thanks!
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const runtimeOpts = {
timeoutSeconds: 180,
memory: '1GB'
}
admin.initializeApp(functions.config().firebase);
exports.sendAdminNotificationForEmptyWishlists = functions.runWith(runtimeOpts)
.database.ref('/dev/fireNotifCloudFunc')
.onWrite(async () => {
// get all users uids
const ref = admin.database().ref().child('Influencers')
return ref.once("value")
.then(function(snapshot) {
snapshot.forEach( async function(child) {
await checkUserWishlists(child.key)
})
})
.catch(function(error){
console.log(error);
});
});
//check users wishlists
async function checkUserWishlists(uid){
var ref = admin.database().ref().child('Articles').child(uid)
ref.once("value")
.then(function(snapshot) {
if(snapshot.exists()){
const wishlistsNumber = snapshot.numChildren();
let wishlistCount = 0
let userHasProduct = false
var BreakException = {};
try {
snapshot.forEach(function(child) {
wishlistCount = wishlistCount + 1
//wishlist node
if(child.child("articles").exists()){
//user have not empty wishlist
userHasProduct = true
throw BreakException
}
});
}
catch(err){
if (err !== BreakException) throw err;
}
if((wishlistCount === wishlistsNumber) && !userHasProduct){
let fcmToken = (async () => {
fcmToken = await getUserFirestoreFCM(uid).then(()=>{
sendPushNotificationToUser(fcmToken,"emptyWishlists")
return fcmToken
})
.catch((error)=>{
console.log("error getting fcm token " + error);
})
})();
}
}
else{
console.log("user did not exist in articles node" );
return
}
})
.catch(function(error){
console.log(error);
});
}
async function getUserFirestoreFCM(uid){
let documentRef = admin.firestore().doc("users_table/" + uid);
documentRef.get().then(documentSnapshot => {
if (documentSnapshot.exists) {
console.log('Document exist')
return documentSnapshot.get('fcmToken')
}
else {
console.log('Document dosen\'t exist')
return
}
})
.catch(function(error){
console.log('error geting fcm token from firestore ' + error);
})
}
async function sendPushNotificationToUser(fcm,type){
//get fcm token
if(type === "emptyWishlists"){
//notification content
const payloadEmptyWishlists = {
notification: {
title: `Vos wishlists sont vides !`,
body: "Hello veuillez ajouter des produits dans vos wishlists pour joindre les
influenceurs dans le fil d'acceil, cheers ! ",
icon: "default",
sound: "default"
}
};
const options = {
priority: "high",
timeToLive: 60 * 60 * 24
};
//send notif
return admin.messaging().sendToDevice(fcm,
payloadEmptyWishlists,options).then(function(response){
console.log('Notification sent successfully:',response);
return
})
.catch(function(error){
console.log('Notification sent failed:',error);
});
}
}
this is a screeshot from the logs

I think you should not use async/await in forEach loop callaback. This generally is tricky. Searching for good explanation I have found this article. I think its very short and clear.
In this situation I don't think that you need this async/await, but I would have to implement it to be sure. Anyway, if it appear that you still need async there is workaround in mentioned article.

Related

When using forEach in a Cloud Function, I can't make .sendToDevice() method work

I can send messages to the iOS device using the second function shown below.
I get the document id in the collection name "users" which is at the first level and send the message using the token stored in the tokens subcollection therefore admin.firestore().collection('users').doc(userId).collection('tokens').
I have to change the way the function looks for the user. Rather than relying on the document id of the user, I now need a query in order to find the user. Being a query, unless I'm wrong, I'm forced to use forEach in order to send the message to the user. The function now looks as shown immediately below. In essence, once I know I have the user that needs to receive the message, I'm using the original function format to send the message but the message is never sent. All I see in the logs is Firebase messaging error and I have yet to figure out where the mistake is.
exports.sendMessage = functions.https.onRequest(async (res, response) => {
const body = res.body;
const orderTotal = body.total;
const orderId = String(body.id);
const query = await usersRef.where('token', '==', token).get();
if (query.empty) {
console.log('No matching documents.');
return;
}
query.forEach(doc => {
const tokens = usersRef.doc(doc.id).collection('tokens');
tokens.get()
.then(snapshot => {
const results = [];
snapshot.forEach(doc => {
const fcmToken = doc.data().fcmToken
console.log("fcmToken =>", fcmToken);
results.push(fcmToken);
})
const payload = {
notification: {
title_loc_key: 'notify_title',
subtitle_loc_key: 'notify_subtitle',
body_loc_key: 'notify_body',
badge: '1',
sound: 'cha-ching.caf',
mutable_content: 'true'
},
data: {
'total': orderTotal,
'orderId': orderId
}
}
response.send([results, , payload])
admin.messaging().sendToDevice(results, payload).then((response) => {
// Response is a message ID string.
console.log('Successfully sent message:', response);
return { success: true };
}).catch((error) => {
return { error: error.code };
})
})
.catch(err => {
console.log("Error getting documents", err);
});
});
});
This is the original function which I used when using the document id.
exports.sendMessage = functions.https.onRequest(async (res, response) => {
const body = res.body
const orderTotal = body.total
const orderId = String(body.id)
const tokenReference = admin.firestore().collection('users').doc(userId).collection('tokens')
const tokenSnapshots = await tokenReference.get()
const results = []
tokenSnapshots.forEach(tokenSnapshot => {
const fcmToken = tokenSnapshot.data().fcmToken
results.push(fcmToken)
})
const payload = {
notification: {
title_loc_key: 'notify_title',
subtitle_loc_key: 'notify_subtitle',
body_loc_key: 'notify_body',
badge: '1',
sound: 'cha-ching.caf',
mutable_content: 'true'
},
data: {
'total': orderTotal,
'orderId': orderId
}
}
response.send([results, , payload])
admin.messaging().sendToDevice(results, payload).then((response) => {
console.log('Successfully sent message:', response);
return { success: true };
}).catch((error) => {
return { error: error.code };
})
})
Screenshot of the error:
The onRequest() function terminates when you return a response. You are using sendToDevice() after response.send(). Also make sure you are handling all the promises correctly. Try refactoring the using async-await syntax as shown below:
exports.sendMessage = functions.https.onRequest(async (res, response) => {
try {
const body = res.body;
const orderTotal = body.total;
const orderId = String(body.id);
const query = await usersRef.where("token", "==", "TOKEN").get();
if (query.empty) {
console.log("No matching documents.");
return;
}
// Query tokens of all users at once
const tokenSnapshots = await Promise.all(
query.docs.map((user) => usersRef.doc(user.id).collection("tokens").get())
);
// Array of all fcmTokens
const results = tokenSnapshots.reduce((acc, snapshot) => {
acc = [...acc, ...snapshot.docs.map((doc) => doc.data().fcmToken)];
return acc;
}, []);
const payload = { ...FCM_PAYLOAD };
const fcmResponse = await getMessaging().sendToDevice(results, payload);
console.log("Successfully sent message:", fcmResponse);
response.send([results, , payload]);
} catch (error) {
console.log(error);
response.json({ error: "An error occured" });
}
});
Also checkout Terminating HTTP Cloud Functions.
After days of working on this, it turns out there wasn't anything wrong with the function. I don't know how VPN works but the fact that I had it enabled on my iPhone was the reason I wasn't getting the notification.
I paused the VPN and the notification was received.

Fetching Realtime Database in Cloud Function return the wrong object

I'm trying to fetch data from my Realtime database but when I do, it return my this object:
{"domain":{"domain":null,"_events":{},"_eventsCount":3,"members":[]}} instead of something like this: {0: 'user1', 1: 'user2'}.
There is a screen from my Realtime database:
My code:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
exports.getProductTypeUpdate = functions.database
.ref("Products/{product}/type")
.onUpdate((snapshot, context) => {
const type = snapshot.after.val();
console.log("Product type created: " + type);
const users = admin.database().ref("Notif/type/" + type)
.once("value").then((snapshot) => {
return snapshot.val();
}).catch((error) => {
console.log("Error sending message:", error);
return false;
});
console.log("Users fetched are: " + JSON.stringify(users));
const result = {etat: "create", users: users};
console.log("Final result is: " + JSON.stringify(result));
return result;
});
And the "error" (it's write in french but it's not important):
Thank you for help !
The problem is that your users variable is actually a Promise. To log it inside the code, you have to either await it, or use then().
It's probably easiest to start using async and await here, which would make your code look like this:
exports.getProductTypeUpdate = functions.database
.ref("Products/{product}/type")
.onUpdate(async (snapshot, context) => {
// 👆
const type = snapshot.after.val();
try {
const snapshot = await admin.database().ref("Notif/type/" + type).once("value");
let users = snapshot.val();
const result = {etat: "create", users: users};
return result;
}
catch (error) {
console.log("Error sending message:", error);
return false;
}
});
I also recommend reading the Firebase documentation on sync, async, promises and how to terminate functions, and the MDN documentation on async/await.
I did it but I think my Node.js version was not able to run ES6, I get messages like "Unexpected token", so I did that and now it works:
exports.getProductTypeUpdate = functions.database
.ref("Products/{product}/type")
.onUpdate((snapshot, context) => {
// on récupère le type du produit
const type = snapshot.after.val();
console.log("Le type du produit créé est: " + type);
// on récupère les utilisateurs interresse par le type d'objet
admin.database().ref("Notif/type/" + type)
.once("value").then((snapshot) => {
const users = snapshot.val();
console.log("Les utilisateurs dans " +
"le snapshot sont:" + snapshot.val());
console.log("Les utilisateurs tirés sont: " + users);
// Retourne les utilisateurs Ă  notifier et l'Ă©tat de la requĂŞte
const result = {state: "create", users: users};
console.log("Le résultat final est: " + JSON.stringify(result));
return result;
}).catch((error) => {
console.log("Error sending message:", error);
return false;
});
});

How to only get one value of child firebase cloud function?

I want to send notifications that trigger into pengumuman topic.
export const onNotifPengumuman = functions.database.ref('/pengumuman_course/{course_id_p}/{pengumuman_id}')
.onCreate((snapshot,context) =>{
const course_id_p = context.params.course_id_p;
const pengumuman_id = context.params.pengumuman_id;
const nama_matkul = admin.database().ref('/courses/'+course_id_p+'name').once('value').then(snap =>{
return snapshot.val();
}).catch(error =>
{
console.log(error);
})
console.log(`cobacobacoba ${nama_matkul}`);
return admin.database().ref('pengumuman/' + pengumuman_id + '/').once('value').then(snap =>{
const pengumumanData = snap.val();
const notifDataPengumuman = {
data:{
data_type: "pengumuman ",
title: "Pengumuman Baru", // data bebas (key, value)
body: `${nama_matkul}`, // chatId = const chatId
sound: "default"
}
}
return admin.messaging().sendToTopic(course_id_p, notifDataPengumuman)
.then(function(response) {
console.log("Successfully sent message:", response);
})
.catch(function(error) {
console.log("Error sending message:", error);
});
}).catch(error => {
console.log(error);
})
});
In the first ref functions.database.ref('/pengumuman_course/{course_id_p}/{pengumuman_id}') I want to access and trigger this child in firebase real time database, the code below :
enter image description here
after that in this code return admin.database().ref('pengumuman/' + pengumuman_id + '/') I'm tring to get all of information about pengumuman and send it into users. The code in below :
enter image description here
But before that I want to get pengumuman name in the courses ref in the database to get value of name ,with this code :
const nama_matkul = admin.database().ref('/courses/'+course_id_p+'name').once('value').then(snap =>{
return snapshot.val();
}).catch(error =>
{
console.log(error);
})
enter image description here
The problem is when I'm using that code to get child name and store it into matkul, when I send/log ,it will return promises object. I want the result showing "REKAYASA PERANGKAT LUNAK".
Thanks and sorry for bad explanation
[FIXED]
Im trying the solution and found this code
export const onNotifPengumuman = functions.database.ref('/pengumuman_course/{course_id_p}/{pengumuman_id}')
.onCreate((snapshot,context) =>{
const course_id_p = context.params.course_id_p;
console.log(`course id pengumuman ${course_id_p}`);
const pengumuman_id = context.params.pengumuman_id;
admin.database().ref('/courses/' + course_id_p + '/').once('value').then(snap2 =>{
const nama_matkul = snap2.child('name').val();
console.log(`nama matkul dari sini ${nama_matkul}`);
admin.database().ref('pengumuman/' + pengumuman_id + '/').once('value').then(snap =>{
const pengumumanData = snap.val();
const notifDataPengumuman = {
data:{
data_type: "pengumuman",
title: "Pengumuman Baru", // data bebas (key, value)
body: `Judul :${nama_matkul}`, // chatId = const chatId
sound: "default"
}
}
return admin.messaging().sendToTopic(course_id_p, notifDataPengumuman)
.then(function(response) {
console.log("Successfully sent message:", response);
})
.catch(function(error) {
console.log("Error sending message:", error);
});
}).catch(error => {
console.log(error);
})
}).catch(error =>{
console.log(error);
})
});
You will need to resolve promises in order to get the value returned by them. What you are doing at the moment is assigning nama_matkul the promise but you never wait for it to complete.
Async/Await
You can either use async/await by defining your function as asynchronous:
.onCreate(async (snapshot,context) =>{
// Your asynchronous code here
}
You can then resolve the promise by running
const nama_matkul = (await admin.database().ref('/courses/'+course_id_p+'name').once('value')).val();
If you need to handle exceptions, wrap the promise and await in a try catch block.
After refactoring your code, it might look something like this:
export const onNotifPengumuman = functions.database.ref('/pengumuman_course/{course_id_p}/{pengumuman_id}')
.onCreate(async (snapshot,context) => {
try {
const course_id_p = context.params.course_id_p;
const pengumuman_id = context.params.pengumuman_id;
const nama_matkul = (await admin.database().ref('/courses/'+course_id_p+'name').once('value')).val();
console.log(`cobacobacoba ${nama_matkul}`);
const pengumumanData = (await admin.database().ref('pengumuman/' + pengumuman_id + '/').once('value')).val();
const notifDataPengumuman = {
data: {
data_type: "pengumuman ",
title: "Pengumuman Baru", // data bebas (key, value)
body: `${nama_matkul}`, // chatId = const chatId
sound: "default"
}
}
try {
await admin.messaging().sendToTopic(course_id_p, notifDataPengumuman);
console.log("Successfully sent message:", response);
} catch (messageSendError) {
console.log("Error sending message:", messageSendError);
}
} catch (error) {
console.log(error);
}
});
Then/Catch
If you do want to stick with the current setup you have and work with callbacks, you can instead keep the .then call and handle your application logic in the callback; your code might look something like this:
export const onNotifPengumuman = functions.database.ref('/pengumuman_course/{course_id_p}/{pengumuman_id}')
.onCreate((snapshot,context) => {
const course_id_p = context.params.course_id_p;
const pengumuman_id = context.params.pengumuman_id;
admin.database().ref('/courses/'+course_id_p+'name').once('value')
.then(nameSnapshot => {
const nama_matkul = nameSnapshot.val();
console.log(`cobacobacoba ${nama_matkul}`);
admin.database().ref('pengumuman/' + pengumuman_id + '/').once('value')
.then(dataSnapshot => {
const pengumumanData = dataSnapshot.val();
const notifDataPengumuman = {
data: {
data_type: "pengumuman ",
title: "Pengumuman Baru", // data bebas (key, value)
body: `${nama_matkul}`, // chatId = const chatId
sound: "default"
}
}
return admin.messaging().sendToTopic(course_id_p, notifDataPengumuman)
.then(response => console.log("Successfully sent message:", response))
.catch(error => console.log("Error sending message:", error));
})
.catch(error => console.log(error));
})
.catch(error => console.log(error))
});
You can of course use a combination of then/catch and await if you so wish, whether this is a good practice or when to use which really depends on the situation in which you use it.

How do I show the content of two entities in Firebase?

I use node.js with express.js and only show me the data of the book, but I also want to show the authors but I do not know how to do it.
Any ideas ?
Note :English is not my mother tongue; please excuse any errors on my part
My Controller :
'use strict'
var Libro = require('../models/libro');
var admin = require('firebase-admin');
const firebaseDb = admin.database();
function getLibros(req,res){
firebaseDb.ref('libros').on('value',gotData,errData);
}
function gotData(data){
console.log(data.val());
}
function errData(){
console.log('TestErr');
}
Result:
{ '-LGeoSBuikhIuSicBH6I':
{
autor: '-LGc4ijGtUtQPoIyduOs',
descripcion: 'Libro que trata sobre temas de memorias2 ',
imagenRuta: 'assets/imagenes/HumbertoBallesteros.jpg',
isbn: '978-958-42-6142-4',
paginas: '200',
precio: '50',
titulo: 'Juego de Memoria'
}
}
My database Firebase
Database Firebase
function getLibros(req, res) {
let todo_libros = []; // array of books
// return Promise
return firebaseDb.ref('libros').once('value')
// libros es una array
.then(libros => {
let autor_promises = [];
for (libro in libros) {
todo_libros.push(libro); // libro
autor_promises.push(firebaseDb.ref('autores').child(libro.autor).once('value')); // author
}
// Array de [autor]
return Promise.all(autor_promises);
})
.then(autores => {
for (let i = 0; i < autores.length; ++i)
todo_libros[i].autor = autores[i];
return null;
})
.then(() => {
gotData(todo_libros);
res.status(200); // send OK response
res.json(todo_libros);
return null;
})
.catch(err => {
errData();
console.log(err); // print error
res.status(503); // Send Error Response
res.send(err);
})
}

Why am I Getting Duplicate Notifications?

I'm trying to use Firebase functions to automatically send push notifications to iOS devices. More specifically, right now when a user creates a post, it will contain an ID, which is actually the user's FCM token, from which the push notification will be sent to that user.
Why does it happen that, upon creating a post, my iOS device doesn't necessarily receive a single push notification, but many? Why is the getResult function being triggered potentially more than once for a given post?
Please see my code below. Thanks!
const functions = require('firebase-functions');
var admin = require('firebase-admin');
var firebase = require('firebase');
admin.initializeApp(functions.config().firebase);
var config = {
apiKey: "XXXXX",
authDomain: "YYYYY",
databaseURL: "ZZZZZ"
}
firebase.initializeApp(config);
firebase.database().ref('posts').on('child_added', function(snapshot1) {
snapshot1.forEach((child) => {
if (child.key == 'id') {
var token = child.val();
admin.database().ref('users').on('value', function(snapshot2) {
snapshot2.forEach(function(user) {
if (user.val().fcmToken == token) {
var newBadgeCount = user.val().badge + 1;
const payload = {
notification: {
title: 'Hello, World!',
body: 'Test Message!',
badge: '' + newBadgeCount,
sound: 'default'
}
};
function getResult(token) {
return result = admin.database().ref('fcmToken/' + token).once('value').then(allToken => {
if (allToken.val()) {
const token = Object.keys(allToken.val());
return admin.messaging().sendToDevice(token, payload).then(function (response) {
console.log("Successfully sent message: ", response.results[0].error);
}).catch(function (error) {
console.log("Error sending message: ", error);
});
};
});
}
function updateBadgeCount(badgeCount, userID) {
firebase.database().ref('users/' + userID + '/badge').set(badgeCount);
}
Promise.all([getResult(token)]).then(function(snapshots) {
updateBadgeCount(newBadgeCount, user.key);
});
};
});
}, function (errorObject) {
console.log("The read failed: " + errorObject.code);
});
};
});
});
Firebase triggers for each record of all records with 'child_added' option once and will re-trigger after each new child added. So, If you like to trigger a function on new write options, you need use other ways.
exports.sendNotifycation = functions.database.ref('/posts/{postId}')
.onWrite(event => {
Your new re-designed codes. (in case I will complete remain)
})

Resources