如何为 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;
如果 updatedAt 或 createdAt 未在前端添加正确的日期和时间,则不允许更新/创建。这要便宜得多,因为它不需要数据函数,也不需要每次更新时额外写入。
不要使用常规的日期字段,请务必在前端添加时间戳:
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,包括对 createdAt
和 updatedAt
字段的更改,以无限循环结束强>....
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()
,以检测更改是否与不是createdAt
或updatedAt
的字段有关。
例如,假设您的 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.createTime
。 FieldValue.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 应用设置安全规则?