Firestore Cloud Function - Get request data object in onUpdate/onCreate - firebase

When writing firebase rules, you can access the request data via request.resource.data. This is useful because you can look at the nature of the request to determine its intent, its write target and permit or deny. This enables merging properties into an object within a document owned by a user, vs using a nested collection of documents.
I would like to access the same request data in the cloud function callbacks update/write/etc, but I don't see it, and I'm left to do an object compare with change.before and change.after. It's not a problem, but did I miss something in the documentation?
Per documentation: https://firebase.google.com/docs/firestore/extend-with-functions
exports.myFunctionName = functions.firestore.document('users/marie').onWrite((change, context) => {
// ... the change or context objects do not contain the request data
});

I had the exact same question when I realized that a function listening for updates was being triggered regardless of the property being updated, despite having a 'status' in data check. The catch that data represented handler.after.data. Although I wasn't able to access the request data, either from the handler or from the context, I was able to solve the problem by adding an additional check which serves the same purpose. Namely:
const dataBefore = handler.before.data();
const dataAfter = handler.after.data();
if (status in dataBefore && status in dataAfter) {
if (dataBefore.status === 'unpublished' && dataAfter.status === 'published') {
// handle update
}
}

Related

Retrieve values from firebase database in conversation flow

I am trying to grab information from my firebase database after a particular intent is invoked in my conversation flow.
I am trying to make a function which takes a parameter of user ID, which will then get the highscore for that user, and then say that users highscore back to them.
app.intent('get-highscore', (conv) => {
var thisUsersHighestscore = fetchHighscoreByUserId(conv.user.id);
conv.ask('your highest score is ${thisUsersHighestScore}, say continue to keep playing.');
});
function fetchHighscoreByUserId(userId){
var highscoresRef = database.ref("highscores");
var thisUsersHighscore;
highscoresRef.on('value',function(snap){
var allHighscores= snap.val();
thisUsersHighscore = allHighscores.users.userId.highscore;
});
return thisUsersHighscore;
}
An example of the data in the database:
"highscores" : {
"users" : {
"1539261356999999924819020" : {
"highscore" : 2,
"nickname" : "default"
},
"15393362381293223232222738" : {
"highscore" : 78,
"nickname" : "quiz master"
},
"15393365724084067696560" : {
"highscore" : "32",
"nickname" : "cutie pie"
},
"45343453535534534353" : {
"highscore" : 1,
"nickname" : "friendly man"
}
}
}
It seems like it is never setting any value to thisUsersHighScore in my function.
You have a number of issues going on here - both with how you're using Firebase, how you're using Actions on Google, and how you're using Javascript. Some of these issues are just that you could be doing things better and more efficiently, while others are causing actual problems.
Accessing values in a structure in JavaScript
The first problem is that allHighscores.users.userId.highscore means "In an object named 'allHighscores', get the property named 'users', from the result of that, get the property named 'userId'". But there is no property named "userId" - there are just a bunch of properties named after a number.
You probably wanted something more like allHighscores.users[userId].highscore, which means "In an object named 'allHighscores', get the property named 'users', fromt he result of that, get the property named by the value of 'userId'".
But if this has thousands or hundreds of thousands of records, this will take up a lot of memory. And will take a lot of time to fetch from Firebase. Wouldn't it be better if you just fetched that one record directly from Firebase?
Two Firebase Issues
From above, you should probably just be fetching one record from Firebase, rather than the whole table and then searching for the one record you want. In firebase, this means you get a reference to the path of the data you want, and then request the value.
To specify the path you want, you might do something like
var userRef = database.ref("highscores/users").child(userId);
var userScoreRef = userRef.child( "highscore" );
(You can, of course, put these in one statement. I broke them up like this for clarity.)
Once you have the reference, however, you want to read the data that is at that reference. You have two issues here.
You're using the on() method, which fetches the value once, but then also sets up a callback to be called every time the score updates. You probably don't need the latter, so you can use the once() method to get the value once.
You have a callback function setup to get the value (which is good, since this is an async operation, and this is the traditional way to handle async operations in Javascript), but you're returning a value outside of that callback. So you're always returning an empty value.
These suggest that you need to make fetchHighScoreByUserId() an asynchronous function as well, and the way we have to do this now is to return a Promise. This Promise will then resolve to an actual value when the async function completes. Fortunately, the Firebase library can return a Promise, and we can get its value as part of the .then() clause in the response, so we can simplify things a lot. (I strongly suggest you read up on Promises in Javascript and how to use them.) It might look something like this:
return userScoreRef.once("value")
.then( function(scoreSnapshot){
var score = scoreSnapshot.val();
return score;
} );
Async functions and Actions on Google
In the Intent Handler, you have a similar problem as above. The call to fetchHighScoreByUserId() is async, so it doesn't finish running (or returning a value) by the time you call conv.ask() or return from the function. AoG needs to know to wait for an async call to finish. How can it do that? Promises again!
AoG Intent Handlers must return a Promise if there is an asyc call involved.
Since the modified fetchHighScoreByUserId() returns a Promise, we will leverage that. We'll also set our response in the .then() part of the Promise chain. It might look something like this:
app.intent('get-highscore', (conv) => {
return fetchHighscoreByUserId(conv.user.id)
.then( function(highScore){
conv.ask(`Your highest score is ${highScore}. Do you want to play again?`);
} );
});
Two asides here:
You need to use backticks "`" to define the string if you're trying to use ${highScore} like that.
The phrase "Say continue if you want to play again." is a very poor Voice User Interface. Better is directly asking if they want to play again.

