Firebase Functions onUpdate circular problem - firebase

I've this situation with a circular function, having trouble finding a solution.
Have a collection where I have a flag that tells if the data has changed. Also want to log the changes.
export async function landWrite(change, context) {
const newDocument = change.after.exists ? change.after.data() : null
const oldDocument = change.before.data()
const log = {
time: FieldValue.serverTimestamp(),
oldDocument: oldDocument,
newDocument: newDocument
}
const landid = change.after.id
const batch = db.batch()
const updated = newDocument && newDocument.updated === oldDocument.updated
if (!updated) {
const landRef = db.collection('land').doc(landid)
batch.update(landRef, {'updated': true })
}
const logRef = db.collection('land').doc(landid).collection('logs').doc()
batch.set(logRef, log)
return batch.commit()
.then(success => {
return true
})
.catch(error => {
return error
})
}
The problem is that this writes the log twice when the UPDATED flag is false.
But also cannot put the log write in the ELSE statement because the flag can already be UPDATED and a new document update be made so a new log has to be written.
Trigger:
import * as landFunctions from './lands/index'
export const landWrite = functions.firestore
.document('land/{land}')
.onWrite((change, context) => {
return landFunctions.landWrite(change, context)
})

If I understand correctly, the problem here is that the updated flag does not specify which event the update is in response to (as you can't really do this with a boolean). In other words - you may have multiple simultaneous "first-stage" writes to lands, and need a way to disambiguate them.
Here are a few possible options that I would try - from (IMHO) worst to best:
The first option is not very elegant to implement
The first and second options both result in your function being
called twice.
The third option means that your function is only
called once, however you must maintain a separate parallel
document/collection alongside lands.
Option 1
Save some sort of unique identifier in the updated field (e.g. a hash of the stringified JSON event - e.g. hash(JSON.stringify(oldDocument)), or a custom event ID [if you have one]).
Option 2
Try checking the updateMask property of the incoming event, and discard any write events that only affect that property.
Option 3
Store your update status in a different document path/collection (e.g. a landUpdates collection at the same level as your lands collection), and configure your Cloud Function to not trigger on that path. (If you need to, you can always create a second Cloud Function that does trigger on the landUpdates path and add either the same logic or different logic to it.)
Hope this helps!

The main problem here is the inability of differentiating changes that are made by this server function or by a client. Whenever you are in this situation, you should try to explicitly differentiate between them. You can even consider having an extra field like fromServer: true that goes with server's updates and helps the server ignore the related trigger. Having said that, I think I have identified the issue and provided a clear solution below.
This line is misleading:
const updated = newDocument && newDocument.updated === oldDocument.updated
It should be named:
const updateStatusDidNotChange = newDocument && newDocument.updated === oldDocument.updated
I understand that you want the updated flag to be managed by this function, not the client. Let me know if this is not the case.
Therefore, the update field is only changed in this function. Since you want to log only changes made outside of this function, you want to log only when updated did not change.
Here's my attempt at fixing your code in this light:
export async function landWrite(change, context) {
const newDocument = change.after.exists ? change.after.data() : null
const oldDocument = change.before.data()
const updateStatusDidNotChange = newDocument && newDocument.updated === oldDocument.updated
if (!updateStatusDidNotChange) return true; //this was a change made by me, ignore
const batch = db.batch()
if (!oldDocument.updated) {
const landid = change.after.id
const landRef = db.collection('land').doc(landid)
batch.update(landRef, {'updated': true })
}
const log = {
time: FieldValue.serverTimestamp(),
oldDocument: oldDocument,
newDocument: newDocument
}
const logRef = db.collection('land').doc(landid).collection('logs').doc()
batch.set(logRef, log)
return batch.commit()
.then(success => {
return true
})
.catch(error => {
return error
})
}
Edit
I had the exact problem and I had to differentiate changes by the server and the client, and ignore the ones that were from the server. I hope you give my suggestion a try.

Related

Firebase Firestore Emulator Returning Data After Deletion?

I'm working on a React web app and I noticed some weird/unusual behavior when using the Firestore emulator...
I have an onUpdate trigger that is pretty basic:
export const onUsernameUpdateFn = functions.firestore
.document("users/{userId}")
.onUpdate(async (change, context) => {
// Retrieve the current and previous value
const newData = change.after.data();
const previousData = change.before.data();
const operation = "username" in previousData ? "update" : "create";
console.log(
"newData",
newData,
"previousData",
previousData,
"operation",
operation
);
// We'll won't update if the username hasn't changed
if (newData.username === previousData.username) {
console.log("Skipping because username hasn't changed");
return null;
}
// Rest of the things
If I delete all data using the Emulator Suite (http://localhost:4000/firestore) and trigger the above code again, change.before.data() still has data in it...
functions: Beginning execution of "onUsernameUpdate"
> newData {
> createdAt: Timestamp { _seconds: 1652132412, _nanoseconds: 273000000 },
> username: 'differentUsername'
> } previousData {
> createdAt: Timestamp { _seconds: 1652132412, _nanoseconds: 273000000 },
> username: 'originalUsername'
> } operation update
... even though the Firestore UI shows it is empty (correctly).
If I wait some time (20-30 mins or more, haven't measured) after the deletion, then it works as expected and the operation becomes "create" because new and previous values are not equal.
I checked and I am not running multiple emulator instances and the correct project is selected.
Is there some caching in Firestore Emulator? Is it safe to assume that when deployed, this will not be an issue?
Or... is there a better way to skip the update when the username hasn't changed, but make sure it executes when the username property gets written for the first time?
Note: When it works "correctly" after waiting some time, the username property is not present, which is what I expect.

How to correctly return array in redux state, if the array did not have to be updated in the reducer?

I am using the aurelia-store state management library for managing state. This question is not specific to Aurelia store, but actually to redux best practices in general since Aurelia store is very much the same thing.
I have an action that fetches unit updates from an API like so:
export const fetchNewUnits = async (state: State): Promise<State> => {
const fetchedUnits = await apiClient.getUnitsMarkers();
// no new updates so don't trigger change in units
// IS THIS ACCEPTABLE?
if (fetchedUnits.length === 0) {
return {
...state,
highwaterMark: new Date()
};
}
const units: UnitMarker[] = state.units.slice();
_.forEach(fetchedUnits, (newUnit) => {
// look for matching unit in store
const idx = _.findIndex(units, {
imei: newUnit.imei
});
// unit was found in store, do update
if (idx !== -1) {
// replace the unit in the store
const replacement = new UnitMarker({...newUnit});
units.splice(idx, 1, replacement);
}
});
// OR SHOULD I ALWAYS DEEP COPY THE ARRAY REFERENCE AND IT'S OBJECTS
return {
...state,
highwaterMark: new Date(),
units: [...units]
};
};
If I do not have any unit changes (i.e. my store is up to date) can I simply return the state with the spread operator as shown in the first return statement? Is this fine since I did not modify the objects?
Or do I always have to do deep replacements such as:
return {
...state,
highwaterMark: new Date(),
units: [...state.units]
};
even if the objects in the array did not change?
The reason why you’re supposed to create a new object is because React components check for prop changes in order to know when to re-render.
If you simply modify an object and pass it in as a prop again, React won’t know that something changed and will fail to rerender.
So in your case, the question is: do you want to rerender, or not? If you don’t, returning the same object is fine and a simple ‘return state’ will let React know that no rerenders are necessary.
See: Why is the requirement to always return new object with new internal references

Firestore get value of Field.increment after update without reading the document data

Is there a way to retrieve the updated value of a document field updated using firestore.FieldValue.increment without asking for the document?
var countersRef = db.collection('system').doc('counters');
await countersRef.update({
nextOrderCode: firebase.firestore.FieldValue.increment(1)
});
// Get the updated nextOrderCode without asking for the document data?
This is not cost related, but for reliability. For example if I want to create a code that increases for each order, there is no guaranty that if >= 2 orders happen at the same time, will have different codes if I read the incremental value right after the doc update resolves, because if >= 2 writes happen before the first read, then at least 2 docs will have the same code even if the nextOrderCode will have proper advance increment.
Update
Possible now, check other answer.
It's not possible. You will have to read the document after the update if you want to know the value.
If you need to control the value of the number to prevent it from being invalid, you will have to use a transaction instead to make sure that the increment will not write an invalid value. FieldValue.increment() would not be a good choice for this case.
We can do it by using Firestore Transactions, like incremental worked before Field.increment feature:
try {
const orderCodesRef = admin.firestore().doc('system/counters/order/codes');
let orderCode = null;
await admin.firestore().runTransaction(async transaction => {
const orderCodesDoc = await transaction.get(orderCodesRef);
if(!orderCodesDoc.exists) {
throw { reason: 'no-order-codes-doc' };
}
let { next } = orderCodesDoc.data();
orderCode = next++;
transaction.update(orderCodesRef, { next });
});
if(orderCode !== null) {
newOrder.code = orderCode;
const orderRef = await admin.firestore().collection('orders').add(newOrder);
return success({ orderId: orderRef.id });
} else {
return fail('no-order-code-result');
}
} catch(error) {
console.error('commitOrder::ERROR', error);
throw errors.CantWriteDatabase({ error });
}
Had the same question and looks like Firestore Python client
doc_ref.update() returns WriteResult that has transform_results attribute with the updated field value

How can I keep track of the last updated at time for a document in Firestore?

I want to have an updatedAt field in my pizza document that should be updated every time there's an update happening in this particular document. I think the best place to handle this is in an onUpdate trigger:
exports.onUpdatePizza = functions.firestore
.document('pizzas/{pizzaId}')
.onUpdate(async (change, context) => {
return change.after.ref.update({ updatedAt: new Date() });
});
However, the above code will fall into an infinite loop. How can I implement this without the undesired side effect?
There is a good explanation there https://medium.com/#krngd2/prevent-infinity-loop-in-firebase-cloud-functions-ea8083afbd35
Add this inside the function:
// simply input data
const after: any = change.after.exists ? change.after.data() : null;
const before: any = change.before.exists ? change.before.data() : null;
const canUpdate = () => {
// if Update Trigger
if (before.updatedAt && after.updatedAt) {
if (after.updatedAt._seconds !== before.updatedAt._seconds) {
return false;
}
}
// if Create Trigger <--- assuming you use it
if (!before.createdAt && after.createdAt) {
return false;
}
return true;
}
if (canUpdate()) {
// update code here
}
Here is my code for universal updates: https://stackoverflow.com/a/60963531/271450
In your function, check to see if the update date is within some threshhold of the current time before updating it again. This will defend against unwanted writes, but the downside is that the update time may lag by that threshold, if there are very frequent updates.

Error occurred while parsing your function triggers

The following error is shown while deploying firebase function.
I tried initializing the firebase functions.
I also double-checked the index.js file.
I'm new to deploying firebase functions so please help me for the same.
index.js is as follows:
const functions = require('firebase-functions');
// replaces keywords with emoji in the "text" key of messages
// pushed to /messages
exports.emojify =
functions.database.ref('/messages/{pushId}/text')
.onWrite(event => {
// Database write events include new, modified, or deleted
// database nodes. All three types of events at the specific
// database path trigger this cloud function.
// For this function we only want to emojify new database nodes,
// so we'll first check to exit out of the function early if
// this isn't a new message.
// !event.data.val() is a deleted event
// event.data.previous.val() is a modified event
if (!event.data.val() || event.data.previous.val()) {
console.log("not a new write event");
return;
}
// Now we begin the emoji transformation
console.log("emojifying!");
// Get the value from the 'text' key of the message
const originalText = event.data.val();
const emojifiedText = emojifyText(originalText);
// Return a JavaScript Promise to update the database node
return event.data.ref.set(emojifiedText);
});
// Returns text with keywords replaced by emoji
// Replacing with the regular expression /.../ig does a case-insensitive
// search (i flag) for all occurrences (g flag) in the string
function emojifyText(text) {
var emojifiedText = text;
emojifiedText = emojifiedText.replace(/\blol\b/ig, "😂");
emojifiedText = emojifiedText.replace(/\bcat\b/ig, "😸");
return emojifiedText;
}
Please check the current documentation on triggers, and specifically on migration from Beta to Version 1.0 .
event.data.previous.val() has changed to change.before.val()
event.data.val() has changed to change.after.val()
Also, the Promise statement changes to:
return change.after.ref.parent.child('text').set(emojifiedText);
The complete index.js looks like:
const functions = require('firebase-functions');
// replaces keywords with emoji in the "text" key of messages
// pushed to /messages
exports.emojify=
functions.database.ref('/messages/{pushId}/text')
.onWrite((change,context)=>{
// Database write events include new, modified, or deleted
// database nodes. All three types of events at the specific
// database path trigger this cloud function.
// For this function we only want to emojify new database nodes,
// so we'll first check to exit out of the function early if
// this isn't a new message.
// Only edit data when it is first created.
if (change.before.exists()){
return null;
}
// Exit when the data is deleted.
if (!change.after.exists()){
return null;
}
// Now we begin the emoji transformation
console.log("emojifying!");
//Get the value from the 'text' key of the message
const originalText = change.after.val();
const emojifiedText = emojifyText(originalText);
//Return a JavaScript Promise to update the database nodeName
return change.after.ref.parent.child('text').set(emojifiedText);
});
// Returns text with keywords replaced by emoji
// Replacing with the regular expression /.../ig does a case-insensitive
// search (i flag) for all occurrences (g flag) in the string
function emojifyText(text){
var emojifiedText=text;
emojifiedText=emojifiedText.replace(/\blol\b/ig,"😂");
emojifiedText=emojifiedText.replace(/\bcat\b/ig,"😸");
return emojifiedText;
}

Resources