How to add timestamp to every collection insert,update in Cloud Functions for firestore database

后端 未结 6 437
心在旅途
心在旅途 2020-11-29 09:23

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 p

相关标签:
6条回答
  • 2020-11-29 10:05

    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....

    0 讨论(0)
  • 2020-11-29 10:08

    UPDATE 11/24/20 - I actually put the below function in my npm package adv-firestore-functions:

    See: https://fireblog.io/blog/post/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;
        });
    
    
    
    0 讨论(0)
  • 2020-11-29 10:21

    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

    0 讨论(0)
  • 2020-11-29 10:21
    const after = change.after.data();
    const before = change.before.data();
    const check = Object.keys(after).filter(key => (key !== 'createdAt') && (key !== 'updatedAt')).map(key => after[key] != before[key]);
    if (check.includes(true)) {
        return change.after.ref.set(
            {
                updatedAt: admin.firestore.FieldValue.serverTimestamp()
            },
            { merge: true }
        );
    } else {
        return false;
    }
    
    0 讨论(0)
  • 2020-11-29 10:21

    You do not need Cloud Functions to do that. It is much simpler (and cheaper) to set server timestamp in client code as follows:

    var timestamp = firebase.firestore.FieldValue.serverTimestamp()   
    post.createdAt = timestamp
    post.updatedAt = timestamp
    
    0 讨论(0)
  • 2020-11-29 10:29

    This solution supports first-level subcollections and is based off @Jonathan's answer above:

    **
     * writes fields common to root-level collection records that are generated by the
     * admin SDK (backend):
     * - createdAt (timestamp)
     * - updatedAt (timestamp)
     */
    exports.createCommonFields = functions.firestore
    .document('{colId}/{docId}')
    .onWrite(async (change, context) => {
        // the collections you want to trigger
        const setCols = ['posts', 'reviews', 'comments', ];
    
        // run the field creator if the document being touched belongs to a registered collection
        if (setCols.includes(context.params.colId)) {
            console.log(`collection ${context.params.colId} is not registered for this trigger`);
            return null;
        } else {
            console.log(`running createCommonFields() for collection: ${context.params.colId}`);
        }
    
        // cause the creation of timestamp fields only
        _createCommonFields(change);
    });
    
    /**
     * createCommonFields' equivalent for sub-collection records
     */
    exports.createCommonFieldsSubColl = functions.firestore
    .document('{colId}/{colDocId}/{subColId}/{subColDocId}')
    .onWrite(async (change, context) => {
        console.log(`collection: ${context.params.colId}, subcollection: ${context.params.subColId}`);
    
        // the subcollections of the collections you want to trigger
        // triggers for documents like 'posts/postId/versions/versionId, etc
        const setCols = {
            'posts': ['versions', 'tags', 'links', ], 
            'reviews': ['authors', 'versions'],
            'comments': ['upvotes', 'flags'],
        };
    
        // parse the collection and subcollection names of this document
        const colId = context.params.colId;
        const subColId = context.params.subColId;
        // check that the document being triggered belongs to a registered subcollection
        // e.g posts/versions; skip the field creation if it's not included
        if (setCols[colId] && setCols[colId].includes(subColId)) {
            console.log(`running createCommonFieldsSubColl() for this subcollection`);
        } else {
            console.log(`collection ${context.params.colId}/${context.params.subColId} is not registered for this trigger`);
            return null;
        }
    
        // cause the creation of timestamp fields
        _createCommonFields(change);
    });
    
    /**
     * performs actual creation of fields that are common to the
     * registered collection being written
     * @param {QueryDocumentSnapshot} change a snapshot for the collection being written
     */
    async function _createCommonFields(change) {
        // 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 = change.after.exists ? change.after.data() : null;
        const before = 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;
        }
    
        const currentTime = admin.firestore.FieldValue.serverTimestamp();
        // add createdAt
        if (createDoc) {
            return change.after.ref.set({
                createdAt: currentTime,
                updatedAt: currentTime,
            }, { merge: true })
            .catch((e) => {
                console.log(e);
                return false;
            });
        }
        // add updatedAt
        if (updateDoc && canUpdate()) {
            return change.after.ref.set({
                updatedAt: currentTime,
            }, { merge: true })
            .catch((e) => {
                console.log(e);
                return false;
            });
        }
        return null;
    }
    
    0 讨论(0)
提交回复
热议问题