Correctly updating the same object on create trigger in firebase realtime DB trigger - firebase

I have a firebase realtime database trigger on a create node. my need is to update a property based on some condition in the create trigger for the same object. The way i am doing currently is below:
exports.on_order_received_validate_doodle_cash_order = functions.database.ref("/orders/{id}")
.onCreate((change, context) => {
console.log("start of on_order_received_deduct_doodle_cash")
const orderId = context.params.id
const order = change.val();
var db = admin.database();
const orderRef = db.ref('orders/')
return orderRef.child(orderId).update({"_verifiedOrder": true})
})
As you can see i am getting order id from context and then querying object again and updating it. My question is do i need to do this circus or can i just update it without querying again?

Generally it looks good. Just some small feedback to make you feel more confident about being on the right track.
Call the parameter snapshot instead of change because the parameter name change only make sense for the onUpdate event trigger.
You do not need to log that you're entering the function. Because entering and leaving the function is automatically logged by Firebase also.
You can remove the order variable that is unused.
You are actually not "querying" the object again. Making a reference to a node in the database doesn't make any network call itself. Not until you subscribe to receiving data. So doing orderRef.child(orderId) is not a query, it's just a database reference.
You can use the snapshot's own reference attribute to shorten your code a bit... effectively throwing away almost all code :-)
So your code code look like this instead. It is doing the exact same thing, just shorter. It was also correct from the beginning.
exports.on_order_received_validate_doodle_cash_order = functions
.database
.ref("/orders/{id}")
.onCreate((snapshot) => {
return snapshot.ref.child("_verifiedOrder").set(true);
});
But as mentioned in my comment above, you are effectively just setting a flag that is confirming that data was saved (or rather: confirming that the function was triggered). You might want to add some logic in there to check whether the order can be placed or not and then set the verified flag to true or false depending on that. Because with the logic of the implementation, all orders will have the value _verifiedOrder set to true, which is a waste of storage in your database.

Related

Firebase cloud functions: How to wait for a document to be created and update it afterwards

Here is the situation:
I have collections 'lists', 'stats', and 'posts'.
From frontend, there is a scenario where the user uploads a content. The frontend function creates a document under 'lists', and after the document is created, it creates another document under 'posts'.
I have a CF that listens to creation of a document under 'lists' and create a new document under 'stats'.
I have a CF that listens to creation of a document under 'posts' and update the document created under 'stats'.
The intended order of things to happen is 2->3->4. However, apparently, step 4 is triggered before step 3, and so there is no relevant document under 'stats' to update, thus throwing an error.
Is there a way to make the function wait for the document creation under 'stats' and update only after it is created? I thought about using setTimeout() for the function in step 4, but guess there might be a better way.
Below is the code that I am using for steps 3 and 4. Can someone advise? Thanks!
//This listens to a creation of a document under 'lists' and creates a new document
//with the same document ID under 'stats'.
exports.statsCreate = functions.firestore
.document('lists/{listid}').onCreate((snap,context)=>{
const listidpath=snap.ref.path;
const pathfinder=listidpath.split('/');
const listid=pathfinder[pathfinder.length-1];
return db.collection('stats').doc(listid).set({
postcount:0,
})
})
//This listens to a creation of a document under 'posts' and updates the corresponding
// document under 'stats'. There is a field under 'posts' with the list ID to make this possible.
// How do I make sure the update operation happens only after the document is actually there?
exports.statsUpdate = functions.firestore
.document('posts/{postid}').onCreate((snap,context)=>{
const data=snap.data();
return db.collection('stats').doc(data.listid).update({
postcount:admin.firestore.FieldValue.increment(1)
})
})
I can see at least two "easy" solutions:
Solution #1: In your front end, set a listener to the to-be-created stat document (with onSnapshot()), and only create the post document when the stat one has been created. Note however that this solution will not work if the user does not have read access right to the posts collection.
Solution #2: Use the "retry on failure" option for background Cloud Functions. Within your statsUpdate Cloud Function you intentionally throw an exception if the stat doc is not found => The CF will be retried until the stat doc is created.
A third solution would be to use a Callable Cloud Function, called from your front-end. This Callable Cloud Function would write the three docs in the following order: list, stat and post. Then the statsUpdate Cloud Function would be triggered in the background (or you could include its business logic in the Callable Cloud Function as well).
One of the drawbacks of this solution is that the Cloud Function may encounter some cold start effect. In this case, from an end-user perspective, the process may take more time than the abonne solutions. However note that you can specify a minimum number of container instances to be kept warm and ready to serve requests.
PS: Note that in the statsCreate CF, you don't need to extract the listid with:
const listidpath=snap.ref.path;
const pathfinder=listidpath.split('/');
const listid=pathfinder[pathfinder.length-1];
Just do:
const listid = context.params.listid;
The context parameter provides information about the Cloud Function's execution.

