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