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:

We'll assume the complete document is the full state of the game, but we don't want to pass the full document around every time we want to update it.

We might want to query for a particular section of the document without returning the full document.

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

When the player starts a level you want to return the current state from the server as a full document, but without the details of the other levels.

We'll imagine we have a player who has already played level 1 and completed it, and has started level 2 but has not yet completed it. In this state, the document we store against the user will look like this:

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 PATHvar inputPath = Spark.data.PATH;
//Get the value that was passed in for VALvar inputValue = Spark.data.VAL;
//We need to create JSON object using a string as a key//We do this using the [] notationvar 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 pathtrue, // 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 PATHvar inputPath = Spark.data.PATH;
if(inputPath.length == 0){
//No input path, we want the whole documentvar resultDocument = progressCollection.find({"_id" : Spark.getPlayer().getPlayerId()});
//Put the results into the response.
Spark.setScriptData("data", resultDocument);
} else {
//The query object for the aggregationvar 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 sensevar 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 aggregationvar partialResult = progressCollection.aggregate(matchObject, projectObject);
//Put the results into the response.
Spark.setScriptData("data", partialResult);
}