Realtime Database onWrite triggers in Cloud Functions has null val() data - javascript

I am trying to implement push notifications trigger using cloud functions in firebase but each time I try the val function returns null. It is not recognizing the pushID, I implemented database from android using push() method.
This is my database structure
And this is my code for push notifications whenever a Post is created.
//import firebase functions modules
const functions = require('firebase-functions');
//import admin module
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// Listens for new messages added to messages/:pushId
exports.pushNotification = functions.database.ref('/Posts/Posts/{pushId}').onWrite( event => {
console.log('Push notification event triggered');
// Grab the current value of what was written to the Realtime Database.
var valueObject = event.data.val();
// if(valueObject.photoUrl != null) {
// valueObject.photoUrl= "Sent you a photo!";
// }
// Create a notification
const payload = {
notification: {
title:valueObject.tittle,
body: valueObject.details,
sound: "default"
},
};
//Create an options object that contains the time to live for the notification and the priority
const options = {
priority: "high",
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToTopic("pushNotifications", payload, options);
});
This is error in console of cloud functions
Edited after using OnCreate:-
exports.pushNotification = functions.database.ref('/Posts/Posts/{pushid}').onCreate((snap, context) => {
const original = snapshot.val();
console.log('Push notification event triggered');
// Grab the current value of what was written to the Realtime Database.
// var valueObject = event.data.val();
var valueObject = snap.val();
// if(valueObject.photoUrl != null) {
// valueObject.photoUrl= "Sent you a photo!";
// }
// Create a notification
const payload = {
notification: {
title:valueObject.tittle,
body: valueObject.details,
sound: "default"
},
};

It looks like you haven't adapted your code to the new Functions 1.0 SDK. The differences are detailed here: https://firebase.google.com/docs/functions/beta-v1-diff
As you can see from that doc in the Realtime Database section, onWrite triggers now give you a Change object with before and after properties that you use to get the value of the database location before or after the update.
Also consider if you actually want an onCreate trigger instead, which is more straightforward to deal with, and only triggers once when data at the matching location is newly created.

Related

How do I Collect User IDs + Retrieve Corresponding Tokens + Send a Push Notification Via Firebase Cloud Function (JS)

The Problem:
I have been unable to use Firebase (Google) Cloud Functions to collect and utilize device tokens for the cloud messaging feature.
Context:
I am a self-taught android-Java developer and have no JavaScript experience. Despite that, I believe I have code that should work and am not sure what the problem is. To my understanding, it could be one of three things:
Somehow my Firebase Realtime Database references are being called incorrectly and I am not retrieving data as expected.
I may need to use Promises to wait for all calls to be made before proceeding, however I don't really understand how I would incorporate that into the code I have.
I may be using multiple return statements incorrectly (which I am also fuzzy on).
My error message on the Firebase Realtime Database console is as follows:
#firebase/database: FIREBASE WARNING: Exception was thrown by user callback. Error: Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array.
at FirebaseMessagingError.FirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:42:28)
at FirebaseMessagingError.PrefixedFirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:88:28)
at new FirebaseMessagingError (/srv/node_modules/firebase-admin/lib/utils/error.js:254:16)
at Messaging.validateRegistrationTokensType (/srv/node_modules/firebase-admin/lib/messaging/messaging.js:729:19)
at Messaging.sendToDevice (/srv/node_modules/firebase-admin/lib/messaging/messaging.js:328:14)
at admin.database.ref.once.snapshot (/srv/index.js:84:12)
at onceCallback (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:4933:51)
at /srv/node_modules/#firebase/database/dist/index.node.cjs.js:4549:22
at exceptionGuard (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:698:9)
at EventList.raise (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:9684:17)
The above indicates I am not retrieving data either at all or by the time the return is called. My JavaScript function code is:
'use strict';
const admin = require('firebase-admin');
admin.initializeApp();
exports.pushNotification = functions.database.ref('/Chat Messages/{chatId}/{pushID}').onCreate((snapshot, context) => {
const valueObject = snapshot.after.val();
return admin.database().ref(`/Chat Basics/${valueObject.chatKey}/Chat Users`).once('value', statusSnapshot => {
var index = 0;
var totalkeys = statusSnapshot.numChildren();
var msgIDs = [];
statusSnapshot.forEach(msg=>{
msgIDs.push(msg.key.toString());
if(index === totalkeys - 1){
const payload = {
notification : {
title: valueObject.userName,
body: valueObject.message,
sound: "default"
}
}
sendNotificationPayload(valueObject.uid, payload);
}
index++;
});
});
});
function sendNotificationPayload(uid, payload){
admin.database()
.ref(`/User Token Data/${uid}`)
.once('value', snapshot=> {
var tokens = [];
//if(!snapshot.exists())return;
snapshot.forEach(item =>{
tokens.push(item.val())
});
admin.messaging()
.sendToDevice(tokens, payload)
.then(res => {
return console.log('Notification sent')
})
.catch(err => {
return console.log('Error in sending notification = '+err)
});
});
}
This code is mostly inspired by what was said to be a working example here from another Stack Overflow question here. I have successfully tested sending a notification to a single device by manually copying a device token into my function, so the function does run to completion. My Java code seems to be irrelevant to the problem, so I have not added it (please ask in the comments if you would like it added for further context).
What I Have Tried:
I have tried implementing promises into my code, but I don't think I was doing it properly. My main reference for this was here. I have also looked at the documentation for literally everything related to this topic, however my knowledge of JS is not sufficient to really apply barebones examples to my code.
My Firebase Realtime Database Nodes:
#1: Loop through chat members to collect user IDs:
"Chat Basics" : {
"1607801501690_TQY41wIfArhHDxEisyupZxwyHya2" : {
"Chat Users" : {
"JXrclZuu1aOwEpCe6KW8vSDea9h2" : true,
"TQY41wIfArhHDxEisyupZxwyHya2" : true
},
#2: Collect user tokens from collected IDs (ignore that tokens are matching):
"User Token Data" : {
"JXrclZuu1aOwEpCe6KW8vSDea9h2" : "duDR3KH3i3I:APA91bH_LCeslZlqL8akYw-LrM9Dv__nx4nU1TquCS0j6bGF1tlIARcheREuNdX1FheC92eelatBC8LO4t6gt8liRdFHV-NDuNLa13oHYxKgl3JBPPlrMo5rB5XhH7viTo4vfYOMftRi",
"TQY41wIfArhHDxEisyupZxwyHya2" : "duDR3KH3i3I:APA91bH_LCeslZlqL8akYw-LrM9Dv__nx4nU1TquCS0j6bGF1tlIARcheREuNdX1FheC92eelatBC8LO4t6gt8liRdFHV-NDuNLa13oHYxKgl3JBPPlrMo5rB5XhH7viTo4vfYOMftRi"
}
Conclusion:
Concrete examples would be much appreciated, especially since I am crunching right now. Thanks for your time and help!
Update:
After some more testing, it looks like the problem is definitely due to my lack of understanding of promises in two areas. Firstly, only one user is collected before the final return is called. Secondly, the final return is called before the 2nd forEach() loop can store snapshot data to an array.
For this code then, how may I modify (or rebuild) it so that it collects all keys before proceeding to retrieve token data from all keys - ultimately before returning the notification?
Just as with every question I post, I managed to figure out how to do it (tentatively) a few hours later. Below is a full example of how to send a notification to chat users based on a message sent (although it does not yet exclude the sender) to a given chat. The order of operations are as such:
User message is saved and triggers event. Relevant data the message contains are:
username, chat key, message
These are retrieved, with (username + message) as the (title + body) of the
notification respectively, and the chat key is used for user id reference.
Loop through chat user keys + collect.
Loop through array of chat user keys to collect array of device tokens.
Send notification when complete.
The code:
//Use firebase functions:log to see log
exports.pushNotification = functions.database.ref('/Chat Messages/{chatId}/{pushId}').onWrite((change, context) => {
const valueObject = change.after.val();
return admin.database().ref(`/Chat Basics/${valueObject.chatKey}/Chat Users`).once('value', statusSnapshot => {
var index = 0;
var totalkeys = statusSnapshot.numChildren();
var msgIDs = [];
statusSnapshot.forEach(msg=>{
msgIDs.push(msg.key.toString());
if(index === totalkeys - 1){
const payload = {
notification : {
title: valueObject.userName,
body: valueObject.message,
sound: "default"
}
}
let promises = [];
var tokens = [];
for(let i=0; i < msgIDs.length; i++){
let userId = msgIDs[i];
let promise = admin.database().ref(`/User Token Data/${userId}`).once('value', snapshot=> {
tokens.push(snapshot.val());
})
promises.push(promise);
}
return Promise.all(promises).then(() => {
return admin.messaging().sendToDevice(tokens, payload);
});
}
index++;
return false;
});
});
});

How to get calender's events by calendar name, with Microsoft-Graph and NodeJS?

How can I make this one API call? This code uses microsoft-graph-client to make two api calls. The frist call gets the ID of the calender I want. The second uses the calendar's ID to get the events of that calendar. Would like to have one API call. All the documentation I have read so far does not have ability to get calendar events of a specified calendar.
//// Modules used...
var authHelper = require('../helpers/auth'); //// used to get the access token
var graph = require('#microsoft/microsoft-graph-client');
...
//// Initialize Graph client with access token.
const client = graph.Client.init({
authProvider: (done) => {
done(null, accessToken);
}
});
//// Specify start and end times for second API call.
const start = new Date(new Date().setHours(0,0,0));
const end = new Date(new Date(start).setDate(start.getDate() + 30000));
/**
* Step 1
* Get all the calendar, then cut out the calendar id I need.
* STEP 2
* Get the events using the calendar id.
*/
const calendars = await client
.api('https://graph.microsoft.com/v1.0/me/calendars/')
.select('name,id')
.get();
/**
* Cut out the id of first calendar named 'School_Calendar' in the array of calendars.
*/
const c = calendars.value.find(obj => {
return obj.name === 'School_Calendar'
});
const calen_id = c.id;
/**
* Now use the Calendar's id to get the calendar's events.
*/
const api = `/me/calendars/${calen_id}/calendarView?startDateTime=${start.toISOString()}&endDateTime=${end.toISOString()}`;
const result = await client
.api(api)
.select('subject,start,end')
.orderby('start/dateTime DESC')
.get();
//// print the events of School_Calendar
console.log(result.value;);
That's doable since calendar could be addressed by id and its name like this:
GET /me/calendars/{id|name}
where name corresponds to a Calendar.name property
Hence events could retrieved via a single request like this:
GET /me/calendars/{name}/calendarView?startDateTime={start_datetime}&endDateTime={end_datetime}
Example
const data = await client
.api(
`/users/${userId}/calendars/${calName}/calendarView?startDateTime=${start.toISOString()}&endDateTime=${end.toISOString()}`
)
.get()
const events = data.value;
for (let event of events) {
//...
}

How to get key of parent

I just want to get the stripe customer ID whenever a new payment method is added to my database.
I'm new to realtime databases and I just need some guidance.
var newPaymentMethodRef = functions.database.ref('/stripe_customers/{uid}/newPaymentMethod');
var stripe_customers = functions.database.ref('/stripe_customers/{uid}');
exports.newPaymentMethod = newPaymentMethodRef.onWrite(event => {
console.log("uid = ${uid}");
console.log("newPaymentMethod created");
});
I've tried
event.key
event.parent.key
event.ref("/stripe_customers/")
I'm obviously lost
If the Stripe customer ID is the value that is passed in the {uid} part if the call to the database, you can get it with:
exports.newPaymentMethod = newPaymentMethodRef.onWrite((change, context) => {
console.log(context.params.uid);
Also see the Firebase documentation on Realtime Database triggers.

Firebase database trigger - onCreate gets triggered on delete and update

I have three Firebase database trigger function to push notifications to users. However, I have noticed that .onCreate() gets triggered on database update and delete. Is this expected? Is there a way to prevent this?
const functions = require('firebase-functions')
exports.onNoteCreate = functions
.region('europe-west1')
.database
.ref('/notes/{noteId}')
.onCreate((snapshot, context) => {
...
//Push notification to affected users
//Compose notification object
const notificationObject = { "test": true }
membersToAlert.forEach((memberId, index) => {
let isAlreadyPresent = false
//Do not write if already present! - This code should not be needed?
const ref = snapshot.ref.root.child(`/notes/${personId}/noteAdditions`)
ref.orderByChild('originId')
.equalTo(noteId)
.on("value", (removeSnapshot) => {
isAlreadyPresent = true
})
//Write notification to all affected users
if(!isAlreadyPresent) {
snapshot.ref.root.child(`/notifications/${personId}/noteAdditions`).push(notificationObject)
}
})
return true
})
My .onUpdate() and .onDelete() triggers are also listening to .ref('/notes/{noteId}'). Is that a problem?
How can I make sure .onCreate() only gets triggered when a new object is inserted?
EDIT:
My testing workflow is as follows:
Create a new node in /notes using .push() -> works as expected
Update the same node using .update() -> works as expected
Delete the node in /notes/{noteId} directly from the Firebase Console
Step 3 triggers both .onCreate() and .onUpdate(). See log below:
I 2019-08-12T17:17:25.867Z onNoteCreate 670055884755913 onNoteCreate ... onNoteCreate 670055884755913
I 2019-08-12T17:17:26.053Z onNoteUpdate 670048941917608 onNoteUpdate ... onNoteUpdate 670048941917608
D 2019-08-12T17:17:26.843878505Z onNoteDelete 670054292162841 Function execution started onNoteDelete 670054292162841
D 2019-08-12T17:17:26.849773576Z onNoteDelete 670054292162841 Function execution took 7 ms, finished with status: 'ok' onNoteDelete 670054292162841
Database before delete
-notifications
-userId
-noteAdditions
-guid01
-notificationData
-noteUpdates
-guid03
-notificationData
Database after delete
//guid01 gets deleted by .onDelete() as expected
//guid03 gets deleted by .onDelete() as expected
-notifications
-userId
-noteAdditions
-guid02
-notificationData //Inserted by .onCreate() upon delete
-noteUpdates
-guid04
-notificationData //Inserted by .onUpdate() upon delete
The listeners are attached to /notes/{noteId} and updates are being made at /notifications/{userId}/...
onNoteCreate
exports.onNoteCreate = functions
.region('europe-west1')
.database
.ref('/notes/{noteId}')
.onCreate((snapshot, context) => {
...
snapshot.ref.root.child(`/notifications/${personId}/noteAdditions`).push(notificationObject)
...
console.log('onNoteCreate', '...')
...
})
onNoteUpdate
exports.onNoteUpdate = functions
.region('europe-west1')
.database
.ref('/notes/{noteId}')
.onUpdate((change, context) => {
...
change.after.ref.root.child(`/notifications/${personId}/noteUpdates`).push(notificationObject)
...
console.log('onNoteUpdate', '...')
...
})
Does it matter that I import the functions like so?
const create = require('./src/db-functions/notes').onNoteCreate
const update = require('./src/db-functions/notes').onNoteUpdate
const delete = require('./src/db-functions/notes').onNoteDelete
exports.onNoteCreate = create
exports.onNoteUpdate = update
exports.onNoteDelete = delete
I failed to include the code in my example where I call
//Get user data - also updated by onNoteCreate, onNoteUpdate , onNoteDelete
dbRoot.child(`users/${userId}`)
.on('value', (snapshot) => {
.on() attached a listener that was being triggered each time the value was updated, thus triggering "onNoteCreate", "onNoteUpdate" and "onNoteDelete" when not expected. I should have used .once() if I did not wish to attach a listener which I did not.
All credits to #doug for pointing this out to me in this post:
Firebase database trigger - how to wait for calls

Firebase get an specific key

i have a realtime-database with the following structure:
-- test-b7a6b
-- locations
-- 0
-- logs
-- alarm_2a330b56-c1b8-4720-902b-df89b82ae13a
...
-- devices
-- deviceTokens
-- 1
-- 2
i am using firebase-functions that gets executed when a new log gets written
let functions = require('firebase-functions');
let admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendPush = functions.database.ref('/locations/0/logs/{devicelogs}/{logs}').onWrite((change, context) => {
let logsData = change.after.val();
loadUsers();
getBody(deviceTypeId);
//managing the results
});
i have other functions that i want to referenciate to the same location as the one with a new log
function loadUsers() {
let dbRef = admin.database().ref('/locations/0/deviceTokens');
//managing users
}
function getBody(deviceTypeId) {
let devicesDB = admin.database().ref('/locations/0/devices');
//managing devices
}
putting the location manually on all 3 functions makes it work just fine but i don't know how to make it listen for the same event on all the locations ( 0, 1 and 2 ) and possibly more locations in the future
So my question: is there a way i can get the location key when a log gets written to any location so i can send it to the other functions
To listen to all locations, use a parameter in the path that triggers the function:
exports.sendPush = functions.database.ref('/locations/{location}/logs/{devicelogs}/{logs}').onWrite((change, context) => {
You can then get the parameters from context.params and pass it on:
exports.sendPush = functions.database.ref('/locations/{location}/logs/{devicelogs}/{logs}').onWrite((change, context) => {
let logsData = change.after.val();
loadUsers(context.params.location);
getBody(deviceTypeId);
//managing the results
});
Also see the Cloud Functions for Firebase documentation on handling event data.

Categories