How to add timestamp to every collection insert,update in Cloud Functions for firestore database
I have a firestore collection called Posts I make an insert on the client side and it works.
I want to add the createdAt and updatedAt fields to every insert in my posts collection firestore using firebase functions.
Solution 1:
UPDATE 1/31/21 - While I believe my package is great code and answers the question, there is a cheaper way of doing this: firestore rules:
allow create: if request.time == request.resource.data.createdAt;
allow update: if request.time == request.resource.data.updatedAt;
If the updatedAt or createdAt are not added on the front end with the correct date and time, it will not allow the update / create. This is much cheaper as it does not require a function for data, nor an extra write everytime you update something.
Do not use a regular date field, be sure to add the timestamp on the frontend via:
firebase.firestore.FieldValue.serverTimestamp;
UPDATE 11/24/20 - I actually put the below function in my npm package adv-firestore-functions:
See my blog article: https://fireblog.io/post/AhEld80Vf0FOn2t8MlZG/automatic-firestore-timestamps
I created a universal cloud function to update whatever documents you want with the createdAt and updatedAt timestamp:
exports.myFunction = functions.firestore
.document('{colId}/{docId}')
.onWrite(async (change, context) => {
// the collections you want to trigger
const setCols = ['posts', 'reviews','comments'];
// if not one of the set columns
if (setCols.indexOf(context.params.colId) === -1) {
return null;
}
// simplify event types
const createDoc = change.after.exists && !change.before.exists;
const updateDoc = change.before.exists && change.after.exists;
const deleteDoc = change.before.exists && !change.after.exists;
if (deleteDoc) {
return null;
}
// simplify input data
const after: any = change.after.exists ? change.after.data() : null;
const before: any = change.before.exists ? change.before.data() : null;
// prevent update loops from triggers
const canUpdate = () => {
// if update trigger
if (before.updatedAt && after.updatedAt) {
if (after.updatedAt._seconds !== before.updatedAt._seconds) {
return false;
}
}
// if create trigger
if (!before.createdAt && after.createdAt) {
return false;
}
return true;
}
// add createdAt
if (createDoc) {
return change.after.ref.set({
createdAt: admin.firestore.FieldValue.serverTimestamp()
}, { merge: true })
.catch((e: any) => {
console.log(e);
return false;
});
}
// add updatedAt
if (updateDoc && canUpdate()) {
return change.after.ref.set({
updatedAt: admin.firestore.FieldValue.serverTimestamp()
}, { merge: true })
.catch((e: any) => {
console.log(e);
return false;
});
}
return null;
});
Solution 2:
In order to add a createdAt
timestamp to a Post
record via a Cloud Function, do as follows:
exports.postsCreatedDate = functions.firestore
.document('Posts/{postId}')
.onCreate((snap, context) => {
return snap.ref.set(
{
createdAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
});
In order to add a modifiedAt
timestamp to an existing Post
you could use the following code. HOWEVER, this Cloud Function will be triggered each time a field of the Post document changes, including changes to the createdAt
and to the updatedAt
fields, ending with an infinite loop....
exports.postsUpdatedDate = functions.firestore
.document('Posts/{postId}')
.onUpdate((change, context) => {
return change.after.ref.set(
{
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
});
So you need to compare the two states of the document (i.e. change.before.data()
and change.after.data()
to detect if the change is concerning a field that is not createdAt
or updatedAt
.
For example, imagine your Post document only contains one field name
(not taking into account the two timestamp fields), you could do as follows:
exports.postsUpdatedDate = functions.firestore
.document('Posts/{postId}')
.onUpdate((change, context) => {
const newValue = change.after.data();
const previousValue = change.before.data();
if (newValue.name !== previousValue.name) {
return change.after.ref.set(
{
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
} else {
return false;
}
});
In other words, I'm afraid you have to compare the two document states field by field....
Solution 3:
This is what I have used to prevent the firebase firestore infinite loop.
I prefer to put the logic in a onWrite
compared to onUpdate
trigger
I use the npm package fast-deep-equal
to compare changes between incoming and previous data.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
const equal = require('fast-deep-equal/es6');
export const notificationUpdated = functions.firestore
.document('notifications/{notificationId}')
.onWrite((change, context) => {
// Get an object with the current document value.
// If the document does not exist, it has been deleted.
const document = change.after.exists ? change.after.data() : null;
// Get an object with the previous document value (for update or delete)
const oldDocument = change.before.data();
if (document && !change.before.exists) {
// This is a new document
return change.after.ref.set(
{
createdAt: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
} else if (document && change.before.exists) {
// This is an update
// Let's check if it's only the time that has changed.
// I'll do this by making updatedAt a constant, then use `fast-deep-equal` to compare the rest
const onlyTimeChanged = equal({ ...oldDocument, updatedAt: 0 }, { ...document, updatedAt: 0 });
console.log(`Only time changed? ${onlyTimeChanged}`);
if (onlyTimeChanged) {
// The document has just been updated.
// Prevents an infinite loop
console.log('Only time has changed. Aborting...');
return false;
}
return change.after.ref.set(
{
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
} else if (!document && change.before.exists) {
// This is a doc delete
// Log or handle it accordingly
return false;
} else {
return false;
}
});
Hope this helps