【问题标题】:Nodejs Firebase Transaction - Update a node and send error if a condition failsNodejs Firebase 事务 - 如果条件失败,则更新节点并发送错误
【发布时间】:2020-07-25 12:10:21
【问题描述】:

场景是当玩家在应用中点击“加入游戏”时,会调用云函数将玩家添加到 Firebase 数据库,并将结果作为响应传递。

数据库结构:

/games
|--/game_id
|----/players => this is a json array of uids and their status([{"uid1": true}, {"uid2":true},...])
|----/max_players = 10

云函数应该检查 /players 的大小是否小于 /max_players,如果是,它应该使用播放器的 uid 更新 /players 并返回响应。

这是我的云功能:

export const addUserToGame = functions.https.onCall((data, context) => {

    // Expected inputs - game_id(from data) and UID(from context)

    if (context.auth == null) {
        return {
            "status": 403,
            "message": "You are not authorized to access this feature"
        };
    }

    const uid = context.auth.uid;
    const game_id = data.game_id;

    let gameIDRef = gamesRef.child(game_id);

    return gameIDRef.transaction(function (t) {

        if (t === null) {
            // don't do anything, can i just do return;
        } else {

            let playersNode;
            let isAlreadyPlayer = false;
            if (t.players) { 
                console.log("players is not empty");

                playersNode = t.players;
                let players_count = playersNode.length;
                let max_players: Number = t.max_players;

                if (players_count >= max_players) {
                    // how can i return an error from here?
                }

                for (var i = 0; i < playersNode.length; i++) {
                    if (playersNode[i].uid === uid) {
                        isAlreadyPlayer = true;
                    }
                    break;
                }

                if (!isAlreadyPlayer) {
                    playersNode.push({
                        [uid]: "active"
                    });
                }
              
             t.players = playersNode;
             return t;

            } else {
                playersNode = [];
                playersNode.push({
                    [uid]: "active"
                });

                t.players = playersNode;
                return t;
            }

        }

    },function(error, committed, snapshot) {

        if(committed) {
            return {
                "status": 200,
                "message": "Player added successfully"
            };
        } else {
            return {
                "status": 403,
                "message": "Error adding player to the game"
            };
        }

    });

});


请看上面代码中的cmets,当条件失败时我如何发回响应?


感谢@Doug 和@Renaud 的快速回复,我还有几个问题:

第一季度。所以我必须使用

 return gameIDRef.transaction(t => {

    }).then(result => {

    }).catch(error => {

    });

但是什么时候使用呢?这是@Renaud 指的回调版本吗?

function (error, committed, snapshot) {
});

第二季度。第一种方式,当调用gameIDRef.transaction(t => {})时,当game_id没有值和game_id没有值时会发生什么,我想告诉用户没有游戏那个指定的 id。我怎样才能做到这一点?

return gameIDRef.transaction(t => {
      if(t === null)
        return; 
//Where does the control go from here? To then? 

// Also i have a conditional check in my logic, which is to make sure the numbers of players doesn't exceed the max_players before adding the player

t.players = where the existing players are stored
t.max_players = maximum number of players a game can have

if(t.players.length >= t.max_players)
// How do i send back a message to client saying that the game is full?
// Throw an error like this?
throw new functions.https.HttpsError('failed-condition', 'The game is already full');

    }).then(result => {

    }).catch(error => {
// and handle it here to send message to client?
    });

