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
Related
Introduction
In order to listen all the docs of my collection that have beed removed, without downloading the entire collection, I am doing the same as commented here.
Rather than deleting docs completely, just add a "deleted: true" property and then listen on the query "db.collection("cities").where("deleted", "==", true)"
Problem
For some reason I don't understand, I am receiving all the "modified" events as "added" from my Firestore listener.
Chat Room Deletions Code
I am implementing a chat list screen with pagination, so I am just fetching old docs when the screen is mounted, and subscribing to all changes that happens on each chat room doc after new Date() (the time in which the screen is mounted).
For "removing" the chat room docs, I am doing the following:
async function deleteChatRoom(chatRoomId, userId) {
const chatRoomRef = firestore.collection("chats").doc(chatRoomId);
const chatRoomDoc = await chatRoomRef.get();
const chatRoomData = chatRoomDoc.data();
const messagesPath = `${chatRoomRef.path}/messages`;
// Check that the user is member of the room
if (!chatRoomData.members[userId]) {
throw chatErrors.notChatRoomMember();
}
// Delete all the chatroom messages
await deleteCollection(messagesPath);
// Delete the chat room
await chatRoomRef.update({
deleted: true, <----
lastMessage: admin.firestore.FieldValue.delete(), // Delete denormalized sensitive data
modifiedAt: admin.firestore.FieldValue.serverTimestamp(), <----
read: admin.firestore.FieldValue.delete(), // Delete denormalized sensitive data
});
}
As you can see, I am updating the "modifiedAt" and "deleted" fields.
Listener Code
With this, what I am trying to reach is being able to listen to all the chat room changes ("lastMessage" field updates, "read" field updates, "deletions"...) that happens after new Date() in the client side, as I commented before. As follows:
export function listenMyChats(
startAt = undefined,
onNext,
onError
) {
const currentUserId = getCurrentUser()?.uid;
if (!currentUserId) {
throw authErrors.authenticationRequired();
}
let query = firestore
.collection("chats")
.where("membersArray", "array-contains", currentUserId)
.orderBy("modifiedAt");
if (startAt) {
query = query.startAt(startAt);
}
return query.onSnapshot(onNext, onError);
}
Something that has sense to me, as I am avoiding reading the entire collection when using the field "startAt" and "orderBy(modifiedAt)".
Subscribing code
Then, to handle all and subscribe to the chat room changes, I am doing the following:
const handleChatsChanges = async (querySnapshot) => {
const changes = querySnapshot.docChanges();
await Promise.all(
changes.map(async (change) => {
if (change.type === "added") {
// NEW INCOMING CHATS
console.log("Added");
} else if (change.type === "modified") {
// MODIFIED CHATS ("deleted", "read", "lastMessage" fields...)
console.log("Modified");
}
});
);
};
useEffect(() => {
const listener = listenMyChats(
startAfter.current,
handleMyChatsChanges,
handleOnListenMyChatsError
);
return () => {
listener();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
The main problem I am experiencing is that, instead of getting the document modifications in the "modified" scope, I am getting "added" events... Why is this happening?
I thought that the listeners behavior were to get all the docs that match the query condition (as "added") when subscribing. And that, after this, the "added" scope was only executed when new docs were added to the target collection (matching the query conditions, of course).
So, why am I receiving the docs fields modifications as "added"?
As detailed in the comments above, the problem comes form the fact that the Firestore documents that are modified where not present in the first snapshot, because your listener is applied on the following query:
firestore
.collection("chats")
.where("membersArray", "array-contains", currentUserId)
.orderBy("modifiedAt");
And when you modify them they are returned by the above query and therefore they are considered as added by the listener.
They would have been considered as modified if, and only if, they were included in the first snapshot returned after setting the listener.
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.
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.
How can I remove ref after my function is finished running? Is it necessary? I want my function to run as quickly as possible, and don't want "things" piling up.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.myFunction = functions.database.ref('/path/{uid}').onWrite(event => {
const ref = event.data.adminRef.root.child('something').child(event.params.uid);
return ref.transaction(current => {
if (event.data.exists() && !event.data.previous.exists()) {
return _.toInteger(current) + _.toInteger(_.get(data, 'value', 0));
}
}).then(() => {
return null; // Avoid "Error serializing return value: TypeError: Converting circular structure to JSON"
});
});
A DatabaseReference is nothing you can "remove". It is just a pointer to a location in your database. The documentation has a page for it:
https://firebase.google.com/docs/reference/admin/node/admin.database.Reference
The only thing you can remove/detach is a callback you set with ref.on(...), with ref.off(...), but there is no callback in your code and I think that ref.once() should get the job done most of the time in Functions.
To be clear: ref.transactions()'s do not have to be detached, they just run once, i.e. there is no callback. Same for ref.set() and ref.once().
I am developing my app, and one of the features will be messaging within the application. What I did, is I've developed 'send message' window, where user can send message to other user. The logic behind it is as following:
1. User A sends message to User B.
2. Firebase creates following nodes in 'Messaging':
"Messaging"->"User A"->"User B"->"Date & Time"->"UserA: Message"
"Messaging"->"User B"->"User A"->"Date & Time"->"UserA: Message"
Here is the code that I am using for sending messages:
sendMsg: function(receiver, content) {
var user = Auth.getUser();
var sender = user.facebook.id;
var receiverId = receiver;
var receiverRef = $firebase(XXX.firebase.child("Messaging").child(receiverId).child(sender).child(Date()));
var senderRef = $firebase(XXX.firebase.child("Messaging").child(sender).child(receiverId).child(Date()));
receiverRef.$set(sender,content);
senderRef.$set(sender,content);
},
(picture 1 in imgur album)
At the moment, I am trying to read the messages from the database, and sort them in according to date. What I've accomplished so far, is that I have stored the content of "Messaging/UserA/" in form of an Object. The object could be seen in the picture I've attached (picture 2).
http://imgur.com/a/3zQ0o
Code for data receiving:
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
Messages.on("value", function (snapshot) {
var messagesObj = snapshot.val();
return messagesObj;
}, function (errorObject) {
console.log("Error code: " + errorObject.code);
});
}
My question is: how can I read the object's messages? I would like to sort the according to the date, get the message and get the Id of user who has sent the message.
Thank you so much!
You seem to be falling for the asynchronous loading trap when you're reading the messages:
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
Messages.on("value", function (snapshot) {
var messagesObj = snapshot.val();
return messagesObj;
}, function (errorObject) {
console.log("Error code: " + errorObject.code);
});
}
That return statement that you have in the Messages.on("value" callback doesn't return that value to anyone.
It's often a bit easier to see what is going on, if we split the callback off into a separate function:
onMessagesChanged(snapshot) {
// when we get here, either the messages have initially loaded
// OR there has been a change in the messages
console.log('Inside on-value listener');
var messagesObj = snapshot.val();
return messagesObj;
},
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
console.log('Before adding on-value listener');
Messages.on("value", onMessagesChanged);
console.log('After adding on-value listener');
}
If you run the snippet like this, you will see that the console logs:
Before adding on-value listener
After adding on-value listener
Inside on-value listener
This is probably not what you expected and is caused by the fact that Firebase has to retrieve the messages from its servers, which could potentially take a long time. Instead of making the user wait, the browser continues executing the code and calls your so-called callback function whenever the data is available.
In the case of Firebase your function may actually be called many times, whenever a users changes or adds a message. So the output more likely will be:
Before adding on-value listener
After adding on-value listener
Inside on-value listener
Inside on-value listener
Inside on-value listener
...
Because the callback function is triggered asynchronously, you cannot return a value to the original function from it. The simplest way to work around this problem is to perform the update of your screens inside the callback. So say you want to log the messages, you'd do:
onMessagesChanged(snapshot) {
// when we get here, either the messages have initially loaded
// OR there has been a change in the messages
console.log('Inside on-value listener');
var i = 0;
snapshot.forEach(function(messageSnapshot) {
console.log((i++)+': '+messageSnapshot.val());
});
},
Note that this problem is the same no matter what API you use to access Firebase. But the different libraries handle it in different ways. For example: AngularFire shields you from a lot of these complexities, by notifying AngularJS of the data changes for you when it gets back.
Also see: Asynchronous access to an array in Firebase