Firebase Function admin.database() null parameter in a transaction - javascript

I'm using Firebase Functions with the "Spark Plan" (free). This is part of my function:
return query.once("value").then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
var childData = childSnapshot.val();
if (childData.displayName === ally) {
existAlly = true;
console.log('uid: '+uid)
var ref = admin.database().ref('users/'+uid).transaction(function (current_value) {
console.log('current_value: '+uid)
current_value.mainAlly = ally;
current_value.coins = (current_value.coins || 0) + 10
return current_value;
}).then(() => {
console.log('New Ally added');
return true;
});
...
Here the logs, you can see "'current_value: null'
But, ss you can see in the next picture, the "ref" is correct:
So, is it a billing issue? The "admin.database()" stops working after a while? Or is it something else?
Thanks!
EDIT: I just did another test, and now the error is in "coins", with the same code:

Everything is working as expected. When working with transactions, you can expect that your handler function will get called the first time with null (which you will have to check for), then again with the actual contents of the database. You should review the documentation, and pay special attention to the note that says:
Transaction Function is Called Multiple Times
Your transaction handler is called multiple times and must be able to
handle null data. Even if there is existing data in your database it
may not be locally cached when the transaction function is run.

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;
});
});
});

#firebase/database: FIREBASE WARNING: Exception was thrown by user callback. TypeError: Cannot read property 'title' of null

UPDATED CODE
So this is making me crazy...I am creating a blog app (Vanilla Js + Firebase).
onDeleteButton deletes a single blog post. It deletes both front end
and backend. no error.
onEditButton edits a single blog post. It works fine as well.
When I first edit a post and after that delete the same post, it deletes from both back and and front end, seemingly works fine BUT in console log throws me and error:
Uncaught TypeError: Cannot read property 'title' of null.
The error message points at the storeTitle.value = editPost.title; line in onEditButton function.
Why does onEditButton function gets called after deleting a post, but only once it has been edited once?
function onDeleteButton(event) {
event.preventDefault();
let id = event.target.parentElement.getAttribute('id');
let deletePost = document.getElementById(id);
firebase.database().ref('posts/' + id).remove();
deletePost.parentNode.removeChild(deletePost);
}
function onEditButton(event) {
event.preventDefault();
let editButton = document.getElementById('edit');
editButton.removeAttribute("hidden");
let id = event.target.parentElement.getAttribute('id');
let posts = firebase.database().ref('posts/' + id);
let storeTitle = document.getElementById('blog-title');
let storeContent = document.getElementById('blog-content');
localStorage.setItem("postID", id);
document.getElementById("blog-id-storage").innerHTML = localStorage.getItem("postID");
posts.on('value', function(snapshot) {
let editPost = snapshot.val();
storeTitle.value = editPost.title;
storeContent.value = editPost.content;
});
}
function enterEditingMode() {
if (document.getElementById('editing-mode-button').innerHTML === "Exit editing mode") {
exitEditingMode();
}
else {
...
addDeleteButton.addEventListener("click", this.onDeleteButton);
addEditButton.addEventListener("click", this.onEditButton);
})
}
}
html:
<button id="editing-mode-button" onclick="enterEditingMode()" class="btn btn-outline-primary">Editing mode</button>
Just in case someone has the same issue in the future, here is the solution.
On the editbutton function I used posts.on('value', function(snapshot) {...} so everytime I run this function once it was listening for changes, the code block in {} was running every time...So instead of posts.on, I had to use posts.once('value', function(snapshot) {...}. .once() without listening for changes
From firebase doc:
Listen for value events
To read data at a path and listen for changes, use the on() or once() methods of firebase.database. Reference to observe events.

Firebase update() not a function with a query

I'm trying to update a property in a record in Firebase Database, with AngularJS. I can set up a query to find my record:
firebase.database().ref('en/').orderByChild('word').equalTo('the').once('value')
.then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
console.log(childSnapshot.val())
});
})
I can update my property, if I hardcode in the record's key:
firebase.database().ref('en/-KloeQHDC-mugPjJMAG4').update({ wordFrequency: 111 })
But if I set up a query to find the record and then update it, I get an error message update is not a function:
firebase.database().ref('en/').orderByChild('word').equalTo('the').update({ wordFrequency: 9001 })
Another answer suggests calling update() from inside a forEach loop:
firebase.database().ref('en/').orderByChild('word').equalTo('the').once('value')
.then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
console.log(childSnapshot.val()); // this works
childSnapshot.ref().update({ wordFrequency: 9001 });
});
});
That returns an error message TypeError: childSnapshot.ref is not a function. I don't see how childSnapshot is a Firebase ref.
Another answer says
When you call update() on a location, Firebase loops over the data
that you pass in (in your case asJson) and for each key performs a
ref.child(key).set(value).
If update() loops over the data, why should I call update() from inside a forEach loop? The documentation doesn't show calling update() from inside a forEach loop.
The Firebase Database SDK provides a Reference.update() method to update data in a single location in a database. Key here is that a Reference is a single location in the database, so it is clear what to update.
My pseudo-code explanation about how multi-path updates work applies to how the database server implements it: given a single location/DatabaseReference it updates each path in the update() call based on that.
A Query can match multiple locations in the database, so it doesn't have an update() method (or set() or remove() for that matter).
To update each location matched by a query, you execute the query and then call update() on each result - either by a child_added listener, or with a value listener and a loop like in your last snippet.
After I posted this question I walked the dog, ate dinner, and then the solution came to me. My new rule is, "The key to Firebase queries is to keep track of the key."
This template is for users to update records in the database. They enter a search term in a form field and click the "Search" button. The $scope.search handler queries the Firebase database and then populates the form fields with the record's properties:
$scope.search = function() {
myFirebase_ref.orderByChild('word').equalTo($scope.word).once('value')
.then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
$scope.wordKey = childSnapshot.key;
$scope.audioArray = childSnapshot.val().audio;
$scope.ipaArray = childSnapshot.val().ipa;
$scope.language = childSnapshot.val().language;
$scope.longLanguage = childSnapshot.val().longLanguage;
$scope.phonemeArray = childSnapshot.val().phonemes;
$scope.translationArray = childSnapshot.val().translations;
$scope.word = childSnapshot.val().word;
$scope.wordFrequency = childSnapshot.val().wordFrequency;
$scope.$apply();
});
})
.catch(function(error) {
console.error("Authentication failed:", error.message);
});
};
Note at the top of the property assignments I have $scope.wordKey = childSnapshot.key;. I'm keeping track of the record's key.
The user then updates a field. Each field has a button next to it for "Update". Each button goes to a handler. For example, to update the wordFrequency field I have this handler:
$scope.updateFrequencyRank = function() {
firebase.database().ref('en/' + $scope.wordKey).update({ wordFrequency: $scope.wordFrequency })
};
One line of code and it works! Even better, I made an onComplete function to tell me if the update succeeded:
$scope.updateFrequencyRank = function() {
var onComplete = function(error) {
if (error) {
console.log('Update failed');
} else {
console.log('Update succeeded');
}
};
firebase.database().ref('en/' + $scope.wordKey).update({ wordFrequency: $scope.wordFrequency }, onComplete);
};

