How to do Partial Queries and Updates to a Complex JSON Document

There are times where you want to maintain a complex JSON document in a Mongo collection that you want to use for storing state.

Important! If you're working on a new game that was created after the Game Data Service was launched in January 2018, you won't be able to create new Mongo Runtime collections and the Cloud Code used in the tutorial will not work. For the alternative tutorial for new games using the Game Data Service, see Submitting JSON Document Queries Using Game Data Service.

In this example:

A concrete example of this sort of case might be where you are storing custom data about every level the player has ever played:

    {
      "_id": "playerid",
      "LEVEL_1": {
        "checkpoint": 5,
        "complete": 1
      },
      "LEVEL_2": {
        "checkpoint": 3,
        "complete": 0
      },
      "LEVEL_3": {
        "checkpoint": 0
      }
    }


More Information! For further details and recommendations on how to use MongoDB collections to store and manage your game data, see the Managing Data Persistence tutorial.

Configuring the Game

Creating the Event for Updating the Document.

We're creating a generic Event for updating a document. This Event will contain the path of the document we want to update and the value we want to set at that path. If this sounds a little complex, bear with us and work through the example - it should all become clear by the time you've work through the example!

We'll add a new Event in the portal that looks like this:

Creating the Script to Process the Update Event

We now need to bind a Cloud Code script to this Event to access the data and make the document updates. This script uses a little bit of Mongo magic to create the document if it doesn't already exist, and sets the value you want at the path you ask for:

     var progressCollection = Spark.runtimeCollection("player_progress");

    //Get the value that was passed in for PATH
    var inputPath = Spark.data.PATH;
    //Get the value that was passed in for VAL
    var inputValue = Spark.data.VAL;

    //We need to create JSON object using a string as a key
    //We do this using the [] notation
    var updateObject = {};
    updateObject[inputPath] = inputValue;

    //Now do the mongo update.
    progressCollection.update(
      {"_id": Spark.getPlayer().getPlayerId()}, //Looks for a doc with the _id of the current player
      {"$set" : updateObject}, // Uses the $set Mongo modifier to set value at a path
      true, // Create the document if it does not exist (upsert)
      false // This query will only affect a single object (multi)
    );

Creating the Event for Querying the Document

We'll use the same pattern here for querying the document. For the query Event only one attribute is required - the path of the document you want to retrieve.

We'll add a new Event in the portal that looks like this:

Creating the Script to Process the Query Event

This script is a little more involved. The basic Mongo find operators don't support getting a partial document. However, the aggregation framework does:

    var progressCollection = Spark.runtimeCollection("player_progress");

    //Get the value that was passed in for PATH
    var inputPath = Spark.data.PATH;

    if(inputPath.length == 0){
      //No input path, we want the whole document
      var resultDocument = progressCollection.find({"_id" : Spark.getPlayer().getPlayerId()});
      //Put the results into the response.
      Spark.setScriptData("data", resultDocument);
    } else {
      //The query object for the aggregation
      var matchObject = {"$match" : {"_id": Spark.getPlayer().getPlayerId()}};

      //We want to use the last part of the path as the key in the returned data
      //We don't have to do it, but the result will make more sense
      var pathArray = inputPath.split(".");
      var lastPathString = pathArray[pathArray.length - 1];

      //Now we build up the object the spacified the fields we want to return  
      var projectData = {"_id":0 };
      //Using [] notation so we can use a variable as a JSON attribute name
      projectData[lastPathString] = "$"+inputPath;
      var projectObject = {"$project" : projectData};

      //Run the aggregation
      var partialResult = progressCollection.aggregate(matchObject, projectObject);

      //Put the results into the response.
      Spark.setScriptData("data", partialResult);
    }

Execution of the Configuration

Register a User to Use for the Tests

    {
        "@class": ".RegistrationRequest",
        "userName": "gamesparks",
        "password": "gspwd",
        "displayName": "GameSparks Test",
        "requestId": "1392894961229"
    }

    {
        "@class": ".RegistrationResponse",
        "authToken": "...",
        "displayName": "GameSparks Test",
        "requestId": "1392894961229",
        "userId": "5305e40f1c26ac2a6576c389"
    }

Start Playing Level 1

We'll first check if the user has any data for level 1:

    {
        "@class": ".LogEventRequest",
        "eventKey": "PROGRESS_QUERY",
        "PATH": "LEVEL_1",
        "requestId": "1392895355079"
    }
    {
        "@class": ".LogEventResponse",
        "requestId": "1392895355079",
        "scriptData": {
            "data": []
        }
    }

We can see now that the user has no data for level 1.

Reach a Checkpoint on Level 1

We want to update the document to put the checkpoint reached into the document:

    {
        "@class": ".LogEventRequest",
        "eventKey": "PROG_UPDATE",
        "PATH": "LEVEL_1.checkpoint",
        "VAL": 1,
        "requestId": "1392895516999"
    }
    {
        "@class": ".LogEventResponse",
        "requestId": "1392895516999"
    }

Once this request has been passed in, we can query the document to get the progress in the future if required:

    {
        "@class": ".LogEventRequest",
        "eventKey": "PROGRESS_QUERY",
        "PATH": "LEVEL_1",
        "requestId": "1392895355079"
    }
    {
        "@class": ".LogEventResponse",
        "requestId": "1392895614721",
        "scriptData": {
            "data": [
                {
                    "LEVEL_1": {
                        "checkpoint": 1
                    }
                }
             ]
    }
    }

Completing Level 1

We'll assume the player has already reached every checkpoint, now we just need to update the "complete" attribute in the document:

    {
      "@class": ".LogEventRequest",
      "eventKey": "PROG_UPDATE",
      "PATH": "LEVEL_1.complete",
      "VAL": 1,
      "requestId": "1392895719046"
    }
    {
      "@class": ".LogEventResponse",
      "requestId": "1392895719046"
    }

Again, we can now query the level 1 data for the user at any point and get the full details:

    {
      "@class": ".LogEventRequest",
      "eventKey": "PROGRESS_QUERY",
      "PATH": "LEVEL_1",
      "requestId": "1392895355079"
    }

Did this page help you? Please enter your feedback below. For questions about using this part of the platform, please contact support here