How can I avoid throttling? (or, is there a way to really batch get?)

Looks like my Firestore requests are being throttled! (see picture below)
If I had to guess, I'd say it's not Firestore doing the throttling, it's actually Chrome... I've seen this kind of throttling before.
One way to get around Chrome's throttling here is to combine multiple requests into one.
As I sit here thinking "I wonder if Firestore can combine these read requests?", I notice that, coincidentally, the requests to Firestore are actually named batchGet! But oddly enough, each batchGet request only has one read inside it:
So, in an effort to avoid Chrome's throttling, my question is: how do I request multiple documents at the same time in Firestore? I suspect it's possible because, well, there's a server endpoint named batchGet. Is it possible?
Many thanks!
Just dug through the source a bit, looks like it's impossible. Transaction's .get() method looks like this:
Transaction.prototype.get = function (documentRef) {
var _this = this;
validateExactNumberOfArgs('Transaction.get', arguments, 1);
var ref = validateReference('Transaction.get', documentRef, this._firestore);
return this._transaction
.lookup([ref._key])
.then(function (docs) {
if (!docs || docs.length !== 1) {
return fail('Mismatch in docs returned from document lookup.');
}
var doc = docs[0];
if (doc instanceof NoDocument) {
return new DocumentSnapshot(_this._firestore, ref._key, null, false);
}
return new DocumentSnapshot(_this._firestore, ref._key, doc, false);
});
};
That .lookup() takes an array of all the documents to put into the batchGet request, and it's coded here to only ever have one element in that array.
So, looks like there's no way to get multiple documents in one batchGet request.

EmberFire: Getting property generated by Cloud Function when saving record completes

