Managing beers

This topic describes the code that interacts with Couchbase Server to display lists of beers and breweries and edit beer documents. All of the basic CRUD operations are demonstrated in this code.

Showing beers

The beer listing page displays each beer along with a link to the brewery that produces it. However, the beer/by_name view returns only the name of the beer. To obtain the brewery, you need to fetch each beer document and examine it. The document contains the brewery that you need later.

After the list of all beers is retrieved, it creates a list of document IDs to fetch by using the Underscore library’s pluck() function and then passes the list to the getMulti() method. Although it is simpler to perform an individual get on each beer’s ID, that method is less efficient in terms of network usage.

Now that you have the beer documents, you can iterate through the list of retrieved values and assign the key (which is the object key) to a property of the beer object itself to allow usage of it in the template.

Now let’s put this all together:

beer_app.js (showing beer listings)
function list_beers(req, res) {
  var q = ViewQuery.from('beer', 'by_name')
      .limit(ENTRIES_PER_PAGE)
      .stale(ViewQuery.Update.BEFORE);

  db.query(q, function(err, values) {
    // 'by_name' view's map function emits beer-name as key and value as
    // null. So values will be a list of
    //      [ {id: <beer-id>, key: <beer-name>, value: <null>}, ... ]

    // we will fetch all the beer documents based on its id.
    var keys = _.pluck(values, 'id');

    db.getMulti( keys, function(err, results) {

      // Add the id to the document before sending to template
      var beers = _.map(results, function(v, k) {
        v.value.id = k;
        return v.value;
      });

      res.render('beer/index', {'beers':beers});
    })
  });
}
app.get('/beers', list_beers);

The code also tells Express to route requests for /beers to this function, and then directs Express to render the beer/index.jade template. Here is the beer/index.jade template:

views/beer/index.jade
extends ../layout
  block content

    h3 Browse Beers
    form(class="navbar-search pull-left")
      input#beer-search(class="search-query" type="text" placeholder="Search for Beers")

    table#beer-table(class="table table-striped")
      thead
        tr
          th Name
          th Brewery
          th
      tbody
        for beer in beers
          tr
            td: a(href="/beers/show/#{beer.id}") #{beer.name}
            td: a(href="/breweries/show/#{beer.brewery_id}") To Brewery
            td
              a(class="btn btn-small btn-warning" href="/beers/edit/#{beer.id}") Edit
              a(class="btn btn-small btn-danger" href="/beers/delete/#{beer.id}") Delete

    div
      a(class="btn btn-small btn-success" href="/beers/create") Add Beer

Navigate to http://localhost:1337/beers to see a listing of beers. Each beer has To Brewery, Edit, and Delete buttons.

On the bottom of the page, you can also see an Add Beer button, which allows you to define new beers.

Deleting Beers

Due to the simplicity of Couchbase and Express, you can implement a single method to delete both beers and breweries:

beer_app.js (showing beer listings):
function delete_object( req, res ) {
  db.remove( req.params.object_id, function(err, meta) {
    if( err ) {
      console.log( 'Unable to delete document `' + req.params.object_id + '`' );
    }

    res.redirect('/welcome');
  });
}
app.get('/beers/delete/:object_id', delete_object);
app.get('/breweries/delete/:object_id', delete_object);

The code tells Express to route the two deletion URLs to the same method. It attempts to delete the object in the Couchbase cluster that is passed through the URL and then redirects the user to the welcome page. If this delete fails, it also logs an error to the console.

If you find that a beer is still displayed after you click the delete button, you can wait a moment and refresh the browser page to verify that the beer has been deleted. Deleted objects might not immediately get removed from the view because they might need to wait for a view index update.

Another way to verify that a beer has been deleted is by clicking the delete button again and getting a 404 error.

Displaying beers

beer_app.js (showing a single beer):
function show_beer(req, res) {
  db.get( req.params.beer_id, function(err, result) {
    var doc = result.value;
    if( doc === undefined ) {
      res.send(404);
    } else {
      doc.id = req.params.beer_id;

      var view = {
        'beer': doc,
        'beerfields': _.map(doc, function(v,k){return {'key':k,'value':v};})
      };
      res.render('beer/show', view);
    }
  });
}
app.get('/beers/show/:beer_id', show_beer);

Similar to the delete example, the code first checks whether the document actually exists within the cluster. The beer ID is passed through the URL, this is passed to use as beer_id, as seen in the Express route.

To retrieve the information for this particular beer, just call the connection's get() method with the beer_id received through the route. First check to ensure a document was received, and if not return an HTTP 404 error.

