In a sequence of multiple updates on a document, how can I execute the trigger on the most recent update?
exports.functionName = functions.firestore
.document("collection/{docId}")
.onUpdate((snapshot,context)=>{
// Trigger function execution
some_heavy_operation();
});
Let's say a field is updated frequently; how can I work on the most recent update to save myself from executing some_heavy_operation() frequently?
That's not possible. From the moment that code is deployed, every update to a matching document will trigger the function. If you want to avoid doing some work in the trigger, you will have to write some code in it to determine if it should happen or not.
Related
Not sure if this is even possible with firebase cloud functions.
Let's assume, I want to trigger a cloud function onCreate on all documents in a specific collection.
After creation, the cloud function should add another document in a different collection.
Passing a value from the manually created document.
Sure, that works!:
export const createAutomaticInvoice = functions.firestore.document('users/{userId}/lessons/{lesson}').onCreate((snap, context) => {
let db = admin.firestore();
let info = snap.ref.data()
db.collection('toAdd').add({
info: info
})
})
But if I create a document within users/{userId}/lessons/ and change the value of info directly afterwards, before the cloud function is triggered, the cloud function takes the old value of info as supposed to the one it was changed to.
Is this expected behaviour? For me it is definetely not as I would assume that it takes the values at runtime.
How can I make my example work as expected?
This is the expected behavior - the function is going to execute as soon as possible after that document is created. The snapshot is always going to contain the contents of the document as it was originally created. It's not going to wait around to see if that document changes at some point in the future, and it's not going to try to query that document in case it might have changed.
If you want to handle updates to a document, you should also be using an onUpdate trigger to know if that happens.
When writing event based cloud functions for firebase firestore it's common to update fields in the affected document, for example:
When a document of users collection is updated a function will trigger, let's say we want to determine the user info state and we have a completeInfo: boolean property, the function will have to perform another update so that the trigger will fire again, if we don't use a flag like needsUpdate: boolean to determine if excecuting the function we will have an infinite loop.
Is there any other way to approach this behavior? Or the situation is a consequence of how the database is designed? How could we avoid ending up in such scenario?
I have a few common approaches to Cloud Functions that transform the data:
Write the transformed data to a different document than the one that triggers the Cloud Function. This is by far the easier approach, since there is no additional code needed - and thus I can't make any mistakes in it. It also means there is no additional trigger, so you're not paying for that extra invocation.
Use granular triggers to ensure my Cloud Function only gets called when it needs to actually do some work. For example, many of my functions only need to run when the document gets created, so by using an onCreate trigger I ensure my code only gets run once, even if it then ends up updating the newly created document.
Write the transformed data into the existing document. In that case I make sure to have the checks for whether the transformation is needed in place before I write the actual code for the transformation. I prefer to not add flag fields, but use the existing data for this check.
A recent example is where I update an amount in a document, which then needs to be fanned out to all users:
exports.fanoutAmount = functions.firestore.document('users/{uid}').onWrite((change, context) => {
let old_amount = change.before && change.before.data() && change.before.data().amount ? change.before.data().amount : 0;
let new_amount = change.after.data().amount;
if (old_amount !== new_amount) {
// TODO: fan out to all documents in the collection
}
});
You need to take care to avoid writing a function that triggers itself infinitely. This is not something that Cloud Functions can do for you. Typically you do this by checking within your function if the work was previously done for the document that was modified in a previous invocation. There are several ways to do this, and you will have to implement something that meets your specific use case.
I would take this approach from an execution time perspective, this means that the function for each document will be run twice. Each time when the document is triggered, a field lastUpdate would be there with a timestamp and the function only updates the document if the time is older than my time - eg 10 seconds.
I'm using Firestore at beta version with Cloud Functions. In my app I need to trigger a function that listens for an onCreate event at /company/{id}/point/{id} and performs an insert (collection('event').add({...}))
My problem is: Cloud Functions with Firestore require an idempotent function. I don't know how to ensure that if my function triggers two times in a row with the same event, I won't add two documents with the same data.
I've found that context.eventId could handle that problem, but I don't recognize a way to use it.
exports.creatingEvents = functions.firestore
.document('/companies/{companyId}/points/{pointId}')
.onCreate((snap, context) => {
//some logic...
return db.doc(`eventlog/${context.params.companyId}`).collection('events').add(data)
})
Two things:
First check your collection to see if a document has a property with the value of context.eventId in it. If it exists, do nothing in the function.
If a document with the event id doesn't already exist, put the value of context.eventId in a property in the document that you add.
This should prevent multiple invocations of the function from adding more than one document for a given event id.
Why not set the document (indexing by the event id from your context) instead of creating it? This way if you write it twice, you'll just overwrite rather than create a new record.
https://firebase.google.com/docs/firestore/manage-data/add-data
This approach makes the write operation idempotent.
I am using the firebase-tools shell CLI to test Firestore cloud functions.
My functions respond to the onCreate trigger for all documents in a certain collection, by using a wildcard, and then mutate that document with an update call.
firestore
.document(`myCollection/{documentId}`)
.onCreate(event => {
const ref = event.data.ref
return ref.update({ some: "mutation"})
})
In the shell I run something like this, (passing some fake auth data required by my database permissions):
myFunction({some: "data"}, { auth: { variable: { uid: "jj5BpbX2PxU7fQn87z10d4Ks6oA3" } } } )
Hoever this results in an error, because the update tries to mutate a document that is not in the database.
Error: no entity to update
In the documentation about unit testing it is explained how you would create mocks for event.data in order to execute the function without touching the actual database.
However I am trying to invoke a real function which should operate on the database. A mock would not make sense, otherwise this is nothing more then a unit test.
I'm wondering what the strategy should be for invoking a function like this?
By using an existing id of a document the function can execute successfully, but this seems cumbersome because you need look it up in the database for every test, and it might not be there anymore at some point.
I think it would be very helpful if the shell would somehow create a new document from the data you pass in, and run the trigger from that. Would this be possible maybe, or is there another way?
The Cloud Functions emulator can only emulate events that could happen within your project. It doesn't emulate the actual change to the database that would have triggered it.
As you're discovering, when your function depends on that actual change previously occurring, you can run into problems. The fact of the matter is that it's entirely possible that the created document may have already been deleted by the time you're handling the event in the function (imagine a user acts quickly to delete, but the event is delayed for whatever reason).
All that said, perhaps you want to use set() with SetOptions that indicate you want to merge instead of overwrite. Bear in mind that if the document was previously deleted (with good reason) before the event triggered, you'll unconditionally recreate the document, which may not be what the user wanted.
I'm dealing with a problem where a user can update a document within a specified time limit, and if he doesn't, the server will.
The update involves incrementing a value and adding an object to an array of a document. I need to ensure that only one of the user/server updates the document. Not both.
To ensure this happens, some checks are run to see if the document has already been updated, but there are times where the user and server run at exactly the same time and both pass the checks and then the document is updated twice.
I've been trying many different ways of fixing this, but I haven't been able to. I tried implement a lock similar to this: http://en.wikipedia.org/wiki/Peterson%27s_algorithm to ensure that only one update will happen and the second update will fail, but I haven't been successful. Any ideas?
To ensure this happens, some checks are run to see if the document has already been updated, but there are times where the user and server run at exactly the same time and both pass the checks and then the document is updated twice.
You can achieve this by using a MongoDB update query that simultaneously checks if the value has been updated and updates it. Like this:
var post = Posts.findOne("ID");
// ... do some stuff with the post ...
Posts.update({counter: post.counter}, {$push: {items: newItem}, $inc: {counter: 1}});
As you can see, in one query we both check the counter and increment it - so if two of these queries run one right after another only one will actually update the document (since the counter won't match anymore).