Cloud Functions - Preventing triggers on certain fields - javascript

I have an app that people can request food , then I have orders, each order have an status for that order so I made a function that will send a user notification each time the status of this order changes, the status can be changed by the store which take the order and by the user when the order is done.
But I want to limit the execution of this function to just 2 fields, the userId and the status fields, because now I have onUpdated, this trigger will always launch whenever an update is made to this document.
I'm planning to update other fields than uid and status for this document and I dont want the trigger to relaunch again and send a notification to the user if not needed.
Is there anyway to limit the trigger by just certain fields in the document?
exports.onOrderUpdated = functions.firestore
.document('orders/{orderId}').onUpdate((change, context) => {
var db = admin.firestore();
try{
const orderDataSnap = change.after.data();
var userId = orderDataSnap.uid;
var orderStatus = orderDataSnap.status;
}catch(error){
return handleErrorToUser(error);
}
Here I only want to execute this function only when userId and status changes in that document
Is there anyway to do this ?
Thanks

According to the documentation of Change, there are before and after snapshots.
You can call the data() method on each of these these and check if the userId and status are both equal in the before and after copies. If they are, just return out of the function early.

No there isn't any possibility to trigger a Cloud Function only if some specific fields of a document are changed. As explained by samdy1 in his answer you can detect, within the Cloud Function, which field(s) has(ve) changed but for that the Cloud Function needs to be triggered.
One solution would be to write a document to another dedicated collection, in parallel to the change.
For example, if you are updating the document with a new status, you write a doc to a collection statusUpdates with the ID of the parent order document and the status value, and you trigger a Cloud Function based on this document creation.
Of course, it implies a document creation and it has a cost (In addition to the CF triggering). It's up to you to do the math, depending on the frequency of the updates, to calculate if this approach will be cheaper than the approach consisting in triggering the Cloud Function for the order document for nothing.

Related

How to update a specific field of data only one time for each user in firesrore in firebase v9?

I am creating a sns-like web application, and trying to implement a "liking a post" function. Basically each post has data in firestore, such as the caption of the post, the image of the post, number of likes on the post and etc. What I want to do is, if a user clicks a like button on any post, the number of likes on the post clicked will be incremented by 1. However, I do not know how I can restricting each user can like only one time on each post. My current code works for incrementing the number of likes but the user can like a post as many times as the user clicks.
I am totally a beginner in firebase, react and javascript and would be really appreciated for any help. Thank you.
here is my code working for incrementing a number of likes.
const likeHandle = async () => {
const docRef = doc(db, "posts", postId);
await updateDoc(docRef, {
noLikes: noLikes + 1
});
}
You won't be able to do this at scale inside the same document where you are tracking the number of likes, because there is a max size to each document.
What you will have to do instead is store a record of each pair of user and like in its own document in a separate collection, and check to make sure that any new like requests are not already represented in that other collection. Your UI should also probably disable likes if there is a record for that pair of user and post so that the user is not presented with an option that you don't want them to take.
There is no direct way to limit how often a user can write a specific piece of data.
What you can do however is:
Add a usersWhoVoted field with UIDs to the document, or a userWhoVoted subcollection under it.
Write both the increased number of likes and the UID to the database in one operation (using a transaction/batched write if you chose a subcollection).
Use security rules that only allow the user to increment the count if they also added their UID in that operation, and only decrease the count if they removed it (if that is also a use-case you want to support).
You should probably also use the atomic increment operation, to prevent depending on client-side state that can be outdated (although your security rules should catch and reject that situation anyway).

Add exceptions on firebase cloud functions triggers

In a social app, I have a cloud function that updates a 'likes' counter whenever a post is liked, which is to say whenever the following reference is updated:
/likes/{postId}/{userid}
'countOfLikes' is written at the same level of the wildcard {userId} :
exports.countLikeChange = functions.database.ref('/likes/{postId}/{userid}').onWrite(event => {
const collectionRef = event.data.ref.parent;
var counterRef = collectionRef.child('countOfLikes');
return collectionRef.once('value').then(messagesData => counterRef.set(messagesData.numChildren() - 1));
});
With my current code, when a user like a post, the function is triggered first to update countOfLike, which in turn trigger the same function to do the same action...
Is there a way to specify an exclusion so that the function will not be triggered if {userId} == 'countOfLikes' ?
I know that I could use onCreate instead of onWrite, but you must know that in my app, one user could also remove his 'like'.
There is no way to exclude a specific child from triggering a function.
If you find that need, it typically means you've combined types of data that should be kept separate.
For example, I would expect your like counts to be in a separate node altogether, e.g. /likeCounts/$postId. If you structure it like that, updating the count for a new like won't retrigger the function.

Firebase trigger on all collection items while only one has been updated

Every time I do an update with the same object(s3) with same values and properties, Firebase trigger the event 'child_added' even if there's nothing to add or update.
I made some test by modifying on the firebase console some values in subcollection of the main object and noticed that it returns a snapshot with the first element correct and then all the other elements of the collections as 'ADDED' elements. This is not true because the collections didn't change except the one on which I performed an action.
I just need that when I send the same identical object that is stored on the db, firebase will recognize smartly that no action is requested and no trigger need to be activated.
var studentiRef = ref.child('studenti/' + s3.matricola);
studentiRef.update(JSON.parse(JSON.stringify(s3)));
studentiRef.on("child_changed", function(userSnapshot) {
var tasseRef = userSnapshot.ref.child('tasse');
tasseRef.on('child_added', function(itemSnapshot, prevKey){
console.log('ADDED ON');
console.log(itemSnapshot.key)
})
});
studentiRef.on("child_changed", function(userSnapshot) {
userSnapshot.ref.child('tasse').on('child_removed', function(itemSnapshot, prevKey){
console.log('REMOVED ON');
console.log(itemSnapshot.key)
})
});
studentiRef.on("child_changed", function(userSnapshot) {
userSnapshot.ref.child('tasse').on('child_changed', function(itemSnapshot, prevKey){
console.log('CHANGED ON');
console.log(itemSnapshot.key)
})
});
UPDATE:
Before posting update I made some experiments with no successful results.
Here the pics of the console, the database and the code.
Going nuts on this.
Here three screenshot: 1 firebase data 2 snippet 3 console log
UPDATE II:
scenario
behaviours on modifying value in firebase
SOLVED:
By getting inspired from the github firebase examples, I found out a common mistake in using firebase: i was not flatting the data.
To continue using my data structure (a root object within a list of objects), the solution was to trigger an update of every single object (pseudocode: ref.update(root/childobject) n-times instead of ref.update(root).
If someone else ran into this problem, I will explain better.
Always, FLAT YOUR DATA! (using firebase)
Most likely these events come directly from the client SDK, which doesn't detect if there was an actual change. The database server does perform such a check, and will only send out changes to other clients if there was an actual change.
Update:
The Firebase client + server behave in the following way when you're calling telling it to update a node to its current value.
The client fires the local event(s) to reflect the update. So child_changed will fire.
The client send the update to the server. This is needed since the client and server may be (slightly) out of sync, and the server is the single-source-of-truth.
The server compares the update with the current value of the node. If it is the same, the process stops here.
If the updated value is different from the current value and passes validation/permission checks, the data is committed to disk and broadcast to any active listeners.
If the updates value is different, but rejected by the validation/permission checks, the servers sends a rejection message to the original client, which then fires another child_changed event to revert the local change.

Firebase database on push get id

I am trying to imitate an insertion trigger on Firebase using the onWrite method. The insertion is done via POST requests since I am testing it (easiest way I found to check database triggers). The trigger includes writing the Firebase generated ID inside the inserted data as a new property.
My cloud function is this:
exports.onNewSeries = functions.database.ref('/series').onWrite(event => {
"use strict";
console.log(event.data.key);
console.log(event.data.current.key);
console.log(event.data.current);
});
Both first logs contain the same key (series), which actually is the key of the parent node where the new data is appended, instead of the new data key (in the quirky form of -adfaa123sdfasdf). The last log prints a Firebase structure containing the new data as well as the generated key in a _data property, however it is not accessible.
While this can be done manually after a request, I have not seen it automated in a database trigger way.
To get the generated key, make the function trigger on a specific child:
exports.onNewSeries = functions.database.ref('/series/{id}').onWrite(event => {
console.log(event.params.id);
});
Also see the Firebase documentation on handling database events.

How to prevent 'value' event on the client that issued set?

A Firebase client calling set() will cause all connected clients to have value triggered - including - the original client that issued the set().
In my case (and I think in most cases), there is no reason for the client that issued the set() to respond to the value event produced by its own call. Obviously its model is correct and there's no need to change it (which may be an expensive operation).
Is there any way for the client to not-receive/prevent/ignore the value event triggered by its own set() call ? I considered using off/on around set() but that can make the client miss value events that came at the same time but were not triggered by it.
Am I missing something obvious ?
Most applications treat the Firebase data itself as their model. So when there's an update, they call ref.set() (or another mutator function) and then the update flows back into their app through an on() event. React/Flux aficionados know this as a unidirectional data-flow, other might know it as Command Query Responsibility Segregation.
But there indeed cases where the model has already been updated and thus you want to ignore the event from Firebase if you're the one who triggered it.
There is no API for not receiving theses self-triggered events. Instead you'll have to "remember" the data that you sent to Firebase and filter it out in your on() handler.
The Android drawing sample from Firebase keeps a list of segments that it sends to Firebase and then ignores those segments in its onChildAdded handler. It uses push ids to identify the line segments and those are generated client-side, so it can use those to track identify the segments.
A JavaScript sample of this:
var pendingChildIds = []; // Push ids of nodes we've sent to the server, but haven't received in `on()` yet
// this code is in your UI event handler, or whatever triggers the needs to update your Firebase data
var newChild = ref.push();
pendingChildIds.push(newChild.key());
newChild.set(
{ property1: 'value1', property2: 3.14 },
function(error) {
// the write operation has completed, remove the child id from the list of pending writes
pendingChildIds.splice(pendingChildIds.indexOf(newChild.key());
}
);
// this is the event handler, using child_added in this case
ref.on('child_added', function(snapshot) {
if (!pendingChildIds.contains(snapshot.key())) {
// this is a child that we DIDN'T generate
}
});
I ended up adding a client ID to the model, something like:
var clientId=(Math.random()*10000000000000000).toFixed(0);
function set(data) {
ref.set(JSON.stringify({ clientId: clientId, data: data }));
}
ref.on('value', function(snapshot) {
var json=JSON.parse(snapshot.val());
if (!json || json.clientId===clientId) return;
var data=json.data;
// update model with data
});

Categories