How to send firebase notifications using nodejs? - node.js

I want to implement push notifications automatically and I've used javascript (node.js) but I got this error
Function returned undefined, expected Promise or value
I am not node js developer I am a flutter developer and I don't know what is promises.
this is my code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var notificationMessageData;
exports.fcmTester = functions.firestore.document('posts/{postID}').onCreate((snapshot , context) => {
notificationMessageData = snapshot.data();
admin.firestore().collection('pushTokens').get().then(async (snapshot) => {
var tokens = [];
if (snapshot.empty) {
console.log('No Devices');
} else {
for (var token of snapshot.docs) {
tokens.push(token.data().tokenID);
}
var payload = {
"notification": {
"title": "from" + notificationMessageData.writer,
"body": "from" + notificationMessageData.name,
"sound": "default"
},
"data": {
"sendername": notificationMessageData.writer,
"message": notificationMessageData.name
}
}
return await admin.messaging().sendToDevice(tokens , payload).then((response) => {
console.log('nice');
}).catch((err) => {
console.log(err);
})
}
})
})
Everything is going okay and I upload it without any problem but when adding a document to the posts collection it outputs in the logs the above error.
I have created a user registration form and I've registered the users and put their token id in a collection called pushTokens and then sending a notification for each user inside that collection but this didn't work.

There are two problems in your code:
You don't return the promises returned by the asynchronous Firebase methods (get() and sendToDevice());
You mix-up the use of async/await with the then() method.
I would suggest you watch the 3 official videos about "JavaScript Promises" from the Firebase video series, and then that you first try with using the then() method to correctly chain your Promises and return the chain.
The following code should work.
exports.fcmTester = functions.firestore.document('posts/{postID}').onCreate((snapshot, context) => {
const notificationMessageData = snapshot.data();
return admin.firestore().collection('pushTokens').get()
.then(snapshot => {
var tokens = [];
if (snapshot.empty) {
console.log('No Devices');
throw new Error('No Devices');
} else {
for (var token of snapshot.docs) {
tokens.push(token.data().tokenID);
}
var payload = {
"notification": {
"title": "from" + notificationMessageData.writer,
"body": "from" + notificationMessageData.name,
"sound": "default"
},
"data": {
"sendername": notificationMessageData.writer,
"message": notificationMessageData.name
}
}
return admin.messaging().sendToDevice(tokens, payload)
}
})
.catch((err) => {
console.log(err);
return null;
})
});
Then, after having experienced the "management" of asynchronous methods with then() (and catch()), you may give a try with async/await: again, this official video from Doug Stevenson will be of great help.

It says:
Function returned undefined, expected Promise or value
Have you tried returning null, or true or false, etc. from your functions?
A promise is something you pass around your program while you wait for it to become a real value.
It seems like the error message is focused on wanting you to return something from your function. Without knowing more, this is the best advice I can give.

Related

How to get inner collection in firebase firestore

