Stickybits

Blog Categories
Twitter Updates
    follow me on Twitter
    Currently Reading
    Powered by Squarespace
    Git Projects
    « Adding Erlang library locations | Main | Introducing ErlangX and ErlangXCode »
    Saturday
    Nov012008

    Contact List using SproutCore, Mochiweb and Mnesia

    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().

    Download the source

    References (1)

    References allow you to track sources for this article, as well as articles that were written in response to this article.
    • Response
      The best part of our service is that we take care of all the work for you, allowing you to focus on running your business or organization. That means no more searching for websites to exchange links with, no more sending out unsolicited email link exchange requests, no more waiting and ...

    Reader Comments (9)

    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).

    Nov 2, 2008 at 11:54 | Unregistered CommenterBenjamin

    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.

    Nov 2, 2008 at 15:32 | Registered CommenterJón Grétar Borgþórsson

    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.

    Nov 2, 2008 at 20:49 | Unregistered CommenterLawrence Pit

    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.

    Nov 2, 2008 at 21:02 | Registered CommenterJón Grétar Borgþórsson

    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.

    Nov 4, 2008 at 1:23 | Unregistered CommenterLawrence Pit

    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.

    Nov 4, 2008 at 3:37 | Registered CommenterJón Grétar Borgþórsson

    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

    Nov 5, 2008 at 15:48 | Unregistered CommenterKevin Smith

    CouchDB has some built in security now. 0.10 has a basic admin user(allowing the sepration of read/write). 0.11 allows for more detailed user management.

    However. 99% of database setups don't use any security at all. I don't think I have ever seen row level security actually in use. The setup is always the same. In a usual SQL setup there is a user that the application uses that has full access to the whole database. So there really is no security in use.

    Jan 30, 2010 at 11:17 | Registered CommenterJón Grétar Borgþórsson

    open a new window, login to yourwow goldaccount, and study your guide is it is the most useful wow

    potool you have and has much of the information you need. If you are using the guide but continue to get lost or cannot find where you need to be, just

    Jul 13, 2010 at 2:08 | Unregistered Commenterfdsafd

    PostPost a New Comment

    Enter your information below to add a new comment.

    My response is on my own website »
    Author Email (optional):
    Author URL (optional):
    Post:
     
    Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>
    Fork me on GitHub