I use a Cloud Function to generate a short unique URL on a record on the 'onWrite' event, and save it. This works well, but when I save a record from my Ember app using EmberFire, I do get a model back as an argument to a callback, but the URL of this model is undefined. Is there a way to return this back to the client? Or do I need to query the record to get the generated URL?
This is how my Cloud Function code looks:
exports.generateUrl = functions.database.ref('/tournaments/{tid}')
.onWrite(event => {
if (event.data.previous.exists()) {
return;
}
if (!event.data.exists()) {
return;
}
const url = shortid.generate();
return event.data.ref.update({ url });
});
Here is my component that saves data through form submission. I'm using an add-on called ember-changeset to handle some validations, but this shouldn't be related to the issue.
export default Ember.Component.extend({
submit(e) {
e.preventDefault();
let snapshot = this.changeset.snapshot();
return this.changeset
.cast(Object.keys(this.get('schema')))
.validate()
.then(() => {
if (this.changeset.get('isValid')) {
return this.changeset
.save()
.then((result) => {
// Here, result.get('url') is undefined.
})
}
})
}
});
If you have a function that writes new data back to a location in the database after a write, you'll have to keep listening to that location on the client in order to get that data back. Don't use a one-time read (once()), use a persistent listener (on()), and in that listener, make sure you're getting the URL or whatever you expect to be generated by the function. Then remove that listener if you don't need it any more.
(Sorry, I don't know Ember or what abstractions it provides around Realtime Database - I'm giving you the plain JavaScript API methods you'd use on a reference.)

Cloud Functions for Firebase monitor node leaf if deleted check if parent exists

I am monitoring for changes in node leaf jobs/{jobid}/proposals. Whenever I remove the proposals the function gets executed and reinsert proposals (this is the expected behavior).
The problem is When I remove its parent {job}, proposals gets reinserted in a new object with same parent ID. Is there a way to do a check if the parent exists? If so, reinsert proposal otherwise not.
exports.RecountProposals = functions.database.ref("/jobs/{jobid}/proposals").onWrite(event => {
const jobid = event.params.jobid;
if (!event.data.exists() && event.data.ref.parent.exists()) {
const propRef = admin.database().ref(`proposals/${jobid}`);
const counterRef = event.data.ref;
const collectionRef = counterRef.parent.child('proposals');
// Return the promise from counterRef.set() so our function
// waits for this async event to complete before it exits.
return propRef.once('value')
.then(messagesData => collectionRef.set(messagesData.numChildren()));
}
});
I am checking if parent exists but it is showing an error:
event.data.ref.parent.exists()
TypeError: event.data.ref.parent.exists is not a function
event.data.ref.parent is a Reference type object. As you can see from the linked doc, there is no exists() method on Reference. In Realtime Database, if you want to know if there is any data at a node, simply fetch the snapshot there and call val() on it to check to see if it's null. Reference objects are just paths, they don't contain any knowledge of data.
To put it another way, there is no such concept as a node that "exists" but contains no data, like an empty folder in a filesystem. For any given path that you can construct, the snapshot of the data there is either available (non-null) or not (null).

Meteor GroundDB granularity for offline/online syncing

Let's say that two users do changes to the same document while offline, but in different sections of the document. If user 2 goes back online after user 1, will the changes made by user 1 be lost?
In my database, each row contains a JS object, and one property of this object is an array. This array is bound to a series of check-boxes on the interface. What I would like is that if two users do changes to those check-boxes, the latest change is kept for each check-box individually, based on the time the when the change was made, not the time when the syncing occurred. Is GroundDB the appropriate tool to achieve this? Is there any mean to add an event handler in which I can add some logic that would be triggered when syncing occurs, and that would take care of the merging ?
The short answer is "yes" none of the ground db versions have conflict resolution since the logic is custom depending on the behaviour of conflict resolution eg. if you want to automate or involve the user.
The old Ground DB simply relied on Meteor's conflict resolution (latest data to the server wins) I'm guessing you can see some issues with that depending on the order of when which client comes online.
Ground db II doesn't have method resume it's more or less just a way to cache data offline. It's observing on an observable source.
I guess you could create a middleware observer for GDB II - one that checks the local data before doing the update and update the client or/and call the server to update the server data. This way you would have a way to handle conflicts.
I think to remember writing some code that supported "deletedAt"/"updatedAt" for some types of conflict handling, but again a conflict handler should be custom for the most part. (opening the door for reusable conflict handlers might be useful)
Especially knowing when data is removed can be tricky if you don't "soft" delete via something like using a "deletedAt" entity.
The "rc" branch is currently grounddb-caching-2016 version "2.0.0-rc.4",
I was thinking about something like:
(mind it's not tested, written directly in SO)
// Create the grounded collection
foo = new Ground.Collection('test');
// Make it observe a source (it's aware of createdAt/updatedAt and
// removedAt entities)
foo.observeSource(bar.find());
bar.find() returns a cursor with a function observe our middleware should do the same. Let's create a createMiddleWare helper for it:
function createMiddleWare(source, middleware) {
const cursor = (typeof (source||{}).observe === 'function') ? source : source.find();
return {
observe: function(observerHandle) {
const sourceObserverHandle = cursor.observe({
added: doc => {
middleware.added.call(observerHandle, doc);
},
updated: (doc, oldDoc) => {
middleware.updated.call(observerHandle, doc, oldDoc);
},
removed: doc => {
middleware.removed.call(observerHandle, doc);
},
});
// Return stop handle
return sourceObserverHandle;
}
};
}
Usage:
foo = new Ground.Collection('test');
foo.observeSource(createMiddleware(bar.find(), {
added: function(doc) {
// just pass it through
this.added(doc);
},
updated: function(doc, oldDoc) {
const fooDoc = foo.findOne(doc._id);
// Example of a simple conflict handler:
if (fooDoc && doc.updatedAt < fooDoc.updatedAt) {
// Seems like the foo doc is newer? lets update the server...
// (we'll just use the regular bar, since thats the meteor
// collection and foo is the grounded data
bar.update(doc._id, fooDoc);
} else {
// pass through
this.updated(doc, oldDoc);
}
},
removed: function(doc) {
// again just pass through for now
this.removed(doc);
}
}));

Resources