Monday, August 29, 2011

Deleting Things in Backbone.js

‹prev | My Chain | next›

After getting a pretty decent Backbone.js view implementation in place yesterday, today I would like to see if I can add a bit of interactivity to the beasty. The easiest thing seems to be deleting. "Easy" usually turns out to be a red-flag, but who knows? Maybe this time it'll just work.

Anyhow, the first thing I need is a delete route in my express.js app. Nothing too fancy ought to be required. I just need to make an HTTP request with a method of "DELETE" to my CouchDB backend. The response from CouchDB can then be piped directly to my Backbone app. Something like this ought to do:
app.delete('/events/:id', function(req, res){
  var options = {
    method: 'DELETE',
    host: 'localhost',
    port: 5984,
    path: '/calendar/' + req.params.id
  };

  // Send the HTTP request with the DELETE options
  var couch_req = http.request(options, function(couch_response) {
    console.log("Got response: %s %s:%d%s", couch_response.statusCode, options.host, options.port, options.path);

    // Pipe the response from CouchDB to the browser
    couch_response.pipe(res);
  }).on('error', function(e) {
    console.log("Got error: " + e.message);
  });

  // Send the complete request.
  couch_req.end();
});
Now to my Backbone application. As usual when I am exploring, I use Chrome's Javascript console for interacting with page elements and Javascript objects. In this case, I would like to delete the Backbone model responsible for the "foo" calendar event on the first of next month:
In the console, I find that entry is the second of four calendar events (clearly, I need to investigate sorting another day):
Assuming that is an Event model, all I need do is call its destroy method and the offending event should be stricken from existence:
> e.destroy()
     => child
Hrm... Dunno what I expected. I suppose it has to be chainable, so maybe it worked..? Actually, no, it did not. Examining the express.js app's log, I see no log entries. Reloading the page, I see that the calendar event is still there. So what gives?

One of the first places I check is the Event model itself:
window.Event = Backbone.Model.extend({});
Say, that looks a bit spartan. Perhaps more is needed for Backbone to know how to delete a thing from the database.

After a bit of research, I find that yes, two things are needed for this to work: a URL root (e.g. /events) and a record ID. In retrospect, both make all kinds of sense. How else is Backbone supposed to infer the resource to be DELETEd?

Anyhow, the fix should be pretty easy. My new and improved Event model looks like:
    window.Event = Backbone.Model.extend({
      urlRoot : '/events',
      initialize: function(attributes) { this.id = attributes['_id']; }
    });
The urlRoot property is fairly self-explanatory. The initialize method for setting the model's id is less so. CouchDB stores document IDs in the "_id" attribute:
{
   "_id": "a38f51509190f265959bbb2b5d001128",
   "_rev": "1-174f31204df52e79a92c1ac875ac09a2",
   "startDate": "2011-09-01",
   "title": "foo",
   "description": "bar"
}
This is available in the model's attributes (I code call event.get("_id") to retrieve it), but Backbone has no way to tie it to the special id property of a Backbone model. So I link the two manually in the model's initializer.

Now I should be able to delete the bogus calendar event. Reloading the page and trying again I get:
Well that is progress. I am seeing an AJAX request logged, but is it doing anything? Checking the express.js logs, it is failing to do something:
Got response: 409 localhost:5984/calendar/a38f51509190f265959bbb2b5d001128
That is certainly progress, but 409?!

Ah, wait. This is CouchDB. I need to work a little harder to delete things. Specifically, I have to assure the database that I am acting on the same revision that is currently in the database. To work properly with this optimistic locking, I need to supply the revision as a query parameter:
DELETE /calendar/a38f51509190f265959bbb2b5d001128?rev=1-174f31204df52e79a92c1ac875ac09a2 HTTP/1.0
Or as an If-Match header:
DELETE /calendar/a38f51509190f265959bbb2b5d001128 HTTP/1.0
If-Match: "1-174f31204df52e79a92c1ac875ac09a2"
Hrm... I tend to think it would be easier to transmit the revision via the If-Match header. If I could set that in my Backbone application, then I ought to be able to pass that directly through to CouchDB:
app.delete('/events/:id', function(req, res){
  var options = {
    method: 'DELETE',
    host: 'localhost',
    port: 5984,
    path: '/calendar/' + req.params.id,
    headers: req.headers
  };

  var couch_req = http.request(options, function(couch_response) {
    // ...
  couch_req.end();
});
I rather like that one line change. No futzing with query parameters just feels cleaner.

But is it even possible to set HTTP headers in Backbone? Actually, it is relatively easy. Anything supplied in the destroy (or update or create) method is sent along to jQuery as an option. Since jQuery AJAX requests recognize a headers attribute, something like this ought to work:
> e.destroy({headers: {'If-Match':'1-174f31204df52e79a92c1ac875ac09a2'} })
And, finally, I see a change in the CouchDB response:
Got response: 200 localhost:5984/calendar/a38f51509190f265959bbb2b5d001128?rev=1-174f31204df52e79a92c1ac875ac09a2
Most importantly, I no longer have a bogus entry on the first:
That is good progress for tonight. I still have work to do with deleting. It would be nice to do this from the UI rather than the Javascript console. Also, I should not have to refresh my display to see the calendar event removed. But I will worry about those things tomorrow.


Day #128

1 comment:

  1. Thanks for writing this up Chris. Definitely a help. I realize it's been awhile since you posted this, but for anyone else reading this, check out Cradle - http://cloudhead.io/cradle for making calls to your CouchDB. It's a lot simpler to use.

    ReplyDelete