In my previous post I spoke about how Mobile Services is good at giving you a REST endpoint where you can store data. This time I want to show off what you can do with node.js as data is read/inserted through these endpoints. These are very common operations that we often handle as part of our REST calls. It further illustrates how node.js JavaScript comes to serve as your server side logic code.
Reading
Normally when you make a request to a Mobile Services table (or endpoint) request.execute() is called causing an array of JSON objects to be returned comprising the contents of the table (or subset if a filter is provided). While this is the general use of READ, what about a cause where you want to augment to return with some additional data.
As part of the demo for my Mobile Services talk, I want to pass the READ the token of the device and return back a listing of the stored channels with a boolean field indicating whether the device is registered with that channel. To do this, I will have to talk to an additional table to determine whether the device is registered.
I will point out that due to some of the decisions made by Mobile Services this process is not as clear cut as you would expect it to be, especially if you commonly work with .NET. For one thing, any database operation has a callback, operations are not synchronous (despite being on the server). With that in mind, our first goal is to read out the stored channels from our Channels table:
var channelTable = tables.getTable("Channels");
channelTable.read({
success: function(results) {
var returnResult = [];
results.forEach(function(result) {
var channelName = result.Name;
var channelId = result.id;
returnResult.push({
Id: channelId,
Name: channelName,
Registered: false
});
});
returnWithRegistrationStatus(returnResult, token, request);
}
});
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
We get a reference to our table (Channels) and then simply call read. read takes a single parameter which allows us to define options (the documentation does not seem to list what other options exist), one of these supported options is a success callback; you will find this pattern used throughout server side coding using MS, I am still undecided on whether I like it or not. It is because of this pattern that I had to structure this relatively simple and straightforward operation is such an interesting way. As I create my return result set, I default the IsRegistered to false. But notice, the JSON object I am creating is not indicative of the schema for Channels, it is entirely custom and is what we will return once we update IsRegistered,
The second part of this is the returnWithRegistrationStatus method call.
function returnWithRegistrationStatus(results, token, request) {
var assocTable = tables.getTable("ClientChannels");
assocTable.where({
Token: token
}).read({
success: function(regs) {
regs.forEach(function(reg) {
var channel = getChannel(reg.id, results);
if (channel != null)
channel.Registered = true;
});
request.respond(200, results);
}
});
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
With this block, a read is performed following a where call on the ClientChannels table. In the case of where, you simply pass a JSON object representing what data you are looking for. We then analyze the results and update the IsRegistered flag as appropriate. The final important aspect to this function is the request.respond(200, results) which effectively terminates the read call sending the 200 response code, with results serialized as JSON objects. This is your response.Content on the other end.
I should also point out how I got the token value into the read call. This is the C# code from the device which makes call to the read operation of the Channels endpoint.
var table = client.GetTable("channels?token=" + url.AsToken()); var result = await table.ReadAsync(string.Empty);
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Notice, that I am using GetTable with a query string, it is read in the endpoint like this. The string.Empty call is effectively an empty filter. Since we arent actually using the filter directly in the read call, this really is irrelevant in this case.
var token = request.parameters.token;
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Simple.
Inserting
One of the other common operation is a multi tier insert. In my case, when I attempt to associate a client with a channel, I want to check if that client is registered. If it is not registered I want to insert it and then perform the association. As with the read operation, this normally simple operation requires a bit of thinking to work with the callback structure of the calls we will use.
var clientTable = tables.getTable("Clients");
clientTable.where({
Token: item.Token
}).read({
success: function (results) {
if (results.length == 0) {
console.log("creating the client");
RegisterClient(item.Token, item.ClientType, item.ChannelId, request);
}
else {
console.log("associating the client");
RegisterWithChannel(item.Token, item.ChannelId, request);
}
}
});
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Here, depending on whether or not the given client (identified by the token principally) we will call a different function. This is similar to the read in that it users a where call combined with read. Registering a client is nothing more than inserting a record into the Clients table. Here is the code for RegisterClient:
1: var clientTable = tables.getTable("Clients");
2: console.log("inserting client");
3: clientTable.insert({
4: Token: token,
5: ClientType: clientType
6: }, {
7: success: function(result) {
8: console.log("associating the client");
9: RegisterWithChannel(token, channelId, request);
10: }
11: });
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Pretty straightforward, as with most operation, simply pass a JSON object to the operation as allow Mobile Services to figure things out. In this case, in addition to the standard id column, Azure Mobile Services will create two additional columns: Token, ClientType. Upon the successful completion of this operation, the script moves to perform the channel to device association. Here is the code for RegisterWithChannel:
function RegisterWithChannel(token, channelId, request) { var assocTable = tables.getTable("ClientChannels"); console.log("adding association"); assocTable.insert({ Token: token, ChannelId: channelId }); request.respond(200); }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
This is similar to the RegisterClient method. It inserts a token and channel Id into the ClientChannels table. Interestingly, it seems that if you perform this operation from within the trigger, the trigger does not get repetitively called. I was worried about this, because we are effectively inserting into the table whose Insert script is being executed. The fact that this repetition does not occur further proves that the purpose of scripts is to perform server side logic and act as a front for the incoming data.
The final operation is the call to respond on request which terminates the script. In this case, I am not returning anything to the calling code, although it is conceivable that you could return a JSON object as the second parameter with additional information.
Closing Thoughts
It is clear that Mobile Services is a work in progress. While the editor for these scripts is very nice and I applaud Microsoft for providing a good developer experience it is still lacking. Right now, the best means of debugging are going to be calls to console.log. I have no doubt this will improve, but for the moment it can make development tricky. Also, the documentation (http://msdn.microsoft.com/en-us/library/windowsazure/jj554226.aspx) could use some elaboration and additional examples.
By far the most annoying this is the structure of these scripts. Operations that should be straightforward and simple require much thought to adhere to the callback pattern. I would prefer to simple call .read() and work with the results that way as opposed to relying on callbacks which can make code messy very quickly. This in conjunction with the reliance on console.log make developing complex scripts difficult.