I'm trying to get the device token of a particular user in firestore which is stored in tokens collection inside either "clients" or "lawyers" collection.
When i remove the second .collection("tokens") from the chain i get the user object back but with the token collection in the chain i just can't seem to get any user (client or lawyer) back, even though the user and it's token exist. what am i doing wrong
exports.onReceiveChatMessage = functions.database
.ref("/messages/{uid}")
.onCreate(async (snapshot, context) => {
const newMessage = snapshot.val();
console.log("NEW_MESSAGE", newMessage);
const senderName = newMessage.sender_name;
const messageContent = newMessage.content;
console.log("SENDER'S_NAME", senderName);
console.log("MESSAGE_BODY", messageContent);
const uid = context.params.uid;
console.log("RECEIVERS_ID", uid);
if (newMessage.sender_id == uid) {
//if sender is receiver, don't send notification
console.log("sender is receiver, dont send notification...");
return;
} else if (newMessage.type === "text") {
console.log(
"LETS LOOK FOR THIS USER, STARTING WITH CLIENTS COLLECTION..."
);
let userDeviceToken;
await firestore
.collection("clients")
.doc(uid)
.collection("tokens")
.get()
.then(async (snapshot) => {
if (!snapshot.exists) {
console.log(
"USER NOT FOUND IN CLIENTS COLLECTION, LETS CHECK LAWYERS..."
);
await firestore
.collection("lawyers")
.doc(uid)
.collection("tokens")
.get()
.then((snapshot) => {
if (!snapshot.exists) {
console.log(
"SORRY!!!, USER NOT FOUND IN LAWYERS COLLECTION EITHER"
);
return;
} else {
snapshot.forEach((doc) => {
console.log("LAWYER_USER_TOKEN=>", doc.data());
userDeviceToken = doc.data().token;
});
}
});
} else {
snapshot.forEach((doc) => {
console.log("CLIENT_USER_TOKEN=>", doc.data());
userDeviceToken = doc.data().token;
});
}
});
// console.log("CLIENT_DEVICE_TOKEN", userDeviceToken);
} else if (newMessage.type === "video_session") {
}
})
This line
if (!snapshot.exists) {
should be:
if (snapshot.empty) {
because you're calling get() on a CollectionReference (which returns a QuerySnapshot), not on a DocumentReference (which returns a DocumentSnapshot).
If you remove the .collection('tokens') from the chain in your example, it does work because a DocumentSnapshot does have the member exists, but a CollectionReference doesn't.
Take a look at their members here:
https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#get
Then:
https://googleapis.dev/nodejs/firestore/latest/QuerySnapshot.html
As a suggestion, I used to confuse snapshots and got that problem because of working with Javascript instead of Typescript. So I got used to calling the result snap when called on a document, and snaps when called on collections. That reminds me of what kind of response I'm working on. Like this:
// single document, returns a DocumentSnapshot
const snap = await db.collection('xyz').doc('123').get();
if (snap.exists) {
snap.data()...
}
// multiple documents, returns a QuerySnapshot
const snaps = await db.collection('xyz').get();
if (!snaps.empty) { // 'if' actually not needed if iterating over docs
snaps.forEach(...);
// or, if you need to await, you can't use the .forEach loop, use a plain for:
for (const snap of snaps.docs) {
await whatever(snap);
}
}

How can I am make sure these chain of functions in Node.js are performed in order (using promises)?

I have a set of functions in Node.js that I would like to load in a certain order. I will provide some mockup code abstracted and simplified:
function updateMyApp() {
loadDataToServer()
.then(() => useData())
.then(() => saveData())
.then(() => { console.log("updateMyApp done") })
}
function loadDataToServer() {
return new Promise( (resolve, reject) {
...preparing data and save file to cloud...
resolve()})
}
function handleDataItem(item) {
// Function that fetches data item from database and updates each data item
console.log("Name", item.name)
}
function saveData() {
// Saves the altered data to some place
}
useData is a bit more complex. In it I would like to, in order:
console.log('Starting alterData()')
Load data, as json, from the cloud data source
Iterate through every item in the json file and do handleDataItem(item) on it.
When #2 is done -> console.log('alterData() done')
Return a resolved promise back to updateMyApp
Go on with saveData() with all data altered.
I want the logs to show:
Starting useData()
Name: Adam
Name: Ben
Name: Casey
useData() done
my take on this is the following:
function useData() {
console.log('Starting useData()')
return new Promise( function(resolve, reject) {
readFromCloudFileserver()
.then(jsonListFromCloud) => {
jsonListFromCloud.forEach((item) => {
handleDataItem(item)
}
})
.then(() => {
resolve() // I put resolve here because it is not until everything is finished above that this function is finished
console.log('useData() done')
}).catch((error) => { console.error(error.message) })
})
}
which seems to work but, as far as I understand this is not how one is supposed to do it. Also, this seems to do the handleDataItem outside of this chain so the logs look like this:
Starting useData()
useData() done
Name: Adam
Name: Ben
Name: Casey
In other words. It doesn't seem like the handleDataItem() calls are finished when the chain has moved on to the next step (.then()). In other words, I can not be sure all items have been updated when it goes on to the saveData() function?
If this is not a good way to handle it, then how should these functions be written? How do I chain the functions properly to make sure everything is done in the right order (as well as making the log events appear in order)?
Edit: As per request, this is handleDataItem less abstracted.
function handleDataItem(data) {
return new Promise( async function (resolve) {
data['member'] = true
if (data['twitter']) {
const cleanedUsername = twitterApi.cleanUsername(data['twitter']).toLowerCase()
if (!data['twitter_numeric']) {
var twitterId = await twitterApi.getTwitterIdFromUsername(cleanedUsername)
if (twitterId) {
data['twitter_numeric'] = twitterId
}
}
if (data['twitter_numeric']) {
if (data['twitter_protected'] != undefined) {
var twitterInfo = await twitterApi.getTwitterGeneralInfoToDb(data['twitter_numeric'])
data['twitter_description'] = twitterInfo.description
data['twitter_protected'] = twitterInfo.protected
data['twitter_profile_pic'] = twitterInfo.profile_image_url.replace("_normal", '_bigger')
data['twitter_status'] = 2
console.log("Tweeter: ", data)
}
} else {
data['twitter_status'] = 1
}
}
resolve(data)
}).then( (data) => {
db.collection('people').doc(data.marker).set(data)
db.collection('people').doc(data.marker).collection('positions').doc(data['report_at']).set(
{
"lat":data['lat'],
"lon":data['lon'],
}
)
}).catch( (error) => { console.log(error) })
}
The twitterAPI functions called:
cleanUsername: function (givenUsername) {
return givenUsername.split('/').pop().replace('#', '').replace('#', '').split(" ").join("").split("?")[0].trim().toLowerCase()
},
getTwitterGeneralInfoToDb: async function (twitter_id) {
var endpointURL = "https://api.twitter.com/2/users/" + twitter_id
var params = {
"user.fields": "name,description,profile_image_url,protected"
}
// this is the HTTP header that adds bearer token authentication
return new Promise( (resolve,reject) => {
needle('get', endpointURL, params, {
headers: {
"User-Agent": "v2UserLookupJS",
"authorization": `Bearer ${TWITTER_TOKEN}`
}
}).then( (res) => {
console.log("result.body", res.body);
if (res.body['errors']) {
if (res.body['errors'][0]['title'] == undefined) {
reject("Twitter API returns undefined error for :'", cleanUsername, "'")
} else {
reject("Twitter API returns error:", res.body['errors'][0]['title'], res.body['errors'][0]['detail'])
}
} else {
resolve(res.body.data)
}
}).catch( (error) => { console.error(error.message) })
})
},
// Get unique id from Twitter user
// Twitter API
getTwitterIdFromUsername: async function (cleanUsername) {
const endpointURL = "https://api.twitter.com/2/users/by?usernames="
const params = {
usernames: cleanUsername, // Edit usernames to look up
}
// this is the HTTP header that adds bearer token authentication
const res = await needle('get', endpointURL, params, {
headers: {
"User-Agent": "v2UserLookupJS",
"authorization": `Bearer ${TWITTER_TOKEN}`
}
})
if (res.body['errors']) {
if (res.body['errors'][0]) {
if (res.body['errors'][0]['title'] == undefined) {
console.error("Twitter API returns undefined error for :'", cleanUsername, "'")
} else {
console.error("Twitter API returns error:", res.body['errors'][0]['title'], res.body['errors'][0]['detail'])
}
} else {
console.error("Twitter API special error:", res.body)
}
} else {
if (res.body['data']) {
return res.body['data'][0].id
} else {
//console.log("??? Could not return ID, despite no error. See: ", res.body)
}
}
},
You have 3 options to deal with your main issue of async methods in a loop.
Instead of forEach, use map and return promises. Then use Promise.all on the returned promises to wait for them to all complete.
Use a for/of loop in combination with async/await.
Use a for await loop.
It sounds like there's a problem in the implementation of handleDataItem() and the promise that it returns. To help you with that, we need to see the code for that function.
You also need to clean up useData() so that it properly returns a promise that propagates both completion and errors.
And, if handleDataItem() returns a promise that is accurate, then you need to change how you do that in a loop here also.
Change from this:
function useData() {
console.log('Starting useData()')
return new Promise( function(resolve, reject) {
readFromCloudFileserver()
.then(jsonListFromCloud) => {
jsonListFromCloud.forEach((item) => {
handleDataItem(item)
}
})
.then(() => {
resolve() // I put resolve here because it is not until everything is finished above that this function is finished
console.log('useData() done')
}).catch((error) => { console.error(error.message) })
})
}
to this:
async function useData() {
try {
console.log('Starting useData()')
const jsonListFromCloud = await readFromCloudFileserver();
for (let item of jsonListFromCloud) {
await handleDataItem(item);
}
console.log('useData() done');
} catch (error) {
// log error and rethrow so caller gets the error
console.error(error.message)
throw error;
}
}
The structural changes here are:
Switch to use async/await to more easily handle the asynchronous items in a loop
Remove the promise anti-pattern that wraps new Promise() around an existing promise - no need for that AND you weren't capturing or propagating rejections from readFromCloudFileServer() which is a common mistake when using that anti-pattern.
rethrow the error inside your catch after logging the error so the error gets propagated back to the caller

retrieve Firestore document from onCreate trigger with Cloud Functions

I need to retrieve information from a Firestore Document when another document is created. When I try to do this I get hit with an error about the function not being async. It has been so long since I used javascript I am basically a novice again and have no idea how to fix this.
ok, so I am using Firebase Cloud Functions and the function in question is a Firestore .onCreate() trigger.
When the function is triggered I set a sender variable (which is the document ID from a different collection that I need to retrieve)
then I try to get the document as per the documentation.
The function ends up like this:
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate((snap, context) => {
// when friend request is created
data = doc.data()//get request data
sender = data["sender"]//get request sender from data
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
});
when I run this in the emulator I get this error:
const doc = await requestRef.get();//get user data of sender
^^^^^
SyntaxError: await is only valid in async functions and the top level bodies of modules
I have absolutely no idea where to go from here.
Can anyone help me with this?
Thanks
The await keyword is valid only in an async function.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate(async (snap, context) => {
// ^^^^^
})
If you are (or need to) use synchronous function then you would have to use promise chaining.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate((snap, context) => {
return requestRef.get().then((snapshot) => {
if (snapshot.exists) { ... }
})
})
Apart from that, the order of variables/statements looks incorrect. With the current code (as in original question), you may end up getting an error: "Cannot access 'doc' before initialization" Try refactoring it like this:
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate(async (snap, context) => {
// accessing data from newly created doc
const newDocData = snap.data()
// const sender = "" // ??
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
})
Where is the sender coming from? I've just commented it above but if the sender is present in new document then you can access it by: const sender = newDocData.sender
If your using await you have to specify that function is asynchronous. Otherwise it will throw error.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}').onCreate(async (snap, context) => {
// when friend request is created
data = doc.data()//get request data
sender = data["sender"]//get request sender from data
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
});
Yet some of your references is unknown to us. Maybe this code is not completed.
The main point is you need to understand when you can access async/await or Promise
All await methods must be inside an async block or be handled in an async manor using .then() promises
in this case, the parent function is on this line .onCreate((snap, context) => {
simply inserting an async at the start of the variables will upgrade the arrow function to an async arrow function
.onCreate(async (snap, context) => {

Why does Async firebase fetching is not working? (NODE JS)

Building a NodeJS REST API.
Trying to send load data from FireBase collection, then sending it to the user (as API response).
Looks like the problem is that it's not waits for the firebase fetch to resolve, but send back a response without the collection data. (tried to use ASYNC-AWAIT but its not working)
exports.getChatMessages = async (req, res, next) => {
const chatId = req.params.chatId
const getChatData = () => {
db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot((snapshot) => {
snapshot.docs.forEach(msg => {
console.log(msg.data().messageContent)
return {
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
}
})
})
}
try {
const chatData = await getChatData()
console.log(chatData)
res.status(200).json({
message: 'Chat Has Found',
chatData: chatData
})
} catch (err) {
if (!err.statusCode) {
err.statusCode(500)
}
next(err)
}
}
As you can see, I've used 2 console.logs to realize what the problem, Terminal logs looks like:
[] (from console.logs(chatData))
All messages (from console.log(msg.data().messageContent))
Is there any way to block the code unti the firebase data realy fetched?
If I correctly understand, you want to send back an array of all the documents present in the messages subcollection. The following should do the trick.
exports.getChatMessages = async (req, res, next) => {
const chatId = req.params.chatId;
const collectionRef = db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc');
try {
const chatsQuerySnapshot = await collectionRef.get();
const chatData = [];
chatsQuerySnapshot.forEach((msg) => {
console.log(msg.data().messageContent);
chatData.push({
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
});
});
console.log(chatData);
res.status(200).json({
message: 'Chat Has Found',
chatData: chatData,
});
} catch (err) {
if (!err.statusCode) {
err.statusCode(500);
}
next(err);
}
};
The asynchronous get() method returns a QuerySnapshot on which you can call forEach() for enumerating all of the documents in the QuerySnapshot.
You can only await a Promise. Currently, getChatData() does not return a Promise, so awaiting it is pointless. You are trying to await a fixed value, so it resolves immediately and jumps to the next line. console.log(chatData) happens. Then, later, your (snapshot) => callback happens, but too late.
const getChatData = () => new Promise(resolve => { // Return a Promise, so it can be awaited
db.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot(resolve) // Equivalent to .onSnapshot((snapshot) => resolve(snapshot))
})
const snapshot = await getChatData();
console.log(snapshot)
// Put your transform logic out of the function that calls the DB. A function should only do one thing if possible : call or transform, not both.
const chatData = snapshot.map(msg => ({
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
}));
res.status(200).json({
message: 'Chat Has Found',
chatData
})
Right now, getChatData is this (short version):
const getChatData = () => {
db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot((snapshot) => {}) // some things inside
}
What that means is that the getChatData function calls some db query, and then returns void (nothing). I bet you'd want to return the db call (hopefully it's a Promise), so that your await does some work for you. Something along the lines of:
const getChatData = async () =>
db
.collection('chats')
// ...
Which is the same as const getChatData = async() => { return db... }
Update: Now that I've reviewed the docs once again, I see that you use onSnapshot, which is meant for updates and can fire multiple times. The first call actually makes a request, but then continues to listen on those updates. Since that seems like a regular request-response, and you want it to happen only once - use .get() docs instead of .onSnapshot(). Otherwise those listeners would stay there and cause troubles. .get() returns a Promise, so the sample fix that I've mentioned above would work perfectly and you don't need to change other pieces of the code.

Only first document is returned as response instead of multiple documents using Firestore and Cloud Functions

I have this Express function:
exports.getSliderTipsteriData = (req, res) => {
let sliderTipsteriData = [];
db.collection("tipsterBanner")
.orderBy("createdAt", "desc")
.where("show", "==", true)
.get()
.then((data) => {
data.forEach((doc) => {
let eventId = doc.data().eventId;
sliderTipsteriData = doc.data();
db.collection("evenimenteTipsteri")
.orderBy("createdAt", "desc")
.get()
.then((data) => {
sliderTipsteriData.tipsteri = [];
data.forEach((doc) => {
if(doc.data().bilet[0].id === sliderTipsteriData.eventId) {
sliderTipsteriData.tipsteri.push({
tipster: doc.data().tipster,
homeTeam: doc.data().bilet[0].homeTeam,
awayTeam: doc.data().bilet[0].awayTeam
})
} else null
})
return res.json(sliderTipsteriData);
})
})
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: err.code });
});
};
and received this as response:
{
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/socialape-bea5b.appspot.com/o/slider1.jpg?alt=media&token=0824a93d-4bc3-49fa-9ae8-4408961a0736",
"event_date": 1614110400,
"awayTeamName": "Bayer Leverkusen",
"awayTeamPercent": 23,
"homeTeamName": "Atletico Madrid",
"homeTeamShortName": "ATL",
"awayTeamEmblem": "https://media.api-sports.io/football/teams/34.png",
"createdAt": "2021-03-22T18:25:03.667Z",
"homeTeamEmblem": "https://media.api-sports.io/football/teams/49.png",
"awayTeamShortName": "LEV",
"homeTeamPercent": "77",
"show": true,
"eventId": 652238,
"homeTeamColor": "#0099ff",
"awayTeamColor": "#ff0000",
"etapa": "Liga Campionilor, Etapa 2",
"tipsteri": [
{
"tipster": "daniel",
"homeTeam": "Lazio",
"awayTeam": "Bayern Munich"
},
{
"tipster": "user",
"homeTeam": "Lazio",
"awayTeam": "Bayern Munich"
}
]
}
The problem is that I have more than one document in tipsterBanner collection, but I receive only the first one. So the forEach doc might not be working properly.
Any idea what I miss here?
I expect to receive as response the sliderTipsteriData array with multiple objects, not only the first one. Looks like the forEach actually doesn't loop.
This is because you have promises trying to run in loops that don't wait for them. Inside your then blocks, you perform more asynchronous calls (collection().get()) but your code isn't waiting for them to resolve, so it's just flying through your forEach loop, creating those promises, but then getting to the end and returning.
There are two ways to solve that problem - (1) put your then/catch calls into a Promise.all and wait for that to resolve or (2) switch to async/await. But I think you have an even better solution. Right now, you're querying your evenimenteTipsteri collection in each loop, but you're not using any information from the tipsterBanner collection as parameters in the evenimenteTipsteri query, so you could just query both of them one time and then handle all the filtering/organizing in code. This will help speed up your results and help protect you from the costs of unnecessary reads on Firestore.
This code is untested because I just copy/pasted your code and rewrote it without being able to run it, but this is the main idea:
exports.getSliderTipsteriData = (req, res) => {
let sliderTipsteriData = [];
// These create the promises which will resolve in our Promise.all()
const tipsterBanner = db.collection("tipsterBanner").orderBy("createdAt", "desc").where("show", "==", true).get()
const evenimenteTipsteri = db.collection("evenimenteTipsteri").orderBy("createdAt", "desc").get()
Promise.all([tipsterBanner, evenimenteTipsteri]).then((results) => {
// results is now an array of your original "data" items from your then blocks
// results[0] is the data from the tipsterBanner query
// results[1] is the data from the evenimenteTipsteri query
const tipsterBannerResults = results[0] // Just to make it more readable
const evenimenteTipsteriResults = results[1] // Just to make it more readable
tipsterBannerResults.forEach(doc => {
let eventId = doc.data().eventId
sliderTipsteriData = doc.data() // Is this right? You could end up overwriting your data
sliderTipsteriData.tipsteri = []
evenimenteTipsteriResults.forEach(doc => {
if(doc.data().bilet[0].id === sliderTipsteriData.eventId) {
sliderTipsteriData.tipsteri.push({
tipster: doc.data().tipster,
homeTeam: doc.data().bilet[0].homeTeam,
awayTeam: doc.data().bilet[0].awayTeam
})
}
})
})
return res.json(sliderTipsteriData)
}).catch(error => {
// Handle errors from your queries
})
}

Resources