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

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.

Related

I have a problem with autosaving data in VueJS, the autosave doesn't complete when I change the current note

so I have a problem this problem with my app. I'm not sure what is the right way to implement this autosaving feature. When you change the note's title or it's content, there is a debounce function that goes off in 2 seconds and if you change to the current note before the debounce update is complete it never updates. Let me know if I've done a poor job of explaining or if there is something that I need to clarify, Thanks!
Here's a video of what occurs: https://www.loom.com/share/ef5188eec3304b94b05960f403703429
And these are the important methods:
updateNoteTitle(e) {
this.noteData.title = e.target.innerText;
this.debouncedUpdate();
},
updateNoteContent(e) {
this.noteData.content = e;
this.debouncedUpdate();
},
debouncedUpdate: debounce(function () {
this.handleUpdateNote();
}, 2000),
async handleUpdateNote() {
this.state = "loading";
try {
await usersCollection
.doc(this.userId)
.collection("notes")
.doc(this.selectedNote.id)
.update(this.noteData)
.then(() => this.setStateToSaved());
} catch (error) {
this.state = "error";
this.error = error;
}
},
setStateToSaved() {
this.state = "saved";
},
Why running every two seconds ?
And an async wait is a really bad approach in a component.
To autosave the note I recommend that you add an eventListener on window closing or on changing the tab event, Like in the video provided (whatever event suits you best)
created () {
window.addEventListener('beforeunload', this.updateNote)
}
Where your updateNote function is not async.
But if you really want to save on each change.
You can make a computed property that looks like this:
note: {
get() {
return this.noteData.title;
},
set(value) {
this.noteData.title = value;
this.state= 'loading'
usersCollection.doc(this.userId)
.collection("notes")
.doc(this.selectedNote.id)
.update(this.noteData)
.then(() => this.setStateToSaved());
}
},
And the add v-model="note" to your input.
Imagine the user will type 10 characters a second That's 10 calls meaning 10 saves
EDIT:
Add a property called isSaved.
On Note change click if(isSaved === false) call your handleUpdateNote function.
updateNoteTitle(e) {
this.noteData.title = e.target.innerText;
this.isSaved = false;
this.debouncedUpdate();
}
and in your function setStateToSaved add this.isSaved = true ;
I don't know if your side bar is a different component or not.
If it is, and you are using $emit to handle the Note change, then use an event listener combined with the isSaved property.

Is there any way to get the data from node on "child_added" event using cloud-function of firebase?

I was using the query "OnUpdate" on each client to get the data from that node and calculate the children-count but it is too costly.
So I decided to use a cloud-function and create another node of children-count based on the node in which all the users exist but there is an issue, I'm unable to find any query like "OnChildAdded".
The available queries listed on firebase documentation are "OnUpdate", "OnDelete", "OnWrite" and "OnCreate" that are useless for this case because using "OnCreate" on child node cannot return me the children of parent node or "OnUpdate" on parent node will again become costly because all the users update their states frequently.
So what about "OnOperation"? Is there any use of it or is there any other way to reduce the cost of query and also create a children-count node?
Here is the structure of my database
{
currentGame: {
players: {
playerId: {...playerGameData},
//,
},
noOfPlayer: // this is what i wanted to create based on above players node children_count.
}
}
Here is the solution to the above problem in case anyone else need to solve a similar issue.
const PLAYER_REF = "currentGame/players/{playerId}";
const PLAYER_COUNT_NODE = "currentGame/noOfPlayers";
exports.incPlayersCount = functions.database.ref (PLAYER_REF).onCreate (async (snap) =>
{
const countRef = snap.ref.root.child (PLAYER_COUNT_NODE);
await countRef.transaction((current) => {
return (typeof current !== "number" || current < 0) ? 1 : current + 1;
});
return null;
});
exports.decPlayersCount = functions.database.ref (PLAYER_REF).onDelete (async (snap) =>
{
const countRef = snap.ref.root.child (PLAYER_COUNT_NODE);
await countRef.transaction((current) => {
return (typeof current !== "number" || current <= 0) ? 0 : current - 1;
});
return null;
});
btw - it is exactly similar to the sample code that #FrankvanPuffelen have shared in the above comments.

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

Firebase Functions onUpdate circular problem

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.

Meteor subscription is not stopping

I've got what should be a relatively simple issue. I set a session, then a subscribe to a collection using the string stored in the session. But when that session changes, I need to clear the subscription data and start again.
My code is as follows:
let subscriptionReady;
let filteredResults = [];
let rawResults = [];
let county = Session.get('county');
let type = Session.get('type');
This is mostly just prep work to create some empty objects to populate later. This all gets set on a click event. After we set these placeholder objects we go and subscribe by those sessions:
if (county && !type) {
return function() {
if (subscriptionReady) {
subscriptionReady.stop();
}
filteredResults = [];
rawResults = [];
subscriptionReady = Meteor.subscribe('resourcesearch', county, {
onReady: () => {
rawResults = resourceCollection.find({}, { sort: {score: -1} }).fetch();
rawResults.forEach((result) => {
if (result.score) {
filteredResults.push(result);
}
});
}
});
}
At the third line I run a check to see if subscriptionReady exists, then it will have the stop method available. So then I run it. But, it doesn't actually stop anything.
What am I missing?
After trial and error, I've got it solved. The issue was the placement of the stop call. I no longer have to check if subscriptionReady exists, instead I stop the subscription inside of the onReady method:
return function() {
filteredResults = [];
rawResults = [];
subscriptionReady = Meteor.subscribe('resourcesearch', county, {
onReady: () => {
rawResults = resourceCollection.find({}, { sort: {score: -1} }).fetch();
rawResults.forEach((result) => {
if (result.score) {
filteredResults.push(result);
}
});
subscriptionReady.stop();
}
});
It's .stop() not .stop docs
Also you can probably avoid your filtering loop by including score in your query. Are you looking for documents where the score key exists {score: {$exists: true}} or just where it is non zero {$score: {$ne: 0}}?
Also you shouldn't need to clear the subscription and start again. If you make your subscription parameter resourcesearch a reactive data source then the subscription will automatically update to give you the documents you need. Starting/stopping a subscription in response to a search would be an anti-pattern.

Resources