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