Example Sync Function

In Sync Gateway, custom application behavior is expressed in a JavaScript function which offers simple but fine-grained control over your data. For the details read the sync function documentation, for now I'll just show you one and then describe how it works.

JavaScript Sync Function


Here is a slightly simplified version of the sync function from our example app, ToDo Lite. The same backend supports the iOS, Android, and PhoneGap versions.

function(doc, oldDoc) {
     if (doc.type == "task") {
          if (!doc.list_id) {
               throw({forbidden : "Items must have a list_id"})
          }
          requireAccess("list-"+doc.list_id);
          channel("list-"+doc.list_id);
     } else if (doc.type == "list") {
          channel("list-"+doc._id);
          if (!doc.owner) {
               throw({forbidden : "List must have an owner"})
          }
          if (oldDoc) {
               requireUser(oldDoc.owner)
          }
          access(doc.owner, "list-"+doc._id);
          if (Array.isArray(doc.members)) {
               access(doc.members, "list-"+doc._id);
          }
     } else if (doc.type == "profile") {
          channel("profiles");
          var user = doc._id.substring(doc._id.indexOf(":")+1);
          if (user !== doc.user_id) {
               throw({forbidden : "Profile user_id must match docid : " + user + " : " + doc.user_id})
          }
          requireUser(user);
          access(user, "profiles");
     }
}

Sync Function Arguments


Let's go through it one bit at a time and see how we can fit most of what your REST web application server does, into a page of code.

function(doc, oldDoc) {

The sync function is run on each mutation independently, and takes two arguments: the proposed version of the document, and the previous version of the document, if one exists. The old version is useful for validating things like that the person making the change is the person who owns this document, or perhaps your application enforces that certain fields on the document are immutable.

Handling Tasks


if (doc.type == "task") {
     if (!doc.list_id) {
          throw({forbidden : "Items must have a list_id"})
     }
     requireAccess("list-"+doc.list_id);
     channel("list-"+doc.list_id);                                   

This block runs if the document represents an individual task in ToDo Lite. It requires that the document have a list_id field, and that the user who is updating or creating the task has access to that list. If the validation passes, the document is routed to a channel for that list. The next time anyone who has access to the list connects, they'll sync the updated task.

Handling ToDo Lists


} else if (doc.type == "list") {
  channel("list-"+doc._id);                                  
                            

This section is concerned with the metadata associated with a list. The call to "channel" will route the list metadata to the channel associated with the list (assuming the validations for this document pass).

if (!doc.owner) {
  throw({forbidden : "List must have an owner"})
}
if (oldDoc) {
  requireUser(oldDoc.owner)
}                            
                          

The validations for list documents are simple: all lists must have an owner, and lists may only be updated by their owner. (Note that we can express the validations and routing in any order -- if the validation fails then any other calls from this invocation of the sync function will have no effect.)

access(doc.owner, "list-"+doc._id);
if (Array.isArray(doc.members)) {
  access(doc.members, "list-"+doc._id);
}                          
                          

The last thing we want to do for the list metadata document, is use it to grant access so the right people can read from the list's channel. The first call to access makes sure that the list owner can sync items from this todo list. The second call is passed an array of members, granting each of them access to the list. These access grants will also impact who can write tasks to the list, because of the call to requireAccess we made in the first block of this function.

Handling User Profiles


} else if (doc.type == "profile") {
  channel("profiles");                            
                            

The final block of the ToDo Lite sync function is responsible for making sure that the list of application users is available to each user, so that users may select each other as members of lists. The first line of code in this block routes user profile documents to the "profiles" channel.

var user = doc._id.substring(doc._id.indexOf(":")+1);
if (user !== doc.user_id) {
  throw({forbidden : "Profile user_id must match docid : " + user + " : " + doc.user_id})
}
requireUser(user);
                          

The validation for profiles ensures that the document meets a schema requirement (the document ID must match the username of it's creator). It also prevents anyone from editing anyone else's profile.

The final line of code might be deceptively simple. It grants the user access to the "profiles" channel. What this means is that when a new user creates their own profile, the system reacts by giving them read access to everyone else's profile via the "profiles" channel.

access(user, "profiles");
                      
                          

This tour of an example sync function shows most (but not all) of the tools it offers. You can imagine how much code it would take to write a REST API server that enforces similar rules. Lots more. And solving the offine sync problem would still be on your todo list.

So, will you be 100 times more productive with Couchbase Mobile?