简体   繁体   English

Nodejs Firebase 事务 - 如果条件失败,更新节点并发送错误

[英]Nodejs Firebase Transaction - Update a node and send error if a condition fails

The scenario is when a player clicks "join game" in an app, a cloud function is invoked to add the player to firebase database and the result is passed as response.该场景是当玩家在应用程序中点击“加入游戏”时,会调用云 function 将玩家添加到 firebase 数据库,并将结果作为响应传递。

Database structure:数据库结构:

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

The cloud function should check if the /players size is less than then /max_players and only if so, it should update the /players with the uid of the player and return the response.云 function 应该检查 /players 的大小是否小于 /max_players,只有这样,它才应该使用播放器的 uid 更新 /players 并返回响应。

Here is my cloud function:这是我的云 function:

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"
            };
        }

    });

});


Please see the comments in the above code, how can i send back a response when a condition fails?请参阅上面代码中的注释,当条件失败时如何发回响应?


Thanks @Doug and @Renaud for quick response, i still have few questions:感谢@Doug 和@Renaud 的快速回复,我还有几个问题:

Q1. Q1。 So i have to use所以我必须使用

 return gameIDRef.transaction(t => {

    }).then(result => {

    }).catch(error => {

    });

but when is this used?但是什么时候使用? is this the callback version that @Renaud is referring to?这是@Renaud 所指的回调版本吗?

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

Q2. Q2。 In the first way, when gameIDRef.transaction(t => {}) is called, what happens when there is no value with the game_id and when there is no value with game_id, i want to tell the user that there is no game with that specified id.第一种方式,当调用gameIDRef.transaction(t => {})时,当game_id没有值和game_id没有值时会发生什么,我想告诉用户没有游戏那个指定的 id。 How can i achieve that?我怎样才能做到这一点?

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

You are trying to access a function property of an undefined variable.您正在尝试访问undefined变量的 function 属性。 In this case, t does not have a property players (you are already checking that on an if statement above).在这种情况下, t没有属性players (您已经在上面的 if 语句中检查了它)。 But then, outside your if statement you are still accessing t.players.update , and since t.players is undefined, you get the error但是,在您的 if 语句之外,您仍在访问t.players.update ,并且由于 t.players 未定义,您会收到错误

There are several key aspects to adapt in your Cloud Function:在您的云 Function 中有几个关键方面需要适应:

  1. As explained in the Cloud Functions doc , you need to manage the asynchronous Firebase operations by using promises .如 Cloud Functions 文档中所述,您需要使用Promise 管理异步 Firebase 操作。 In your code, you use the callback "version" of the Transaction .在您的代码中,您使用Transaction的回调“版本”。 You should use the "promise" version, as shown below.您应该使用“promise”版本,如下所示。
  2. In the transaction, you need to return the new desired state you would like to write to the database node.在事务中,您需要返回您想要写入数据库节点的新的所需 state。 In a previous version of your question you were returning the object you intended to send back to the Cloud Function caller: this is not correct.在您的问题的先前版本中,您返回 object 您打算发送回云 Function 调用方:这是不正确的。
  3. You need to send data back to the client (ie the caller) when the promise returned by the transaction is resolved, as shown below (ie in .then(r => {..}) ).当事务返回的 promise 被解析时,您需要将数据发送回客户端(即调用方),如下图所示(即在.then(r => {..})中)。

Finally, to answer to your question at the bottom of your answer ("how can i send back a response when a condition fails?").最后,在答案底部回答您的问题(“当条件失败时,我如何发回响应?”)。 Just do return;只是return; as explained in the doc "if undefined is returned (ie you return with no arguments) the transaction will be aborted and the data at this location will not be modified".如文档中所述,“如果返回未定义(即您不带参数返回),则事务将被中止,并且不会修改此位置的数据”。

So, based on the above, you could think that the following should work.因此,基于上述内容,您可以认为以下内容应该有效。 However, you will rapidly see that the if (t === null) test does not work.但是,您很快就会发现if (t === null)测试不起作用。 This is because the first invocation of a transaction handler may return a null value, see the documentation , as well as the following posts: Firebase transaction api call current data is null and https://groups.google.com/forum/#!topic/firebase-talk/Lyb0KlPdQEo . This is because the first invocation of a transaction handler may return a null value, see the documentation , as well as the following posts: Firebase transaction api call current data is null and https://groups.google.com/forum/#!主题/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
        })
});

You will find a workaround in the SO post I referred above ( Firebase transaction api call current data is null ).您将在我上面提到的 SO 帖子中找到解决方法( Firebase 事务 api 调用当前数据为 null )。 I've never tested it, I'm interested by knowing how it worked for you, since this answer got a lot of votes.我从来没有测试过它,我很想知道它是如何为你工作的,因为这个答案得到了很多选票。


Another, more radical, workaround would be to use Firestore, instead of the Realtime Database.另一个更激进的解决方法是使用 Firestore,而不是实时数据库。 With Firestore Transactions , you can very well check in a document exists or not.使用 Firestore Transactions ,您可以很好地检查文档是否存在。


UPDATE Following the comments:更新在评论之后:

If you want to send back different responses to the Cloud Function caller, depending on what was checked in the transaction, use a variable as shown with the dummy following example.如果您想根据事务中检查的内容向 Cloud Function 调用方发回不同的响应,请使用如下虚拟示例所示的变量。

Do not re-execute the checks outside of the transaction (ie in the then() ): Only in the transaction you can be sure that if another client writes to the database node before your new value is successfully written, the update function (ie t ) will be called again with the new current value, and the write will be retried.不要在事务之外重新执行检查(即在then()中):只有在事务中你才能确定如果另一个客户端在你的新值成功写入之前写入数据库节点,更新 function (即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
    })

With callable functions, you have to return a promise that resolves with the data object to send to the client.使用可调用函数,您必须返回一个 promise,它使用数据 object 解析以发送到客户端。 You can't just return any promise from a promise chain.您不能只从 promise 链返回任何 promise。

After the transaction promise resolves, you will need to chain another then callback to it to return the value you want.在事务 promise 解决后,您需要链接另一个then回调它以返回您想要的值。 The general structure is:一般结构是:

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

Be sure to read the API docs for transaction .请务必阅读 API 文档以了解事务

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM