简体   繁体   中英

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.

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.

Here is my cloud 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:

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?

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

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. 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. In this case, t does not have a property players (you are already checking that on an if statement above). But then, outside your if statement you are still accessing t.players.update , and since t.players is undefined, you get the error

There are several key aspects to adapt in your Cloud Function:

  1. As explained in the Cloud Functions doc , you need to manage the asynchronous Firebase operations by using promises . In your code, you use the callback "version" of the Transaction . You should use the "promise" version, as shown below.
  2. In the transaction, you need to return the new desired state you would like to write to the database node. 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.
  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 => {..}) ).

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; 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. 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 .

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 ). 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. With Firestore Transactions , you can very well check in a document exists or not.


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.

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.

// ...
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. You can't just return any promise from a promise chain.

After the transaction promise resolves, you will need to chain another then callback to it to return the value you want. 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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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