Before You Start
This tutorial will teach you how to make a persevere-database sitting in the background and accessing it with sproutcore to fetch and store data. I've looked at the tasks-source and got debug-support from okito, without them this tutorial wouldn't work, so big thanks to them!
About Persevere
Persevere is a database-backend which can be accessed by a RESTful JSON interface and which offers schema-free storage and an explorer to inspect the data in the database (docs). It will serve the pages via jetty (a Java web server) and can be enhanced with JavaScript logic to manipulate data also on the server-side. In our case, we simply define the data structure and are ready to go.
Generating The Server
Before you get started, you have to download the Persevere. To do so, head to http://code.google.com/p/persevere-framework/ and download a package suitable for your operating system. I downloaded the just-released version 1.0.
Install the package (normally by unpacking the archive), and add, to make things simpler, the bin directory from the package to your PATH.
Now, you generate a basic server-skeleton by going into your todo project directory and entering "persvr --gen-server todoserver". Inside the "todoserver" directory (which it creates) it will generate the following structure:
css
images
WEB_INF
config
data
jslib
tests
todoserver_tests.js
web.xml
index.html
ReadMe
You can already start the server by going into the server-directory and starting persrv:
cd todoserver
persvr
However, let us first define the structure (you can quit the server by entering "shutdown").
Go to todoserver->WEB_INF->jslib and create a file named "task.js". This file will describe the data we want to store in the database. Edit the file and enter in the following text:
Class (
{
id: "task",
properties: {
isDone: { type:"boolean" },
description: { type:"string" }
}
} ) ;
This will create a new class inside the database with the structure we need to save our todos. Now we can start the server. This time, however, we will use the --base-uri option so that we can more easily proxy to it using SproutCore.
persvr --base-uri /todoserver
Thats all for the server-side!
Creating the DataSource
Now we can create the datasource for SproutCore. Go to your SproutCore project directory and type:
sc-gen data-source Todos.TaskDataSource
This will generate a basic datasource-skeleton which is described in more detail in the "Hooking up to the backend"-chapter of the tutorial. To load the initial records we need to fill the body of the fetch-function in the newly created datasource. Go to the following file:
apps/todos/data_sources/task.js
and replace the empty fetch method with the following text:
fetch: function(store, query) {
if(query == Todos.TASKS_QUERY) {
SC.Request.getUrl(this.getServerPath('task')).json()
.header('Accept', 'application/json')
.notify(this, 'didFetchTasks', store, query)
.send();
return YES; // query handled by us...
}
return NO;
},
and we need a 'didFetchTasks'-method like this:
didFetchTasks: function(response, store, query) {
if(SC.ok(response)) {
var records = this._normalizeResponseArray(response.get('encodedBody'));
store.loadRecords(Todos.Task, records);
store.dataSourceDidFetchQuery(query);
} else {
store.dataSourceDidErrorQuery(query, response);
}
},
This will check that the query is the TASKS_QUERY, create the URL for the correct server and send the request. Here are two special methods used which I've taken from the tasks-sources. You need both because the answer from Persevere seems to be strange when running on OS X. Also you need to set the header value of the request to get a valid response format from Persevere(otherwise the first 4 characters are '{}&&'). I'm not sure if the fixes for OS X are needed because I don't have access to an OS X server, so I've copy'n'pasted the source from the tasks-source and hope they are working:
_normalizeResponseArray: function(hashes) {
// HACK: [SE] Browsers running in OS X get a string and not a hash, and they don't like the
// format of the string that Persevere sends over the wire. We have to do some <sigh>
// massaging to get it to work.
if (SC.typeOf(hashes) === SC.T_STRING) {
hashes = SC.json.decode(hashes);
}
var ret = hashes ? hashes : [];
var len = hashes.length;
for (var i = 0; i < len; i++) {
this._normalizeResponse(hashes[i]);
}
return ret;
},
_normalizeResponse: function(hash) {
// HACK: [SE] Browsers running in OS X get a string and not a hash, so we have to convert it.
if (SC.typeOf(hash) === SC.T_STRING) {
// HACK: [SE] Also, for some reason, JSON.parse() doesn't like the parentheses that Persevere
// uses to enclose its responses to POST requests, but only in browsers running on OS X.
if (hash.indexOf("(") === 0) {
var tempHash = hash;
hash = tempHash.slice(1, -1);
}
hash = SC.json.decode(hash);
}
var id = hash.id;
if (id && SC.typeOf(id) === SC.T_STRING)
hash.id = id.replace(/^.*\//, '') * 1;
return hash;
},
For completeness the query at the top of the file looks like this:
Todos.TASKS_QUERY = SC.Query.local(Todos.Task, {
orderBy: 'description'
});
To make the fetch-method work, we now need to add the "getServerPath"-function after the declaration:
Todos.TaskDataSource = SC.DataSource.extend(
//the following is new:
_dbpath: 'todoserver',
getServerPath: function(resourceName) {
var path = '/' + this._dbpath + '/' + resourceName;
return path;
},
This will create with a resourceName a complete url to get data from the database-server (in our example the resourcename is always 'task' and the function will create with this '/todoserver/task').
Learn From My Mistakes!
Initially, I thought it would be a great idea to specify the whole server in the 'getServerPath'-function like 'http://localhost:8080/todoserver/task'. Unfortunately, this approach essentially creates a cross-port-xhr-request, which is not generally allowed by browsers! The strange thing is that the initial fetch-request (a get-url) works, but the putUrl or deleteUrl are not working (in firebug you will see a 'options'-request instead of the 'put'-request).
Creating A Proxy For The Server
The correct way to connect the sproutcore application to the server is by creating a proxy. In the root directory of your SproutCore project edit the 'Buildfile' and append the proxy-line:
proxy '/todoserver', :to => 'localhost:8080'
With this, all requests to '/todoserver' are forwarded to 'localhost:8080/todoserver', which is our Persevere server.
Now the initial get should work and if you use the Persevere explorer (http://localhost:8080/todoserver/explorer.html), you can add some test-data to your 'task'-database.
It is easy to make the rest work. You can mostly use the default source code provided by the tutorial.
For completness, here are the routines. The only thin special about the routines are the request-methodes (postUrl, putUrl and deleteUrl).
createRecord: function(store, storeKey) {
if (SC.kindOf(store.recordTypeFor(storeKey), Todos.Task)) {
SC.Request.postUrl(this.getServerPath('task')).json()
.header('Accept', 'application/json')
.notify(this, this.didCreateTask, store, storeKey)
.send(store.readDataHash(storeKey));
return YES;
}
return NO ; // return YES if you handled the storeKey
},
didCreateTask: function(response, store, storeKey) {
if (SC.ok(response)) {
var url = response.header('Location');
store.dataSourceDidComplete(storeKey, null, url); // update url
} else {
store.dataSourceDidError(storeKey, response);
}
},
updateRecord: function(store, storeKey) {
if (SC.kindOf(store.recordTypeFor(storeKey), Todos.Task)) {
var dataHash = store.readDataHash(storeKey);
SC.Request.putUrl(this.getServerPath('task')+'/'+store.idFor(storeKey)).json()
.header('Accept', 'application/json')
.notify(this, this.didUpdateTask, store, storeKey)
.send(dataHash);
return YES;
}
return NO;
},
didUpdateTask: function(response, store, storeKey) {
if (SC.ok(response)) {
var data = response.get('body');
if (data)
data = data.content; // if hash is returned; use it.
store.dataSourceDidComplete(storeKey, data) ;
} else {
store.dataSourceDidError(storeKey);
}
},
destroyRecord: function(store, storeKey) {
if (SC.kindOf(store.recordTypeFor(storeKey), Todos.Task)) {
SC.Request.deleteUrl(this.getServerPath('task')+'/'+store.idFor(storeKey)).json()
.header('Accept', 'application/json')
.notify(this, this.didDestroyTask, store, storeKey)
.send();
return YES;
}
return NO;
},
didDestroyTask: function(response, store, storeKey) {
if (SC.ok(response)) {
store.dataSourceDidDestroy(storeKey);
} else {
store.dataSourceDidError(response);
}
}
At The End
I'm not sure if I handle the id between Persevere and the store.idFor() correctly, but it seems to work. The other missing function is 'retrieveRecord', but in our example it seems that the function is not used, so I've left it empty. For a better understanding how to connect to Persevere study the tasks source code!
Comments (2)
Stephen Bannasch said
at 11:25 am on Nov 22, 2009
Alex,
I was looking at the latest release of Tasks and couldn't get it working on my Mac OS 10.5.8 system.
The issue occurs because the requests going to the Perservere server allow returned content-types of 'application/javascript' -- Tasks uses this for the Accept header on requests:
'application/json, text/javascript, application/xml, text/xml, text/html, */*'
Even though 'application/json' is on the list so is '*/*' and the ordering in the sequence does not matter, in this Accept header all of the content-types are equal in Q value.
In my checkout of Tasks the SC method that parses the returned data from a login tries to parse this as JSON without normalizing it first -- why it works for anybody is a mystery to me right now.
I modified the request so that only a Content-Type of 'application/json' is accepted, Persevere then returns valid json without the '{}&&' preamble.
Take a look at these two commits from my fork of Tasks:
only accept json data from persevere
http://github.com/stepheneb/Tasks/commit/27d00bf797dfba99c13b1d58820f6563a71dcefd
when responses are in json they can also be arrays
http://github.com/stepheneb/Tasks/commit/3a881acf7b422cc7fb78f969973edf3ab148dba1
Also see discussion:
http://groups.google.com/group/sproutcore/browse_thread/thread/54dfa61a00e4360f
Kai said
at 1:03 am on Nov 23, 2009
Hi!
*after* I've posted the tutorial I've seen the discussion-thread at the mailinglist... I'll modify the source...
You don't have permission to comment on this page.