Why does Firestore send all results, instead of just the new items when using on_snapshot?

I'm using Firebase's Firestore to store and publish new events.
In the code below, I'm subscribing to a collection and want to be notified when a new items is added (this code is executing on a browser).
When I first connect, I would like to receive a true snapshot. However, once I'm connected to Firestore and have received an initial snapshot, with each new item, I only want to get the udpates, not the whole collection over and over again!
function queryExercise(exercise){
db.collection("exercises").where("exercise","==",exercise).onSnapshot(function(querySnapshot){
querySnapshot.forEach(function(doc){
var d_ = doc.data()
console.log(d_);
...do somethign with d_...
})
})
}
When I publish a new item to the collection, my console is full of all events received earlier...in other words, it is sending me the full snapshot, instead of just the deltas.
Am I doing something wrong or does the API really not support delta updates?
Looks like I needed to read on docChanges:
function queryExercise(exercise){
db.collection("exercises").where("exercise","==",exercise).onSnapshot(function(querySnapshot){
// \/-----this thing
querySnapshot.docChanges().forEach(function(change){
var d_ = change.doc.data()
console.log("Change type:", change.type, d_);
...
});
})
}
From https://firebase.google.com/docs/firestore/query-data/listen
That's the way Firestore queries work. If you don't provide a filter for which documents you want in a collection, you will get all the documents in that collection. The only way to change this behavior is to provide a filter in your query using a where clause.
It sounds like you have a thought in mind about what makes for a "new" document in your collection. You will need to represent that using some field in the documents in your collection. Usually this will be a timestamp type field that's added or modified whenever a document is created or changed. This will be part of your where clause that determines what's "new". Use this field as a filter to find out what's new.

Why does querying Firebase realtime db require listener?

I am new to firebase and noSQL databases. I've read the docs and watched instructional videos but there is one concept I haven't fully grasped.
Why is it that querying the database requires a listener such as .on("value")?
Since these listeners are triggered whenever there is a change of sorts (nodes created, edited, children created) shouldn't there be a more direct way of getting the data from the db? Such as
ref.orderBy("age"). equalTo(30).get()
A method to just get what's in there at the time he instruction is executed, without having to listen to some sort of event?
In SQL it's not like you have to wait for something to change in your db to make this query work:
SELECT * FROM TABLE WHERE X == Y
PS: I know .once() also exists, but my question is more about: if my db never changed, how would I be able to query it and always get the same query result/snapshot?
You didn't define a platform so I will use this Swift pseudo-code example. Suppose we want to get a list of all users, one time.
let usersRef = my_firebase.child("users")
usersRef.observeSingleEvent(by: .value, with: { snapshot in
//all users are returned to the app in the snapshot
// .value means 'give me everything in the specified node'
// so then we can iterate over the snapshot to get each users data
}
You can call the above code any time to get the list of users and it does not add an observer (listener) to the node. It's a one-shot.
Whereas, if we want to be notified of users that are added
let usersRef = my_firebase.child("users")
usersRef.observe(by: .childAdded, with: { snapshot in
//upon first calling this, each user will be sent to the app in the snapshot
// and after that, any user that's added
}
The above code attaches an observer (listener) to the users node and whenever a user is added, it's provided to the app via the snapshot.
Note the use of .observeSingleEvent vs .observe and .value to get everything in the node vs .childAdded to get a specific node.

Firebase cloud function use current value and not value when function was initially called

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.

How to trigger onCreate in Firestore cloud functions shell without using an existing document

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.

Resources