【问题讨论】:

    标签: javascript node.js firebase firebase-realtime-database google-cloud-functions


    【解决方案1】:

    您的云功能需要适应几个关键方面:

    1. 如 Cloud Functions doc 中所述,您需要使用 Promise 管理异步 Firebase 操作。在您的代码中,您使用Transaction 的回调“版本”。您应该使用“promise”版本,如下所示。
    2. 在事务中,您需要返回您想要写入数据库节点的新期望状态。在您的问题的先前版本中,您正在返回您打算发送回 Cloud Function 调用者的对象:这是不正确的。
    3. 当事务返回的promise被解析后,您需要将数据发送回客户端(即调用者),如下所示(即在.then(r =&gt; {..})中)。

    最后,在您的答案底部回答您的问题(“当条件失败时,我如何发回响应?”)。只需按照文档中的说明执行return;“如果返回未定义(即您不带参数返回),则事务将中止,并且不会修改此位置的数据”。

    因此,基于上述内容,您可以认为以下内容应该有效。但是,您很快就会发现if (t === null) 测试不起作用。这是因为第一次调用事务处理程序可能会返回 null 值,请参阅 documentation,以及以下帖子:Firebase transaction api call current data is nullhttps://groups.google.com/forum/#!topic/firebase-talk/Lyb0KlPdQEo

    export const addUserToGame = functions.https.onCall((data, context) => {
        // Expected inputs - game_id(from data) and UID(from context)
    
        // ...
    
        return gameIDRef
            .transaction(t => {
    
                if (t === null) {  // DOES NOT WORK...
                    return;
                } else {
                    // your "complex" logic
                    return { players: ..., max_players: xxx };
                }
            })
            .then(r => {
                return { response: 'OK' };
            })
            .catch(error => {
                // See https://firebase.google.com/docs/functions/callable?authuser=0#handle_errors
            })
    });
    

    您将在我上面提到的 SO 帖子 (Firebase transaction api call current data is null) 中找到解决方法。我从未测试过它,我很想知道它是如何为你工作的,因为这个答案得到了很多投票。


    另一个更激进的解决方法是使用 Firestore,而不是实时数据库。使用 Firestore Transactions,您可以很好地检查文档是否存在。


    更新在 cmets 之后:

    如果您想根据事务中检查的内容向 Cloud Function 调用者发回不同的响应,请使用以下虚拟示例所示的变量。

    不要在事务之外重新执行检查(即在then()):只有在事务中你才能确定如果另一个客户端在你的新值被成功写入之前写入数据库节点,更新函数(即t)将使用新的当前值再次调用,并重试写入。

    // ...
    let result = "updated";
    return gameIDRef
        .transaction(t => {
            console.log(t);
            if (t !== null && t.max_players > 20) {
                result = "not updated because t.max_players > 20"
                return;
            } else if (t !== null && t.max_players > 10) {
                result = "not updated because t.max_players > 10"
                return;
            }
            else {
                return { max_players: 3 };
            }
        })
        .then(r => {
            return { response: result };
        })
        .catch(error => {
            console.log(error);
            // See https://firebase.google.com/docs/functions/callable?authuser=0#handle_errors
        })
    

    【讨论】:

    • 谢谢,我已经编辑了我的疑问,你能再看一遍吗?
    • 在您的答案底部回答您的问题(“当条件失败时我如何发回响应?”)。只是返回; => 我知道值没有改变,但是要将消息发送回客户端,我应该在“then”中再次检查 /game_id 的值并返回消息吗?
    • 要在您返回 undefined 时发现这种情况,只需在 then() 中查看 committed 属性的值即可。它将是`false
    • 好吧,这很有道理。谢谢你。如果我想向用户发送条件响应,假设是否达到最大玩家限制,然后在 then() 中如果提交为假,那么将玩家数组与 max_players 进行比较并发送响应?应该这样写吗?
    • 接受您的指导并回答
    【解决方案2】:

    您正在尝试访问 undefined 变量的函数属性。 在这种情况下,t 没有属性 players(您已经在上面的 if 语句中检查了它)。 但是,在您的 if 语句之外,您仍在访问 t.players.update,并且由于 t.players 未定义,您会收到错误

    【讨论】:

    • 当 t.players 处于 else 状态时如何添加数据?有什么建议吗?
    【解决方案3】:

    使用可调用函数,您必须返回一个通过数据对象解析的 Promise 以发送给客户端。你不能只从承诺链中返回任何承诺。

    交易承诺解决后,您需要将另一个then 回调链接到它以返回您想要的值。大体结构是:

    return child.transaction(t => {
        return { some: "data" }
    })
    .then(someData => {
       // transform the result into something to send to the client
       const response = convertDataToResponse(someData)
       return response
    })
    .catch(err => {
       // Decide what you want to return in case of error
    })
    

    请务必阅读transaction 的 API 文档。

    【讨论】:

    • 谢谢你,我已经编辑了我的疑问,你能再看一遍吗?
    • 如果您还有其他问题,您应该将这些问题作为新问题发布。在 Stack Overflow 上“超载”包含许多问题和更新的单个帖子是不可接受的。
    • 当然可以.. 但似乎问题已解决。将接受答案或将发布另一个问题
    猜你喜欢
    • 2014-01-03
    • 1970-01-01
    • 2019-08-18
    • 2014-07-22
    • 1970-01-01
    • 2022-12-02
    • 1970-01-01
    • 1970-01-01
    • 2023-03-26
    相关资源
    最近更新 更多