Contact List using SproutCore, Mochiweb and Mnesia
Saturday, November 1, 2008 at 21:01 by
Jón Grétar Borgþórsson Here is an example program using a SproutCore frontend talking to an Erlang backend using the ReST protocol.
Sproutcore
Lets start by creating the Sproutcore application. I will not go into detail into the Sproutcore code as JavaScript and Sprutcore are way beyond a single tutorial.
sc-init mochi_contacts
cd mochi_contacts
Inside mochi_contacts we find a nice little framework for our application. Lets start by creating our model with the command. "sc-gen model mochi_contacts/contacts". Then we edit our model at clients/mochi_contacts/model/contacts.js and add the following code.
resourceURL: 'contacts',
properties: ["guid","fullname",'email','mobile'],
commitChanges: function(){
if(this.get('fullname') == '') this.set('fullname', 'Unnamed');
this.commit();
}
This is pretty straight forward. resourceURL defines the location of the data source. In this case it would be http://localhost/contacts/. In properties we define the keys we want to read from that resource. The commitChanges function then makes sure we have the fullname property defined and commits the changes to the resource.
Next up is our master controller. The master contoller handles our list view at the side. Create it with the command "sc-gen controller mochi_contacts/master SC.CollectionController", open up the file clients/mochi_contacts/controller/masterController.js and add the following.
allowsEmptySelection: false,
allowsMultipleSelection: false,
canEditCollection: true,
addContact: function(sender){
var content = this.get('content');
var contact = MochiContacts.Contacts.newRecord({
fullname: "Unnamed"
},MochiContacts.server);
contact.commitChanges();
this.set('selection',[contact]);
},
delContact: function(sender){
if(!confirm('Are you sure?')) return;
var sel = (this.get('selection') || []).clone() ;
var idx = sel.get('length') ;
while(--idx >= 0) {
var contact = sel.objectAt(idx);
contact.destroy();
}
},
Not much to see here. Just basic functions for creating and deleting contacts from the list and 3 properties for defining the behaviour of the list view.
So now we need a second controller that will handle the form itself. Create it with the command "sc-gen controller mochi_contacts/detail SC.ObjectController" and put the following inside clients/mochi_contacts/controller/detailController.js
contentBinding: 'MochiContacts.masterController.selection',
commitChangesImmediately: false
Wow. Even less we do here. We simply bind the content of the controller to the currently selected object of the master controller. We then define that we dont want the changes to be posted automatically because we will add a button to do that.
Now we have to define the view. Open up clients/mochi_contacts/english.lproj/body.rhtml and remove everything. Replace with the following:
<% content_for('body') do %>
<% split_view :workspace, :class => 'sc-app-workspace footer',
:direction => :horizontal do %>
<% view :sidebar, :outlet => true do %>
<% scroll_view :master_list, :outlet => true do %>
<%= list_view :list_view,
:outlet => true,
:content_value_key => 'fullname',
:content_value_editable => false,
:can_reorder_content => true,
:bind => {
:content => 'MochiContacts.masterController.arrangedObjects',
:selection => 'MochiContacts.masterController.selection'
}
%>
<% end %>
<% end %>
<%= split_divider_view :outlet => true, :width => 5 %>
<% view :detail_view, :outlet => true do %>
<table class="card-detail">
<tr>
<td><label>Full Name:</label></td>
<td>
<%= text_field_view :outlet => true, :hint => "Full Name",
:bind => {
:value => 'MochiContacts.detailController.fullname'
} %>
</td>
</tr>
<tr>
<td><label>Email:</label></td>
<td>
<%= text_field_view :outlet => true, :hint => "email@email.com",
:bind => {
:value => 'MochiContacts.detailController.email'
} %>
</td>
</tr>
<tr>
<td><label>Mobile:</label></td>
<td>
<%= text_field_view :outlet => true, :hint => "555-0000",
:bind => {
:value => 'MochiContacts.detailController.mobile'
} %>
</td>
</tr>
<tr>
<td colspan="2" class="buttons">
<%= button_view :outlet => true,
:title => "Cancel",
:action => 'MochiContacts.detailController.discardChanges',
:bind => {
:enabled => 'MochiContacts.detailController.hasChanges'
} %>
<%= button_view :outlet => true,
:title => "Save Changes", :default => true,
:action => 'MochiContacts.detailController.commitChanges',
:bind => {
:enabled => "MochiContacts.detailController.hasChanges"
} %>
</td>
</tr>
</table>
<% end %>
<% end %>
<% view :footer, :class => 'sc-footer sc-square-theme' do %>
<div class="left">
<%= button_view :outlet => true, :label => '+',
:action => 'MochiContacts.masterController.addContact' %>
<%= button_view :outlet => true, :label => '-',
:action => 'MochiContacts.masterController.delContact' %>
</div>
<% end %> <!-- footer -->
<% end %>
In here we have defined the views and bound their properties to the controllers.
Next we connect the contollers to the model. We do that inside clients/mochi_contacts/main.js by replacing the line MochiContacts.server.preload(MochiContacts.FIXTURES); with the following commands:
var contactList = MochiContacts.Contacts.collection();
contactList.refresh();
MochiContacts.masterController.set('content', contactList);
MochiContacts.server.listFor(contactList);
Before we move to the Erlang part we have to edit sc-config. As javascript request are bound to it's own domain we have to proxy the request by adding this as the very last line:
proxy '/contacts', :to => 'localhost:8808', :cookie_domain => 'localhost'
Now you should be able to start the server with the command sc-server and take a look at the result in http://localhost:4020/mochi_contacts.
The Format
The format SproutCore uses for REST calls are:
- CREATE = [POST]http://somesite/resource/create
- READ = [GET]http://somesite/resource/list
- UPDATE = [POST]http://somesite/resource/updateGUIDNUMBER
- DELETE = [POST]http://somesite/resource/DESTROYGUIDNUMBER
The Post values for CREATE and UPDATE methods are like:
"records[0][mobile]" = "555-1234"
"records[0][email]" = "sam.jones@example.com"
"records[0][fullname]" = "Sam Joness"
"records[0][id]" = "@658"
The body expected to return for the READ method is in JSON format and looks like:
{"records": [
{
"guid": "@570",
"type": "Contacts",
"fullname": "Frank Smith",
"email": "frank.smith@example.com",
"mobile": "555-4433"
},{
"guid": "@658",
"type": "Contacts",
"fullname": "Sam Jones",
"email": "sam.jones@example.com",
"mobile": "555-1234"
}],
"ids": ["@570", "@658"]
}
Each record needs to have guid and type must be the name of the model it belongs to. ids must be a list of all the guid keys.
For mochijson2:encode/1 to be able to create this from an erlang term the format of the erlang term would be:
{struct,[{<<"records">>,
[{struct,[{<<"guid">>,<<"@570">>},
{<<"type">>,<<"Contacts">>},
{<<"fullname">>,<<"Frank Smith">>},
{<<"email">>,<<"frank.smith@example.com">>},
{<<"mobile">>,<<"555-4433">>}]},
{struct,[{<<"guid">>,<<"@658">>},
{<<"type">>,<<"Contacts">>},
{<<"fullname">>,<<"Sam Jones">>},
{<<"email">>,<<"sam.jones@example.com">>},
{<<"mobile">>,<<"555-1234">>}]}]},
{<<"ids">>,[<<"@570">>,<<"@658">>]}]}
The Mochiweb Server
Knowing the expected formats it should be easy to write a mochiweb server to handle this. This is my version. Possibly one of my worst Erlang code but after spending way to much time figuring out mochijson2 I'm just putting this out there. (How about some documentation mochiguys? ;)
-module(web_server).
-include_lib("stdlib/include/qlc.hrl").
-export([start/0,stop/0,dispatch_requests/1]).
-record (contacts, {guid, type, fullname, email, mobile}).
start() ->
mnesia:create_schema([node()]),
mnesia:start(),
try
mnesia:table_info(contacts, type)
catch
exit: _ ->
mnesia:create_table(contacts,
[{attributes, record_info(fields, contacts)},
{type, set},
{disc_copies, [node()]}])
end,
mochiweb_http:start([{port, 8808},{loop, fun dispatch_requests/1}]).
stop() ->
mochiweb_http:stop().
dispatch_requests(Req) ->
Path = Req:get(path),
Method = Req:get(method),
Post = Req:parse_post(),
io:format("~p request for ~p with post: ~p~n", [Method, Path, Post]),
Response = handle(Method, Path, Post),
Req:respond(Response).
handle('GET', "/contacts/list", _Post) ->
F = fun() ->
Q = qlc:q([ build_struct(C) || C <- mnesia:table(contacts) ]),
Q2 = qlc:q(
[ list_to_binary(C#contacts.guid) || C <- mnesia:table(contacts) ]),
Records = qlc:e(Q),
IDs = qlc:e(Q2),
Struct = {struct, [{<<"records">>, Records},{<<"ids">>, IDs}]},
JSON = mochijson2:encode(Struct),
list_to_binary(JSON)
end,
{atomic, Result} = mnesia:transaction(F),
{200, [{"Content-Type", "text/javascript"}], Result };
handle('POST', "/contacts/create", Post) ->
Guid = proplists:get_value("records[0][_guid]", Post),
FullName = proplists:get_value("records[0][fullname]", Post),
Email = proplists:get_value("records[0][email]", Post),
Mobile = proplists:get_value("records[0][mobile]", Post),
F = fun() ->
mnesia:write(#contacts{
guid=Guid, type="Contacts", fullname=FullName,
email=Email, mobile=Mobile})
end,
mnesia:transaction(F),
{200, [{"Content-Type", "text/plain"}], <<"Created">> };
handle('POST', "/contacts/update"++Guid, Post) ->
FullName = proplists:get_value("records[0][fullname]", Post),
Email = proplists:get_value("records[0][email]", Post),
Mobile = proplists:get_value("records[0][mobile]", Post),
F = fun() ->
mnesia:write(#contacts{
guid=Guid, type="Contacts", fullname=FullName,
email=Email, mobile=Mobile})
end,
mnesia:transaction(F),
{200, [{"Content-Type", "text/plain"}], <<"Updated">> };
handle('POST', "/contacts/destroy"++Guid, _Post) ->
F = fun() ->
mnesia:delete({contacts, Guid})
end,
mnesia:transaction(F),
{200, [{"Content-Type", "text/plain"}], <<"Destoyed">> };
handle(_, _, _) ->
{404, [{"Content-Type", "text/plain"}], <<"Unknown Request">>}.
build_struct(Record) ->
{struct, [
{<<"guid">>, list_to_binary(Record#contacts.guid)},
{<<"type">>, list_to_binary(Record#contacts.type)},
{<<"fullname">>, list_to_binary(Record#contacts.fullname)},
{<<"email">>, list_to_binary(Record#contacts.email)},
{<<"mobile">>, list_to_binary(Record#contacts.mobile)}
]}.
Just compile this and start with web_server:start().



Reader Comments (7)
Nice Post!
Mochiweb is an excellent choice for writing REST interfaces, I've also written a post about it (http://21ccw.blogspot.com/2008/06/migrating-native-erlang-interface-to.html)
Have you considered using CouchDB instead of mnesia? Then you could just put the JSON docs directly into the database without worrying about translating between JSON and records (which I think are a bit of a pain, being statically typed).
If we were to use CouchDB we would propably rather connect it straight to the CouchDB server and skip the Erlang part in the middle. It is possible to define types of servers in SproutCore. Currently there is standard REST and Rails REST defined but I doubt it is difficult to change the source to include the CouchDB standard.
Personally I have not worked with this using any databases really. In a project I have started I use Erlang as a middleware to connect SproutCore to Asterisk. Without any databases in the middle.
But yes. I hear you. Records are one of the most painful part of mnesia.
I was considering maybe using erlang as the middelware, but after seeing this code....it looks terrible to me ;-)
Aren't there any (orm or json-to-record) frameworks out there that help you minimize this kind of code? Perhaps browing the couchdb sourcecode can provide some hints in this area.
Btw, I think you're missing the GET /contacts/show handler.
There is a CouchDB adapter for SproutCore. I'd still want to go through some middleware though for data security reasons.
No need for show in this example. It's not used. But yeah the biggest problem is mochijson2. I just used it because it's included in mochiweb.
For the CouchDB adapter. Just use a proxy for the security. Just use Apache or Yaws with basic auth or something.
Re CouchDB adapter: that's what most are suggesting, but it only takes care of user authentication, not data security. As an authenticated user I could quite easily view data from other users that is not meant for my eyes.
Well... If you are writing software that needs data security then frankly CouchDB is hardly ready for you.
You could write some middleware as a security layer but the possibilites for a disasterous bug are too many.
But CouchDB will have this soon I guess.
A small nit, but you don't have any error handling around mnesia transactions. I'd modify the code to look like this:
case mnesia:transaction(F) of
{aborted, Reason} ->
{500, [{"Content-Type", "text/plain"}], list_to_binary(atom_to_list(Reason))};
{atomic, _} ->
{200, [{"Content-Type", "text/plain"}], <<"ok">>}
end