I have tried following Firebase's documentation and other SO posts to access a parameter value for a cloud function I've successfully deployed.
Unfortunately I've still been receiving a
Type Error: cannot read property 'id' of undefined
I've logged event.params and it is outputting as undefined, so I understand the issue, but am unsure how, syntactically, I'm supposed to derive the param value.
Below is my js code for reference:
exports.observeCreate = functions.firestore.document('/pathOne/{id}/pathTwo/{anotherId}').onCreate(event => {
console.log(event.params);
//event prints out data but params undefined...
const data = event.data()
var id = event.params.id;
return admin.firestore().collection('path').doc(id).get().then(doc => {
const data = doc.data();
var fcmToken = data.fcmToken;
var message = {
notification: {
title: "x",
body: "x"
},
token: fcmToken
};
admin.messaging().send(message)
.then((response) => {
console.log('Successfully sent message:', response);
return;
})
.catch((error) => {
console.log('Error sending message:', error);
return;
});
return;
})
})
You're using the pre-1.0 API for the firebase-functions module, but the acutal version of it you have installed is 1.0 or later. The API changed in 1.0. Read about the changes here.
Firestore (and other types of) triggers now take a second parameter of type EventContext. This has a property called params that contains the data that used to be in event.params.
exports.observeCreate = functions.firestore.document('/pathOne/{id}/pathTwo/{anotherId}').onCreate((snapshot, context) => {
console.log(context.params);
console.log(context.params.id);
});
Please also read the documentation for the most up-to-date information about Firestore triggers.
Related
I'm writing a cloud function for my iOS app to watch for any follower changes in my users to notify them when someone follows them. My follower subcollection is within each user data document and I use wildcards to listen to any changes. I've also provided good logging during each step, so it's easy to see where the problem is, however, since I'm rather new to cloud functions, I don't know exactly how I'd fix it.
The cloud function is as follows.
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
let title
let body
let payload
let FCMRegistrationToken_KEY
exports.sendNotificationOnFollowerCreate = functions.firestore
.document('Users/{userID}/Followers/{followerID}')
.onCreate((snapshot, context) => {
if(snapshot.after){
// Get the userId and followerId
const userID = context.params.userID;
const followerID = context.params.followerID;
// Get the data of the follower document
const newData = snapshot.after.data()
const fullName = newData.firstName + " " + newData.lastName
title = 'Someone just followed you'
body = fullName + ' Just followed you right now!\n' + 'username: ' + newData.userName
// Create the notification payload
payload = {
notification: {
title: title,
body: body
}
}
// Get FMC token by fetching the FCMToken Document for the userID above.
admin.firestore().collection('FCMTokens').doc(userID).get().then(doc => {
if(!doc.exists) {
console.log('User not found!');
} else {
// Get the data of the document
const data = doc.data();
console.log(data);
FCMRegistrationToken_KEY = data.token
}
})
.catch(error => {
console.log(error);
})
.finally(() => {
//more code here
// Send the notification
admin.messaging().sendToDevice(FCMRegistrationToken_KEY, payload)
.then(response => {
console.log('Notification sent successfully:', response);
})
.catch(error => {
console.log('Error sending notification:', error);
});
});
}
})
Basically when there's a new follower added, I use the userID from the context parameters to fetch the FCM token I have saved for all my users in a FCMTokens collection. After retrieving the token and creating my payload, I invoke a sendToDevice() call through admin.messaging() but it fails for some reason.
However, it fails right after that giving the following error
{
"textPayload": "Function returned undefined, expected Promise or value",
"insertId": "63c38ba0000e2c35c9c62c1d",
"resource": {
"type": "cloud_function",
"labels": {
"function_name": "sendNotificationOnFollowerCreate",
"region": "us-central1",
"project_id": "fir-eris"
}
},
"timestamp": "2023-01-15T05:14:08.928821Z",
"severity": "WARNING",
"labels": {
"execution_id": "no23uq1mg5a3",
"instance_id": "00c61b117c173e48fc2cb6c3b49f2c059090e49b7252db1b187115bd42a62998c4093f283fe06ba4ec0bf7981f108fcadb527843a8c4b3c77ec1"
},
"logName": "projects/fir-eris/logs/cloudfunctions.googleapis.com%2Fcloud-functions",
"trace": "projects/fir-eris/traces/e0d7dfae3ea1340e1ec101d16defc94b",
"receiveTimestamp": "2023-01-15T05:14:09.204309551Z"
}
I'm thoroughly confused as I really don't have that much experience with cloud functions. Can someone guide me through what's happening and what could be a potential fix for this?
Thank you.
The error that you have mentioned in the question is basically seen when a function does not or has incorrectly a return statement. The code you have for cloud function does not seem to have any return statement which will have a promise return.To make sure Cloud functions knows when your code is done, you need to either return a value from the top-level function (in the case that all work happens synchronously), or return a promise from the top-level function (in the case that some work continues after the closing } of the function).
The sendNotificationOnFollowerCreate might be aborted when the trigger function finishes because it isn't waiting for that promise.
Try adding the return similar to example below:
return DeviceToken.then(result => { const token_id = result.val();
console.log(token_id); const payload = { notification:
{ title: "New Job Request", body: `JobID ` + job_id, tag: collapseKey, } };
return admin.messaging().sendToDevice(token_id, payload)
Also check these following examples with similar implementations:
Each then should return a value firebase cloud function
Send Push notification using Cloud function for firebase
Firebase Cloud function says unreachable
Firebase Cloud push notification not being sent to device
Is there a way to send notification by identifying user rather than
device
I'm trying to implement a messaging application using Firebase Firestore and Firebase Cloud Functions.
In essence, chat messages are stored as individual documents in a subcollection. At first, I implemented this as directly adding a document from the client and listening on the collection and updating the clients when a change happens but later I decided to switch to using Cloud functions so that I can add some functionality that's better done on the server side(filtering etc.).
So I created a function for sending messages, which creates the documents on behalf of the users when the users call the function from the app(i.e. tap the send button).
The function worked and I was able to monitor the processes through the logs. Unfortunately, the functions began to die out without error, the console was reporting that the functions are executed successfully and it usually took less than a second to execute.
I suspect that it has something to do with the promises that probably continue to run but this is the same code that was working but failing today.
If I try a few more times, the functions seem to be working again. Do I need to keep the functions "warm"? Are cloud functions not reliable enough to handle this kind of tasks? When I say my user that a message is sent, I need to be able to confirm that it is sent and communicate it with the users if it failed.
It's hard to debug the issue because no errors are thrown(not even info message, it's just as if didn't happen), it just says that the function successfully finished execution and nothing happened.
Am I missing something here? Thank you.
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
return;
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent")
}).catch(err => {
console.log("Error sending mesage:", err)
})
})
As explained in the documentation of the HTTP Callable Cloud Functions:
To return data after an asynchronous operation, return a promise.
Then follows an example:
const sanitizedMessage = sanitizer.sanitizeText(text); // Sanitize the message.
return admin.database().ref('/messages').push({
text: sanitizedMessage,
author: { uid, name, picture, email },
}).then(() => {
console.log('New Message written');
// Returning the sanitized message to the client.
return { text: sanitizedMessage };
})
So you need to adapt your code as follows:
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
//Here send back an error as explained here: https://firebase.google.com/docs/functions/callable#handle_errors
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
//Note the return on next line
return admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent");
return { text: "Message sent" };
}).catch(err => {
console.log("Error sending mesage:", err);
//Here, again, send back an error as explained here: https://firebase.google.com/docs/functions/callable#handle_errors
})
})
If you don't want to return a value to the client, you could do as follows, returning null when the Promise returned by the add() asynchronous method resolves. (Not tested but it should work).
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
return null;
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
//Note the return on next line
return admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent"); //Actually, if you don't need this console.log() you can remove this entire then() block, returning the promise from add() is enough
return null;
}).catch(err => {
console.log("Error sending mesage:", err);
return null;
})
})
EDIT**
Ok so I was able to get the parameters working thanks to first answer provided but now I have an issue whereby my function is creating a new user entirely in Firebase and not update an existing one, the uid that i am passing into the auth.admin.updateUser is teh uid of the existing user who's email i want to update. Here is the updated cloud function which is adding a new user rather than updating the existing:
exports.updateEmail = functions.https.onCall((data, context) => {
const email = data.email;
const uid = data.uid;
admin.auth().updateUser(uid, {
email: email
})
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully updated user", userRecord.toJSON());
return response.status(200).json(userRecord.toJSON());
})
.catch(function(error) {
console.log("Error updating user:", error);
return response.status(404).json({
error: 'Something went wrong.'
});
});
});
I got the function from the firebase docs but it isn't doing what I intended it to do.
ORIGINAL POST**
I'm having some difficulty getting a cloud function to work when calling the function from within my flutter code. The issue that I am having is that the uid and email fields are undefined even though I am passing them through to the cloud function using busboy fields.
I'm trying to pass the email and uid field though to the function as follows:
final request = http.MultipartRequest('POST', Uri.parse('****************my function url************'));
request.fields['email'] = Uri.encodeComponent(newEmail);
request.fields['uid'] = Uri.encodeComponent(selectedUser.uid);
request.headers['Authorization'] = 'Bearer ${_authenticatedUser.token}';
final http.StreamedResponse streamedResponse = await request.send();
And on the Node.js side I am trying to use these fields using busboy, here is my cloud function in Node.js:
exports.changeEmail = functions.https.onRequest((request, response) => {
if (!request.headers.authorization ||
!request.headers.authorization.startsWith('Bearer ')
) {
return response.status(401).json({
error: 'Unauthorized.'
});
}
let idToken;
idToken = request.headers.authorization.split('Bearer ')[1];
let email;
let uid;
const busboy = new Busboy({
headers: request.headers
});
busboy.on('field', (fieldname, value) => {
if (fieldname == 'email') {
email = decodeURIComponent(value);
}
if (fieldname == 'uid') {
uid = decodeURIComponent(value);
}
});
admin.auth().updateUser(uid, {
email: email
})
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully updated user", userRecord.toJSON());
return response.status(200).json(userRecord.toJSON());
})
.catch(function(error) {
console.log("Error updating user:", error);
return response.status(404).json({
error: 'Something went wrong.'
});
});
});
Even though I am passing the fields in with busboy fields they are not getting set in the function, is there something I am doing wrong here?
Why don't you use a callable function? It will automatically receive the authentication data.
The documentation even has examples on how to get the uid and email:
Declare the function:
exports.addMessage = functions.https.onCall((data, context) => {
// ...
});
Get the user properties from the context parameter:
// Message text passed from the client.
const text = data.text;
// Authentication / user information is automatically added to the request.
const uid = context.auth.uid;
const name = context.auth.token.name || null;
const picture = context.auth.token.picture || null;
const email = context.auth.token.email || null;
Call the function from your Flutter code:
Install cloud_functions package and then:
import 'package:cloud_functions/cloud_functions.dart';
await CloudFunctions.instance.call(functionName: 'addMessage');
If the user is authenticated before calling the function that's all you need to do.
You can also pass additional parameters to the function:
await CloudFunctions.instance.call(functionName: 'addMessage', parameters: {"email": "whatever#example.com"});
Any parameters will be passed to the data parameter on the function side.
In a small webshop that I am trying to setup, I need to update the opening hours in the background with firebase functions and google place details when a user creates a shoppingcart.
I can succesfully sent a GET request with POSTMAN to retrieve the opening hours of a shop using the following instructions:
https://developers.google.com/places/web-service/details
But I cannot access the response from the GET request as I usually do with JSON responses.
I tried also:response.result.opening_hours.json()
Can someone tell me what I am doing wrong?
export const mapGooglePlaces = functions.database
.ref('/shopping-carts/{shoppingCartId}/shippingManner')
.onWrite(event => {
const shippingManner = event.data.val();
const optionsAPI = {
method: 'GET',
uri: 'https://maps.googleapis.com/maps/api/place/details/json?placeid=ChIJN1t_tDeuEmsRUsoyG83frY4&key=YOUR_API_KEY',
};
return request(optionsAPI)
.then(response => {
const openingHours = response.result.opening_hours;
console.log(openingHours);
return;
})
.catch(function (err) {
console.log(err);
});
});
The response is not a JSON object. It is JSON formatted text and must be parsed to create an object. Modify the code as follows:
return request(optionsAPI)
.then(response => {
const responseObject = JSON.parse(response);
const openingHours = responseObject.result.opening_hours;
console.log(openingHours);
return;
})
.catch(function (err) {
console.log(err);
});
Also, before using the opening_hours or any other property of result, you should test responseObject.status === 'OK' to confirm that a place was found and at least one result was returned.
Hello my firebase cloud function gets called multiple times when I don't put in check for previous.exists().
I get multiple push notifications.
if (!event.data.exists()){
return;
}
if (event.data.previous.exists()){
return;
}
But when I check for it i don't get push notification.
Here is the not working code:
What should I change?
exports.sendShoppingListInvitationNotification = functions.database.ref('/invites/{id}/').onWrite(event => {
//get the snapshot of the written data
const snapshot = event.data;
if (!event.data.exists()){
return;
}
if (event.data.previous.exists()){
return;
}
//get snapshot values
console.log(snapshot.key);
const receiptToken = snapshot.child('receiptFcmToken').val();
const senderName = snapshot.child('senderNickname').val();
const inviteMessage = snapshot.child('inviteMessage').val();
const senderImage = snapshot.child('senderProfileImageURL').val();
//create Notification
const payload = {
notification: {
title: `Invitation from ${senderName}`,
body: `${inviteMessage}`,
icon: `${senderImage}`,
badge: '1',
sound: 'default',
}
};
//send a notification to firends token
return admin.messaging().sendToDevice(receiptToken, payload).then(response => {
console.log("Successfully sent message:", response);
}).catch((err) => {
console.log(err);
});
});
I don't get error message on cloud console.
This is the firebase structure:
Seems like it shouldn’t be called multiple times unless you’re doing multiple writes to that location. Try using .onCreate instead of .onWriteif you only want to send a notification on the first write to the path. Then you won’t need that check for previous data. See the documentation here which outlines the different database triggers.