如何为 Firestore 数据库的 Cloud Functions 中的每个集合插入、更新添加时间戳

Posted

技术标签:

【中文标题】如何为 Firestore 数据库的 Cloud Functions 中的每个集合插入、更新添加时间戳【英文标题】:How to add timestamp to every collection insert,update in Cloud Functions for firestore database 【发布时间】:2019-03-10 16:28:34 【问题描述】:

我有一个名为 Posts 的 Firestore 集合 我在客户端做了一个插入,它可以工作。

我想使用 firebase 函数将 createdAt 和 updatedAt 字段添加到我的帖子集合 firestore 中的每个插入。

【问题讨论】:

“firebase 函数”是什么意思?您的意思是“Firebase 的云功能”firebase.google.com/docs/functions? 是的@RenaudTarnec 【参考方案1】:

21 年 1 月 31 日更新 - 虽然我相信我的包是很好的代码并回答了问题,但有一种更便宜的方法:firestore 规则

allow create: if request.time == request.resource.data.createdAt;
allow update: if request.time == request.resource.data.updatedAt;

如果 updatedAtcreatedAt 未在前端添加正确的日期和时间,则不允许更新/创建。这要便宜得多,因为它不需要数据函数,也不需要每次更新时额外写入。

不要使用常规的日期字段,请务必在前端添加时间戳

firebase.firestore.FieldValue.serverTimestamp;

2020 年 11 月 24 日更新 - 我实际上将以下函数放入我的 npm 包 adv-firestore-functions

见我的博文:https://fireblog.io/post/AhEld80Vf0FOn2t8MlZG/automatic-firestore-timestamps


我创建了一个通用云函数来使用 createdAt 和 updatedAt 时间戳更新您想要的任何文档:

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;
    );


【讨论】:

这是很棒的东西——你真的应该把它写在一篇 Medium 文章之类的文章中。我认为它唯一缺少的是包含子集合的能力。我会考虑一下这会如何发生? 完成了,您可以通过子收藏轻松做到这一点,我制作了两个通用子收藏...请参阅上面的我的网站链接 很好的解决方案@Jonathan。我已经继续并为子集合查询发布了一个解决方案 (***.com/a/64998774/1145905),但直到现在才看到你的更新 - 很酷的包! 这是一个绝妙的解决方案。 ? 我赞同@shankie_san,你应该写一篇 Medium 文章。许多教程和 SO 答案使用模式从客户端插入服务器时间戳。这是一个更简单且无故障的模式。 ?? 我确实在我的博客上写了一篇文章,但是,请参阅我的更新答案。谢谢!【参考方案2】:

要通过云函数将createdAt 时间戳添加到Post 记录,请执行以下操作:

exports.postsCreatedDate = functions.firestore
  .document('Posts/postId')
  .onCreate((snap, context) => 
    return snap.ref.set(
      
        createdAt: admin.firestore.FieldValue.serverTimestamp()
      ,
       merge: true 
    );
  );

要将modifiedAt 时间戳添加到现有Post,您可以使用以下代码。 但是,每次 Post 文档的字段发生变化时都会触发此 Cloud Function,包括对 createdAtupdatedAt 字段的更改,以无限循环结束强>....

exports.postsUpdatedDate = functions.firestore
  .document('Posts/postId')
  .onUpdate((change, context) => 
    return change.after.ref.set(
      
        updatedAt: admin.firestore.FieldValue.serverTimestamp()
      ,
       merge: true 
    );
  );

因此您需要比较文档的两种状态(即change.before.data()change.after.data(),以检测更改是否与不是createdAtupdatedAt 的字段有关。

例如,假设您的 Post 文档只包含一个字段 name(不考虑两个时间戳字段),您可以这样做:

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;
    
  );

换句话说,恐怕您必须逐个字段比较两个文档状态....

【讨论】:

非常感谢您的回答。我还没试过。我只想先使用插入选项。让我回到这个选项,因为我将它标记为答案。谢谢楼主 @Mustafa 你好,你有机会检查你是否可以接受这个答案吗? @renuad 不,我没有。 对于真正的文档创建时间戳,请改用createdAt: snap.createTimeFieldValue.serverTimestamp() 有一个问题,它根据onCreate 函数的调用设置时间戳,这可能会晚几百或几千毫秒。【参考方案3】:

这是我用来防止 Firebase Firestore 无限循环的方法。 与onUpdate 触发器相比,我更喜欢将逻辑放在onWrite 中 我使用 npm 包 fast-deep-equal 来比较传入数据和以前数据之间的变化。

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;
    
  );


希望对你有帮助

【讨论】:

【参考方案4】:
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;

【讨论】:

请在回答问题时尽量解释清楚。【参考方案5】:

此解决方案支持一级子集合,基于@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;
    

【讨论】:

【参考方案6】:

您不需要 Cloud Functions 来执行此操作。在客户端代码中设置服务器时间戳更简单(也更便宜),如下所示:

var timestamp = firebase.firestore.FieldValue.serverTimestamp()   
post.createdAt = timestamp
post.updatedAt = timestamp

【讨论】:

但是有人可以修改它,欺骗系统并伪造创建日期。 @DustinSilk 很想知道 Angular 应用程序是否真的存在这样的问题(我实际上主要开发移动应用程序)。那么你如何保护你的其他数据不被篡改,为什么你不能对这个时间戳做同样的事情呢?如果确实有人篡改了您的数据,那么拥有一个有效的时间戳可能并不会改善这种情况。 如果时间戳意味着您的内容在社区中被优先考虑,例如,就有动机能够更改它。这意味着前端的用户可以轻松拦截 http 调用并更改时间戳以符合自己的利益。保护其他数据取决于它是什么。如果它的用户创建了数据,通常他们无论如何都可以更改它,否则需要在服务器上进行检查以验证它。 为什么要使用 http 而不是 https?如果安全很重要,http 调用不是一个选项。 你没有抓住重点。不管是http还是https。无论使用哪一种,用户都可以编辑 javascript 并轻松更改时间戳。

以上是关于如何为 Firestore 数据库的 Cloud Functions 中的每个集合插入、更新添加时间戳的主要内容,如果未能解决你的问题,请参考以下文章

如何为使用 Cloud Firestore 的 Flutter 应用设置安全规则?

如何为 Firestore 创建大量示例数据?

新的 Firebase Firestore DocumentDb 如何为大型子集合建模

如何为 Firestore 文档设置到期日期 [重复]

如何为使用 Firestore 文档快照的模型编写测试

如何为从 Firestore 填充的 RecyclerView 实现过滤器?