如何限制可对 Cloud Firestore 中的特定集合执行的写入量?

How can I limit the amount of writes that can be done to a certain collection in Cloud Firestore?

我正在寻找一种方法来防止在给定时间段内向(子)集合写入超过给定限制的文档。

例如:Messenger A 每 24 小时不能写超过 1000 条消息。

这应该在 Firebase 函数 API 端点的上下文中完成,因为它由第三方调用。

端点

app.post('/message', async function(req:any, res:any) {
        // get the messenger's API key
        const key = req.query.key

        // if no key was provided, return error
        if(!key) {
            res.status(400).send('Please provide a valid key')
            return
        }

        // get the messenger by the provided key
        const getMessengerResult = await admin.firestore().collection('messengers')
            .where('key', '==', key).limit(1).get()

        // if there is no result the messenger is not valid, return error
        if (getMessengerResult.empty){
            res.status(400).send('Please provide a valid key')
            return
        }

        // get the messenger from the result
        const messenger = getMessengerResult.docs[0]

        // TODO: check if messenger hasn't reached limit of 1000 messages per 24 hours

        // get message info from the body
        const title:String = req.body.title
        const body: String = req.body.body

        // add message
        await messenger.ref.collection('messages').add({
            'title':title,
            'body':body,
            'timestamp': Timestamp.now()
        })

        // send response
        res.status(201).send('The notification has been created');
    })

我试过的一件事是用下面的代码代替 TODO::

// get the limit message and validate its timestamp
const limitMessageResult = await messenger.ref.collection('messages')
    .orderBy('timestamp',"desc").limit(1).offset(1000).get()

if(!limitMessageResult.empty) {
    const limitMessage = limitMessageResult.docs[0]
    const timestamp: Timestamp = limitMessage.data()['timestamp']

    // create a date object for 24 hours ago
    const 24HoursAgo = new Date()
    24HoursAgo.setDate(24HoursAgo.getDate()-1)

    if(24HoursAgo < timestamp.toDate()) {
        res.status(405).send('You\'ve exceeded your messages limit, please try again later!')
        return
    }
}

此代码有效,但有一个很大的 BUT。偏移量确实会跳过 1000 个结果,但 Firebase 仍会向您收费!因此,每次 Messenger 尝试添加 1 条消息时,都会读取 1000 多条消息……这很昂贵。

所以我需要更好(更便宜)的方法来做到这一点。

我想到但尚未尝试的一件事是向消息添加一个 index/counter 字段,每条消息增加 1。 然后而不是做:

const limitMessageResult = await messenger.ref.collection('messages')
    .orderBy('timestamp',"desc").limit(1).offset(1000).get()

我可以这样做:

const limitMessageResult = await messenger.ref.collection('messages')
    .where('index','==', currentIndex-1000).limit(1).get()

但我想知道这是否是一种保存方式。 例如,如果同时有多个请求会发生什么。 我首先需要从最后一条消息中获取当前索引并添加带有索引+1 的新消息。但是两个请求可以读取并写入相同的索引吗?或者这可以通过交易来处理吗?

或者是否有完全不同的方法来解决我的问题?

我强烈反对在我的服务器端代码中使用 offset(),正是因为它看起来像是在跳过文档,实际上是在读取并丢弃它们。

我能想到的实现每日最大写入次数的最简单方法是为每个 Messenger 保留一个每日写入计数器,然后在他们写消息时更新该计数器。

例如,您可以在每次写消息时执行以下操作:

await messenger.ref.collection('messages').add({
    'title':title,
    'body':body,
    'timestamp': Timestamp.now()
})
const today = new Date().toISOString().substring(0, 10); // e.g. "2020-04-11"
await messenger.ref.set({
  [today]: admin.firestore.FieldValue.increment(1)
}, { merge: true })

因此,这会在您的 Messenger 文档中为每一天添加一个额外的字段,然后它会记录 Messenger 当天所写消息的数量。

然后您将使用此计数代替当前的 limitMessageResult

const messageCount = (await messenger.get()).data()[today] || 0;
if (messageCount < 1000) {
  ... post the message and increase the counter
}
else {
  ... reject the message, and return a message
}

剩下的步骤:

  • 您需要保护对计数器字段的写入权限,因为信使不能自行修改这些字段。
  • 如果您担心 Messenger 的文档变得太大,您可能需要定期清除旧消息计数。我更喜欢保留这些类型的计数器,因为它们提供了一个机会,可以在需要时廉价地提供一些统计数据。