If the beer exists, build a view object to pass to the template that contains the beer object and a mapped list of all fields and values that are inside of the beer object. Pass this data to the views/beer/show.jade template:

views/beer/show.jade:
extends ../layout
block content

  h3 Show Details for Beer #{beer.name}
  table(class="table table-striped")
    tbody
      tr
        td: strong #{beer.brewery_id}
        td: a(href="/breweries/show/#{beer.brewery_id}") #{beer.brewery_id}
      for beerfield in beerfields
        tr
          td: strong #{beerfield.key}
          td #{beerfield.value}

  a(class="btn btn-medium btn-warning" href="/beers/edit/#{beer.id}") Edit
  a(class="btn btn-medium btn-danger" href="/beers/delete/#{beer.id}") Delete

The code extracts the brewery_id and creates a special entry with a link pointing to the page to display the actual brewery. Next it iterates over the rest of the fields, printing out the key and value of each one. Finally, it provides links at the bottom to edit and delete the beer.

Editing beers

The following code shows how to edit beer documents:

beer_app.js (beer editing):
function normalize_beer_fields(data) {
  var doc = {};
  _.each(data, function(value, key) {
    if(key.substr(0,4) == 'beer') {
      doc[key.substr(5)] = value;
    }
  });

  if (!doc['name']) {
    throw new Error('Must have name');
  }
  if (!doc['brewery_id']) {
    throw new Error('Must have brewery ID');
  }

  return doc;
}

function begin_edit_beer(req, res) {
  db.get(req.params.beer_id, function(err, result) {
    var doc = result.value;
    if( doc === undefined ) { // Trying to edit non-existing doc ?
      res.send(404);
    } else { // render form.
      doc.id = req.params.beer_id;
      var view = { is_create: false, beer: doc };
      res.render('beer/edit', view);
    }
  });
}
function done_edit_beer(req, res) {
  var doc = normalize_beer_fields(req.body);

  db.get( rc.doc.brewery_id, function(err, result) {
    if (result.value === undefined) { // Trying to edit non-existing doc ?
      res.send(404);
    } else {    // Set and redirect.
      db.set( req.params.beer_id, doc, function(err, doc, meta) {
        res.redirect('/beers/show/'+req.params.beer_id);
      })
    }
  });
}
app.get('/beers/edit/:beer_id', begin_edit_beer);
app.post('/beers/edit/:beer_id', done_edit_beer);

The code defines two handlers for editing. The first handler is the GET method for /beers/edit/:beer_id, which displays a nice HTML form that you can use to edit the beer. It passes the following parameters to the template: the beer object and a Boolean that indicates this is not a new beer (because the same template is also used for the create beer form).

The second handler is the POST method, which validates the input. The POST handler calls the normalize_beer_fields() function, which converts the form fields into properly formed names for the beer document, checks to see that the beer has a valid name, and checks to see that a brewery_id is specified and that it indeed exists. If all the checks pass, the function returns the formatted document. If an exception is thrown, Express catches the error and renders it to the user. Otherwise, the document is sent to Couchbase by using the set() method and the user is redirected to the newly created beer’s show page.

The following template for the editing page is rather wordy because it enumerates all the possible fields with a nice description.

views/beer/edit.jade:
extends ../layout
block content

  if is_create
    h3 Create Beer
  else
    h3 Editing #{beer.name}

  form(method="post" action="")
    fieldset
      legend General Info
      .span12
        .span6
          label Type
          input(type="text" name="beer_type" placeholder="Type of the document" value="#{beer.type}")
          label Name
          input(type="text" name="beer_name" placeholder="The name of the beer" value="#{beer.name}")
          label Description
          input(type="text" name="beer_description" placeholder="A short description" value="#{beer.description}")
        .span6
          label Style
          input(type="text" name="beer_style" placeholder="Bitter? Sweet? Hoppy?" value="#{beer.style}")
          label Category
          input(type="text" name="beer_category" placeholder="Ale? Stout? Lager?" value="#{beer.category}")
    fieldset
      legend Details
      .span12
        .span6
          label Alcohol (ABV)
          input(type="text" name="beer_abv" placeholder="The beer's ABV" value="#{beer.abv}")
          label Biterness (IBU)
          input(type="text" name="beer_ibu" placeholder="The beer's IBU" value="#{beer.ibu}")
        .span6
          label Beer Color (SRM)
          input(type="text" name="beer_srm" placeholder="The beer's SRM" value="#{beer.srm}")
          label Universal Product Code (UPC)
          input(type="text" name="beer_upc" placeholder="The beer's UPC" value="#{beer.upc}")
    fieldset
      legend Brewery
      .span12
        .span6
          label Brewery
            input(type="text" name="beer_brewery_id" placeholder="The brewery" value="#{beer.brewery_id}")

    .form-actions
      button(type="submit" class="btn btn-primary") Save changes

