首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >如何使用Firestore实现幂等的可调用函数?

如何使用Firestore实现幂等的可调用函数?
EN

Stack Overflow用户
提问于 2021-05-30 17:44:50
回答 1查看 363关注 0票数 1

有时,我会从一个可调用的函数获得如下所示的复制文档:

代码语言:javascript
复制
const { default: Big } = require('big.js');
const { firestore } = require('firebase-admin');
const functions = require('firebase-functions');
const { createLog } = require('./utils/createLog');
const { payCart } = require('./utils/payCart');
const { unlockCart } = require('./utils/unlockCart');

exports.completeRechargedTransaction = functions.https.onCall(
  async (data, context) => {
    try {
      if (!context.auth) {
        throw new functions.https.HttpsError(
          'unauthenticated',
          'unauthenticated'
        );
      }

      const requiredProperties = [
        'foo',
        'bar',
        'etc'
      ];

      const isDataValid = requiredProperties.every(prop => {
        return Object.keys(data).includes(prop);
      });

      if (!isDataValid) {
        throw new functions.https.HttpsError(
          'failed-precondition',
          'failed-precondition'
        );
      }

      const transactionRef = firestore()
        .collection('transactions')
        .doc(data.transactionID);

      const userRef = firestore().collection('users').doc(data.paidBy.userID);

      let currentTransaction = null;

      await firestore().runTransaction(async transaction => {
        try {
          const transactionSnap = await transaction.get(transactionRef);

          if (!transactionSnap.exists) {
            throw new functions.https.HttpsError(
              'not-found',
              'not-found'
            );
          }

          const transactionData = transactionSnap.data();

          if (transactionData.status !== 'recharged') {
            throw new functions.https.HttpsError(
              'invalid-argument',
              'invalid-argument'
            );
          }

          if (transactionData.type !== 'recharge') {
            throw new functions.https.HttpsError(
              'invalid-argument',
              'invalid-argument'
            );
          }

          if (transactionData.paidBy === null) {
            throw new functions.https.HttpsError(
              'invalid-argument',
              'invalid-argument',
            );
          }

          const userSnap = await transaction.get(userRef);

          if (!userSnap.exists) {
            throw new functions.https.HttpsError(
              'not-found',
              'not-found',
            );
          }

          const userData = userSnap.data();

          const newUserPoints = new Big(userData.points).plus(data.points);

          if (!data.isGoldUser) {
            transaction.update(userRef, {
              points: parseFloat(newUserPoints.toFixed(2))
            });
          }

          currentTransaction = {
            ...data,
            remainingBalance: parseFloat(newUserPoints.toFixed(2)),
            status: 'completed'
          };

          transaction.update(transactionRef, currentTransaction);
        } catch (error) {
          console.error(error);
          throw error;
        }
      });

      const { paymentMethod } = data.rechargeDetails;

      let cashAmount = 0;

      if (paymentMethod && paymentMethod.paymentMethod === 'cash') {
        cashAmount = data.points;
      }

      let cartResponse = null;

      if (
        data.rechargeDetails.isProcessingCart &&
        Boolean(data.paidBy.userID) &&
        !data.isGoldUser
      ) {
        cartResponse = await payCart(context, data.paidBy.userID, cashAmount); 
        // This is the function that does all the writes and for some reason it is getting
        // called twice or thrice in some rare cases, and I'm pretty much sure that 
        // The Angular Client is only calling this function "completeRechargedTransaction " once.
      }

      await createLog({
        message: 'Success',
        createdAt: new Date(),
        type: 'activity',
        collectionName: 'transactions',
        callerID: context.auth.uid || null,
        docID: transactionRef.id
      });

      return {
        code: 200,
        message: 'Success',
        transaction: currentTransaction,
        cartResponse
      };
    } catch (error) {
      console.error(error);

      await unlockCart(data.paidBy.userID);

      await createLog({
        message: error.message,
        createdAt: new Date(),
        type: 'error',
        collectionName: 'transactions',
        callerID: context.auth.uid || null,
        docID: data.transactionID,
        errorSource:
          'completeRechargedTransaction'
      });

      throw error;
    }
  }
);

我正在阅读大量的firebase文档,但我无法找到在可调用函数上实现幂等性的解决方案,可调用函数中的上下文参数与后台函数和触发器非常不同,可调用的上下文如下所示:

.callablecontext

我确实找到了一个有用的blogpost来使用firebase触发器实现幂等性:

云函数专业提示:构建幂等函数

但我并不完全理解这种方法,因为我认为这是假设文档是在客户端(也就是前端应用程序)编写的,我并不认为这是一个很好的方法,因为它过于依赖客户机,我也担心安全性问题。

所以,是的,我想知道有一种方法可以在可调用函数上实现不可调用性,我需要一些类似于EventID的东西,但是对于可调用函数,可以安全地在我的应用程序和第三方apis (如stripe )上实现支付。

如果你能给我任何帮助或暗示,我将不胜感激。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2021-06-02 05:29:06

幂等函数的使用主要适用于响应诸如上传到云存储的文件添加到Firestore中的文档等事件的自动触发云函数。在这些情况下,事件触发要执行的函数,如果函数成功,一切正常。但是,如果该函数失败,它将被自动重试,从而导致在你链接的博客文章中讨论的问题。

对于用户触发的云函数( HTTPS事件可赎回云函数),不会自动重试。这些函数的调用方可以选择处理任何错误,以及客户端是否再次调用这些错误。

由于这些用户触发的函数仅由客户端代码执行,因此您应该检查以确保completeRechargedTransaction()不会被多次调用。测试它的一种方法是在调用函数之前为事件ID提供自己的值,如下所示:

代码语言:javascript
复制
// using a firebase push ID as a UUID
// could also use someFirestoreCollectionReference.doc().id or uuid()
const eventId = firebase.database.ref().push().key;

completeRechargedTransaction({
  eventId,
  /* ... other data ... */
})
  .then(console.log.bind(null, "Successfully completed recharged transaction:"))
  .catch(console.error.bind(null, "Failed to complete recharged transaction:"));

备注:客户端调用函数两次的最常见方式之一是重发器,您在其中更新状态以显示“加载”消息,然后再次调用该函数。作为一个反应的例子,您将确保数据库调用被包装在它自己的useEffect()调用中。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/67764159

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档