How to get database value not related to the event in Cloud Functions for Firebase?

I have a firebase database and I am currently trying to use cloud functions to perform an operation when a value in my database changes. So far, it successfully triggers code to run when the value in my database changes. However, when the database value changes, I now need to check another value to determine it's status, and then perform an action after that. The problem is that I have ~0 experience with JS and I have no way of debugging my code other than deploying, changing the value in my database, and looking at the console log.
Is there any way to look up another value in the database and read it? How about look up a value and then set a value for it? Here is the code:
exports.determineCompletion =
functions.database.ref('/Jobs/{pushId}/client_job_complete')
.onWrite(event => {
const status = event.data.val();
const other = functions.database.ref('/Jobs/' + event.params.pushId + '/other_job_complete');
console.log('Status', status, other);
if(status == true && **other.getValueSomehow** == true) {
return **setAnotherValue**;
}
});
This code partially works, it successfully gets the value associated with client_job_complete and stores it in status. But how do I get the other value?
Additionally, if anyone has any JS or firebase documentation that they think would help me, please share! I have read a bunch on firebase here : https://firebase.google.com/docs/functions/database-events but it only talks about events and is very brief
Thank you for your help!
When writing a database trigger function, the event contains two properties that are references to the location of the data that changed:
event.data.ref
event.data.adminRef
ref is limited to the permissions of the user who triggered the function. adminRef has full access to the database.
Each of those Reference objects has a root property which gives you a reference to the root of your database. You can use that reference to build a path to a reference in another part of your database, and read it with the once() method.
You can also use the Firebase admin SDK.
There are lots of code samples that you should probably look at as well.
I'm maybe a bit late, but I hope my solution can help some people:
exports.processJob = functions.database.ref('/Jobs/{pushId}/client_job_complete').onWrite(event => {
const status = event.data.val();
return admin.database().ref('Jobs/' + event.params.pushId + '/other_job_complete').once('value').then((snap) => {
const other = snap.val();
console.log('Status', status, other);
/** do something with your data here, for example increase its value by 5 */
other = (other + 5);
/** when finished with processing your data, return the value to the {{ admin.database().ref(); }} request */
return snap.ref.set(other).catch((error) => {
return console.error(error);
});
});
});
But take notice of your firebase database rules.
If no user should have access to write Jobs/pushId/other_job_complete, except Your cloud function admin, You need to initialize Your cloud function admin with a recognizable, unique uid.
For example:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const adminCredentials = require('path/to/admin/credentials.json');
admin.initializeApp({
credential: admin.credential.cert(adminCredentials),
databaseURL: "https://your-database-url-com",
databaseAuthVariableOverride: {
uid: 'super-special-unique-firebase-admin-uid'
}
});
Then Your firebase database rule should look something like this:
"client_job_complete": {
".read": "auth !== null",
".write": "auth.uid === 'super-special-unique-firebase-admin-uid'"
}
Hope it helps!
You have to wait on the promise from a once() on the new ref, something like:
exports.processJob = functions.database.ref('/Jobs/{pushId}/client_job_complete')
.onWrite(event => {
const status = event.data.val();
const ref = event.data.adminRef.root.child('Jobs/'+event.params.pushId+'/other_job_complete');
ref.once('value').then(function(snap){
const other = snap.val();
console.log('Status', status, other);
if(status && other) {
return other;
}
});
});
Edit to fix the error that #Doug Stevenson noticed (I did say "something like")

How to read Object from Firebase using AngularJS

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

Categories