The template first checks the is_create variable. If it’s false, that means the user is editing an existing beer, and the caption is filled with that name. Otherwise, it’s titled as Create Beer.

Creating beers

The code for creating beers is very similar to the code for editing beers:

beer_app.js (create beer):
function begin_create_beer(req, res) {
  var view = { is_create : true, beer:{
    type: '',
    name: '',
    description: '',
    style: '',
    category: '',
    abv: '',
    ibu: '',
    srm: '',
    upc: '',
    brewery_id: ''
  } };
  res.render('beer/edit', view);
}
function done_create_beer(req, res) {
  var doc = normalize_beer_fields(req.body);
  var beer_id = doc.brewery_id + '-' +
                doc.name.replace(' ', '-').toLowerCase();
  db.add( beer_id, doc, function(err, result) {
    if (err) throw err;
    res.redirect('/beers/show/'+beer_id);
  });
}
app.get('/beers/create', begin_create_beer);
app.post('/beers/create', done_create_beer);

The begin_create_beer() function displays the same form as the one used for editing beers, except in this case the is_create parameter is set to true and an empty beer object is passed in. The empty beer object is necessary because the template still tries to populate the form fields with existing values.

The POST handler calls the normalize_beer_fields() function. Next it uses the add() method to create a new beer in Couchbase Server. This raise causes the callback to be invoked with an error if the beer already exists. If there is an error, it is caught and displayed to the user. If everything went well, the user is redirected to the beer display page for the newly created beer.

Searching beers

In the beer listing page, you might have noticed a search box at the top. You can use it to dynamically filter the table based on user input. The code uses JavaScript at the client layer to perform the querying and filtering and views with range queries at the server (Node.js/Express) layer to return the results.

Before you implement the server-side search method, put the following in the static/js/beersample.js file (if it’s not there already) to listen on search box changes and update the table with the resulting JSON (which is returned from the search method):

static/js/beersample.js (snippet):
$(document).ready(function() {

    /**
     * AJAX Beer Search Filter
     */
    $("#beer-search").keyup(function() {
       var content = $("#beer-search").val();
       if(content.length >= 0) {
           $.getJSON("/beers/search", {"value": content}, function(data) {
               $("#beer-table tbody tr").remove();
               for(var i=0;i<data.length;i++) {
                   var html = "<tr>";
                   html += "<td><a href=\"/beers/show/"+data[i].id+"\">"+data[i].name+"</a></td>";
                   html += "<td><a href=\"/breweries/show/"+data[i].brewery+"\">To Brewery</a></td>";
                   html += "<td>";
                   html += "<a class=\"btn btn-small btn-warning\" href=\"/beers/edit/"+data[i].id+"\">Edit</a>\n";
                   html += "<a class=\"btn btn-small btn-danger\" href=\"/beers/delete/"+data[i].id+"\">Delete</a>";
                   html += "</td>";
                   html += "</tr>";
                   $("#beer-table tbody").append(html);
               }
           });
       }
    });
});

The code waits for key-up events on the search field, and if they happen, it issues an AJAX query on the search function within the app. The search handler computes the result (using views) and returns it as JSON. The JavaScript then clears the table, iterates over the results, and creates new rows.

The search handler looks like this:

beer_app.js (ajax search response):
function search_beer(req, res) {
  var value = req.query.value;
  var q = ViewQuery.from('beer', 'by_name')
      .range(value, value + JSON.parse('"\u0FFF"'))
      .stale(ViewQuery.Update.BEFORE)
      .limit(ENTRIES_PER_PAGE);
  db.query(q, function(err, values) {
    var keys = _.pluck(values, 'id');
    db.getMulti( keys, function(err, results) {
      var beers = [];
      for(var k in results) {
        beers.push({
          'id': k,
          'name': results[k].value.name,
          'brewery_id': results[k].value.brewery_id
        });
      }

      res.send(beers);
    });
  });
};
app.get('/beers/search', search_beer);

The search_beer() function first extracts the user input by examining the query string from the request. It then builds an options object that is passed to the view query API. The code passes the user's input for the startkey and for the endkey passes the user's input appended with a Unicode \u0FFF value, which for the view engine means “end here.” You need to get used to it a bit, but it’s actually very neat and efficient.

It uses the getMulti() method to retrieve the complete data for each beer. However, unlike previous uses of the method, rather than rendering a template using the retrieved data, the object is sent directly to Express, which serializes it to JSON.

Now your search box should work nicely.