A newer version of this software is available
You are viewing the documentation for an older version of this software. To find the documentation for the current version, visit the Couchbase documentation home page.
This guide provides information for developers who want to use the Couchbase Node.js SDK to build applications that use Couchbase Server.
The following sections demonstrate how to get started using Couchbase with the Node.js SDK. We’ll first show how to install the SDK and then demonstrate how it can be used to perform some simple operations.
The Node.js SDK is compatible only with Node.js v0.8, v0.10 and v0.11. Because the SDK has a C++ component, you also need a C++ compiler and associated build tools. The SDK package for Microsoft Windows ships with prebuilt binaries.
To get started with the Node.js SDK, install the following packages:
Install the Node.js SDK. The easiest way to do this is via the npm tool:
shell> npm install couchbase
If all goes well, you won’t see any errors printed to the screen.
To follow the tradition of programming tutorials, we’ll start with “Hello Couchbase”. This example works with the “beer-sample” bucket that is provided with the default install.
var couchbase = require("couchbase");
var bucket = new couchbase.Connection({
'bucket':'beer-sample',
'host':'127.0.0.1:8091'
}, function(err) {
if (err) {
// Failed to make a connection to the Couchbase cluster.
throw err;
}
bucket.get('aass_brewery-juleol', function(err, result) {
if (err) {
// Failed to retrieve key
throw err;
}
var doc = result.value;
console.log(doc.name + ', ABV: ' + doc.abv);
doc.comment = "Random beer from Norway";
bucket.replace('aass_brewery-juleol', doc, function(err, result) {
if (err) {
// Failed to replace key
throw err;
}
console.log(result);
// Success!
process.exit(0);
});
});
});
The following points explain each step in the example:
Connecting
A Connection object represents a connection to a single bucket within the cluster.
A bucket represents a logical namespace for a key. All keys must be unique within a single bucket, but multiple buckets can have keys with the same names and they will not conflict. A new connection object must be created for each bucket that you want to interact with in your application. This example creates one connection to the beer-sample bucket.
The constructor is passed the bucket name, which is beer-sample, and a node on the cluster to connect to. You can pass any node that is a member of the cluster. This example uses the local cluster instance.
Retrieving Data
The get method initiates an asynchronous request to retrieve the key requested. If the key exists, the callback will be invoked with a results object that contains the value of the key as well as any additional metadata returned from the cluster. To get the actual value of the object, you can access the result object’s value property.
If the key does not exist on the server, the callback will be invoked with an unset value property on the result object (it will not set the callbacks error parameter).
Storing Data
To store documents in the server, you can use one of the set family of methods. Here we use replace, which enforces the constraint that a previous value of the document must already exist. This can be thought of as an update operation in terms of CRUD (create, read, update, delete).
The storage methods also return a result object that contains metadata about the value that was stored.
Now we’re ready to run our first Couchbase Program:
shell> node hello-couchbase.js
A document in Couchbase server consists of a key, value, and metadata.
Key
A key is a unique identifier for your data. Each document must have a unique key. The key can be any valid string.
Value
The value is your own application data that exists under the key. The format of the value can be anything, couchnode will automatically choose the best storage format.
Metadata
The metadata contains information concerning the format of the value that is, whether it’s JSON, a raw type, or something else. It also contains revision information such as the CAS, which we’ll read about later.
You can store documents by providing the unique key under which the document will be stored, and the value which contains the actual document. You can retrieve documents either by directly specifying the unique key under which the document was stored or by querying views that retrieve documents based on specific criteria.
Couchbase provides two ways to fetch your documents: you can retrieve a document by its key, or you can retrieve a set of documents that match some constraint by using Views. Because views are more complex, we’ll first demonstrate getting documents by their keys.
To get a single document, simply supply the key as the first argument to the get method. It returns a result object on success that can then be used to extract the value.
bucket.get(‘test1’, function(err, result) {
console.log(result.value);
});
To get multiple documents, you can use the more efficient getMulti method. You can pass it an array of keys and it will return an object containing keys that match the keys you passed into getMulti and result objects similar to the get call as values.
bucket.getMulti([‘test1’, ‘test2’], function(err, results) {
for(key in results) {
console.log(key + ‘: ‘ + results[key].value);
}
});
If the key does not exist on the server, the callback will be invoked with an unset value property on the result objects (it will not set the callbacks error parameter). In the case of a getMulti call, it is possible that some results will be returned, while others will not. For every other kinds of error such as temporaryError, it is possible that some result objects will have error set, while others do not. If any of the results from a getMulti call failed, there error parameter of the callback will be set to 1 to signify that at least one operation failed to execute successfully.
This section provides a bit more insight on how to store documents. This is a prerequisite to demonstrate how to retrieve documents because there must be something to retrieve.
There are additional storage methods beyond those described here, which are covered in the Advanced section, see Chapter 4, Advanced Usage. These include manipulating numeric counters, setting expiration times for documents, and appending/prepending to existing values.
The Connection object provides the following store operations, which conform to the CRUD model:
set(key, value, options)
Stores the document value under the key. If the key did not previously exist, it is created. If the key already exists, its existing value is overwritten with the new contents of value.
add(key, value, options)
Stores the document value under the key, but only if key does not already exist. If key already exists, an exception is thrown.
replace(key, value, options)
Replace is the inverse of add. It sets the contents of key to value, but only if the key already exists. If the key does not already exist, an exception is thrown.
remove(key, options)
Deletes the key from the bucket. Future attempts to access this key via get raise an exception until something is stored again for this key using one of the set methods.
The following code demonstrates the store operations.
var couchbase = require('couchbase');
var key = "demo_key";
var value = "demo_value";
// We use the 'default' bucket.
bucket = new couchbase.Connection({bucket: 'default', host:'localhost'}, function(err) {
if (err) throw err;
console.log('Setting key ' + key + ' with value ' + value);
bucket.set(key, value, function(err, result) {
if (err) throw err;
console.log(result);
console.log('Getting value for key ' + key);
bucket.get(key, function(err, result) {
if (err) throw err;
console.log(result);
console.log('Creating new key ' + key + ' with value "new_value"');
console.log('This will fail as ' + key + ' already exists');
bucket.add(key, 'new_value', function(err, result) {
console.log(err);
console.log('Replacing existing key ' + key + ' with new value');
bucket.replace(key, 'new value', function(err, result) {
if (err) throw err;
console.log(result);
console.log('Getting new value for key ' + key);
bucket.get(key, function(err, result) {
if (err) throw err;
console.log(result);
console.log('Deleting key ' + key);
bucket.remove(key, function(err, result) {
if (err) throw err;
console.log(result);
console.log('Getting value for key ' + key);
console.log('This will fail as it has been deleted');
bucket.get(key, function(err, result) {
console.log(err);
// Done
process.exit(0);
});
});
});
});
});
});
});
});
Output:
Setting key demo_key with value demo_value { cas: { '0': 467599872, '1':
461666438 } } Getting value for key demo_key { cas: { '0': 467599872, '1':
461666438 }, flags: 4, value: 'demo_value' } Creating new key demo_key with
value "new_value" This will fail as demo_key already exists { [Error: Key exists
(with a different CAS value)] code: 12 } Replacing existing key demo_key with
new value { cas: { '0': 467599872, '1': 3664555910 } } Getting new value for key
demo_key { cas: { '0': 467599872, '1': 3664555910 }, flags: 4, value: 'new
value' } Deleting key demo_key { cas: { '0': 467599872, '1': 3681333126 } }
Getting value for key demo_key This will fail as it has been deleted { [Error:
No such key] code: 13 }
In addition to fetching documents by keys, you can also employ Views to retrieve information using secondary indexes. This guide gets you started on how to use them from the Node.js SDK. If you want to learn more about views, see the chapter in the Couchbase Server 2.0 documentation.
First, create your view definition using by the web UI (though you may also do this directly from the Node.js SDK, as will be shown later).
You can then query the view results by calling the query method on the Connection object. Simply pass it the design and view name.
The view method returns a ViewQuery object which provides an interface for accessing the data stored in a view. The view can be then queried via the ViewQuery method through the use of the query or firstPage methods which return a full list of results, or only the first page respectively. Both the view method as well as the query methods accept additional optional options that control the behavior of the results returned. You can thus use it as follows:
var viewQuery = bucket.view('beer', 'brewery_beers', {opt1: value1, opt2: value2});
viewQuery.query(function(err, results) {
for(key in results) {
// Do something with the results
}
});
Or alternatively like this to retrieve only the first 30 results:
var viewQuery = bucket.view('beer', 'brewery_beers', {opt1: value1, opt2: value2});
viewQuery.firstPage({limit:30}, function(err, results) {
for(key in results) {
// Do something with the results
}
});
Here are some of the available options for the query methods. A full listing can be found in the API documentation.
reduce
This boolean parameter indicates whether the server should also pass the results to the view’s reduce function. An error is triggered if the view does not have a reduce method defined.
limit
This numeric parameter indicates the maximum amount of results to fetch from the query. This parameter is handy if your query can produce a lot of results.
descending
This boolean parameter indicates that the results should be returned in reverse order.
stale
This string parameter controls the trade-off between performance and freshness of data. Possible values include “false” (force update), “update_after” (force after view request) and “ok” (default: do not force any updates).
debug
This boolean parameter fetches low-level debugging information from the view engine.
In this chapter we build on the foundations of the Getting Started guide and build a simple web application. Make sure you have the beer-sample bucket installed, because we’ll be using it. The sample application will allow you to edit and manage various beers and breweries.
This tutorial is not entirely complete, and leaves some features that remain to be implemented. Implementing them is left as an exercise for the reader.
The full source code for the sample application is available in the beersample-node project through couchbaselabs on GitHub.
Note that the sample application provides more content than described in this tutorial, but it should be simple to navigate while reading this tutorial.
Download Couchbase Server and install it. Make sure to install the beer-sample dataset when you run the wizard because this tutorial application works with it.
Clone the repository and install dependencies (express, jade):
shell> git clone git://github.com/couchbaselabs/beersample-node Cloning into
'beersample-node' #... shell> cd beersample-node shell> npm install
Some views need to be set up. You can set up the views manually via the Web UI, or invoke the beer.js script located in the beersample-node directory with the –setup parameter.
In the beer design document, create a development view called by_name, then promote it to a production view (more details later on why this is neccessary):
function (doc, meta) {
if (doc.type && doc.type == "beer") {
emit(doc.name, null);
}
}
Create another design document called brewery and add a view called by_name (dont forget to promote it to a production view as above!):
function (doc, meta) {
if (doc.type && doc.type == "brewery") {
emit(doc.name, null);
}
}
Invoke the beer.js script:
shell> node beer.js Server running at http://127.0.0.1:1337/
Navigate to http://localhost:1337/welcome and enjoy the application!
In this section we’ll talk a bit about setting up your directory layout and adding some views in the server before we start dealing with the Node.js SDK and express+jade itself.
Create a project directory named beer:
shell> mkdir beer shell> cd beer shell> mkdir views shell> mkdir views/beer
shell> mkdir views/brewery shell> mkdir static shell> mkdir static/js shell>
mkdir static/css
Showing your directory contents displays something like this:
shell> find. -type
d./static./static/js./static/css./views./views/brewery./views/beer
To make the
application look pretty, we’re incorporating jQuery and Twitter Bootstrap. You
can either download the libraries and put them in their appropriate css and js
directories (under static), or clone the project repository and use it from
there. If you followed the Quickstart steps, you already have the files in your
beersample-node directory. Either way, make sure you have the following files in
place:
static/css/beersample.css static/css/bootstrap.min.css (the minified twitter
bootstrap library) static/css/bootstrap-responsive.min.css (the minified
responsive layout classes from bootstrap) static/js/beersample.js
static/js/jquery.min.js (the jQuery javascript library)
From here on, you
should have a bare bones web application configured that has all the
dependencies included. We’ll now move on and configure the beer-sample bucket
the way we need it.
The beer-sample bucket comes with a small set of predefined views, but to make our application function correctly we need some more. This is also a good chance to explore the view management possibilities inside the Web-UI.
Because we want to list beers and breweries by name, we need to define one view for each. Head over to the Web-UI and click on the Views menu. Select beer-sample from the drop-down list to switch to the correct bucket. Now click on Development Views and then Create Development View to define your first view. You need to give it the name of both the design document and the actual view. Insert the following names:
Design Document Name: _design/dev_beer
View Name: by_name
The next step is to define the map and (optional) reduce functions. In our examples, we won’t use the reduce functions at all but you can play around and see what happens. Insert the following map function (that’s JavaScript) and click Save.
function (doc, meta) {
if(doc.type && doc.type == "beer") {
emit(doc.name, null);
}
}
Every map function takes the full document (doc) and its associated metadata (meta) as the arguments. You are then free to inspect this data and emit a result when you want to have it in your index. In our case, we emit the name of the beer (doc.name) when the document both has a type field and the type is beer. We don’t need to emit a value — that’s why we are using null here. It’s always advisable to keep the index as small as possible. Resist the urge to include the full document through emit(meta.id, doc), because it will increase the size of your view indexes. If you need to access the full document you can call db.get(result.key) to get the individual doc for a single row. The resulting retrieval of the document might be slightly out of sync with your view, but it will be fast and efficient.
Now we need to define a view for our breweries. You already know how to do this — here is all the information you need to create a brewery view:
Design Document Name: _design/dev_brewery
View Name: by_name
Map Function:
function (doc, meta) {
if(doc.type && doc.type == "brewery") {
emit(doc.name, null);
}
}
The final step is to push the design documents in production. While the design documents are in development, the index is applied only to a deterministic subset of the data. Because we want to have the index on the whole dataset, click the Publish button on both design documents (and accept any pop-up windows that warn you about overriding the old design documents).
For more information about using views for indexing and querying from Couchbase Server, see the following helpful resources in the Couchbase Server Manual:
General information: Views and Indexes
Sample patterns: View and Query Pattern Samples
Time stamp patterns: Many developers ask about extracting information based on date or time. To find out more, see Date and Time Selection
The first route we will implement is that of the welcome page, that is, the page that is displayed when someone goes to the root of your site. Because there is no Couchbase interaction involved, we just tell express to render the template.
function welcome(req, res) {
res.render('welcome');
}
app.get('/welcome', welcome);
The welcome.html template is actually a Jade template inside the views directory. It looks like this:
extends layout
block content
.span6
.span12
h4 Browse all Beers
a(href="/beers" class="btn btn-warning") Show me all beers
hr
.span12
h4 Browse all Breweries
a(href="/breweries" class="btn btn-info") Take me to the breweries
.span6
.span12
h4 About this App
p Welcome to Couchbase!
p
| This application helps you to get started on application
| development with Couchbase. It shows how to create, update and
| delete documents and how to work with JSON documents.
The template simply provides some links to the brewery and beer pages (which are shown later).
An interesting thing about this template is that it “inherits” from the common layout.jade template. All pages in the beer app have a common header and footer to them — with only their body differing. This is the layout.jade template.
!!!5
html(lang="en")
head
meta(charset="utf-8")
title Couchbase Node.js Beer Sample
meta(name="viewport" content="width=device-width, initial-scale=1.0")
meta(name="description" content="The Couchbase Node.js Beer-Sample App")
meta(name="author" content="Couchbase, Inc. 2013")
link(href="/css/bootstrap.min.css" rel="stylesheet")
link(href="/css/beersample.css" rel="stylesheet")
link(href="/css/bootstrap-responsive.min.css" rel="stylesheet")
//HTML5 shim, for IE6-8 support of HTML5 elements
//if lt IE 9
script(src="http://html5shim.googlecode.com/svn/trunk/html5.js")
body
.container-narrow
.masthead
ul.nav.nav-pills.pull-right
li: a(href="/welcome") Home
li: a(href="/beers") Beers
li: a(href="/breweries") Breweries
h2.muted Couchbase Beer Sample
hr
.row-fluid
.span12
block content
hr
.footer
p © Couchbase, Inc. 2013
script(src="/js/jquery.min.js")
script(src="/js/beersample.js")
The template simply provides some links to the brewery and beer pages (which are shown later).
If you start your app now, you should be able to navigate to http://localhost:1337/welcome and see the welcome page. You’ll get a 404 error if you try to visit any links though - this is because we haven’t implemented them yet. Let’s do that now!
Now we’re finally getting into the cooler stuff of this tutorial. In the beer listing page, we want to display each beer along with a link to the brewery that produces it. However, we’ve defined the beer/by_name view to return only the name of the beer. To obtain the brewery, we need to fetch each beer document and examine it. The document contains the brewery that we need later.
After we’ve retrieved a list of all the beers, we create a list of document IDs to fetch by using the underscore libraries pluck function. We pass this list to getMulti.
While we could have made this simpler by performing an individual get on each beer’s id, that would be less efficient in terms of network usage.
Now that we have the beer documents, we iterate through the list of values we retrieved 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 = {
limit : ENTRIES_PER_PAGE, // configure max number of entries.
stale : false // We don't want stale views here.
};
db.view( "beer", "by_name", q).query(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, null, 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);
We also tell express to route requests for /beers to this function, we then direct 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.
Let’s implement the Delete button next!
Due to the simplicity of Couchbase and express, we 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);
Here we tell express to route our two deletion URLs to the same method. We attempt to delete the object in our Couchbase cluster that is passed through the url then redirect the user to our Welcome page. If this delete fails, we also log 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 may not immediately get removed from our view, but may instead 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.
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);
Like for the delete example, we first check if the document actually exists within our cluster. We pass the beer ID through the URL, this is passed to use as beer_id, as seen in the express route.
In order to retrieve the information or this particular beer, we simply call the connections get method with the beer_id we received through the route. We first check the ensure we received a document and if not return a HTTP 404 error.
If the beer exists, we build prepare a view object to pass to the template which contains the beer object as well as a mapped list of all fields and values that are inside of the beer object. We pass this data to the views/beer/show.jade template, which you is shown below.
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
Here we extract the brewery_id, and create a special entry with a link pointing to the page to display the actual brewery.
Then we is iterate over the rest of the fields, printing out the key and value of each.
Finally, we provide links at the bottom to Edit and Delete the beer.
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);
We define two handlers for editing. The first handler is the GET method for /beers/edit/:beer_id, which displays a nice HTML form that we 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 will catch this error and render it to the user. Otherwise, the document is set to Couchbase using the set method and the user is redirected to the newly created beer’s show page.
This template is rather wordy because we enumerate 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, then we’re editing an existing beer, and the caption is filled with that name. Otherwise, it’s titled as Create Beer.
Creating beers is largely the same as 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);
Here we display the same form as the one for editing beers, except we set the is_create parameter to true, and pass an empty beer object. This is necessary because the template still tries to populate the form fields with existing values.
In the POST handler, we call normalize_beer_field as above when editing beers.
Because we’re creating a new beer, we use the add method instead. This raise will cause the callback to be invoked with an error if the beer already exists. We catch this and display it to the user.
If everything went well, the user is redirected to the beer display page for the newly created beer.
In the beer listing page above, you might have noticed a search box at the top. We can use it to dynamically filter our table based on user input. We’ll use Javascript at the client layer to perform the querying and filtering, and views with range queries at the server (nodejs/express) layer to return the results.
Before we implement the server-side search method, we need to 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 keyup 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 = { startkey : value,
endkey : value + JSON.parse('"\u0FFF"'),
stale : false,
limit : ENTRIES_PER_PAGE }
db.view( "beer", "by_name", q).query(function(err, values) {
var keys = _.pluck(values, 'id');
db.getMulti( keys, null, 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 beer_search function first extracts the user input by examining the query string from the request.
It then builds an options object which will be passed to the view query api. We pass the users input for the startkey, and pass the users input appended with a Unicode \u0FFF, 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.
We use getMulti (as has been seen in list beers page in a previous tutorial section) to retrieve the complete data for each beer. However, unlike before, rather than rendering a template using the data we have retrieved, we send the object directly to express, which will kindly serialize it to JSON for us.
Now your search box should work nicely.
While you may notice that the breweries management code has been implemented in the repository, it is left as an exercise to the user to work out the details of this. Additionally, here are some things that are not implemented in the example which you might add while learning to use the SDK:
When deleting a brewery, ensure it has no beers dependent on it.
Provide a search where one can query beers belonging to a given brewery.
Handle concurrent updates to a beer and/or brewery.
Implement a like feature, where one can like a beer or a brewery; likewise, they can unlike one as well!
The tutorial presents an easy approach to start a web application with Couchbase Server as the underlying data source. If you want to dig a little bit deeper, the full source code in the couchbaselabs repository on GitHub has more code to learn from. This code might be extended and updated from time to time.
In addition to the default methods for you to retrieve documents from the cluster (get), we additionally provide the ability to request documents from replica sets (via the getReplica and getReplicaMulti functions). This allows you to gracefully handle instances where the active node holding your document goes offline, but accessing the document from a replica node may still be acceptable. Keep in mind that doing storage operations directly to replica nodes is not possible. Additionally, replica-reads are not meant to provide load balancing capabilities, the cluster handles mapping of keys to individual nodes to provide load balancing across all available nodes.
Note that retrieving data from a replica node could return potentially stale document contents if the replica did not receive a newer mutation of the document.
The replica read function works nearly identically to a normal get request, though it does not provide the ability to do getAndTouch operations. Additionally, replica-reads allow you to specify which replica you wish to retrieve the document from which can be any number from 0
to num_replicas-1
, you may also specify -1
(the default) to retrieve from the fastest responding replica node.
Most API functions have both single and multi-key (batched) variants. The batched variant has the same name as the single-key variant, but its method name has Multi appended to it.
The batched operations are significantly quicker and more efficient, especially when dealing with many small values, because they allow pipelining of requests and responses, saving on network latency.
Batched operations tend to accept an array of keys and return an object where the object key matches the key requested and the value of said keys are the same as if a singular operation was done.
Sometimes a single (or many) keys in a batched operation will fail. In this case, the callback will receive an error parameter containing 1 (rather than the usual 0), and the keys that failed will have a result object which contains an error key containing an error object rather than value key which would normally contain the value that was retrieved.
This section explains how to uncover bugs in your application (or in the SDK itself).
To debug anything, you must be able to identify in which domain a problem is found. Specifically, the following components participate in typical Couchbase operations:
Couchbase Server
This is the server itself, which stores your data. Errors can happen here if your data does not exist, or if there are connectivity issues with one or more nodes in the server. Note that while Couchbase Server is scalable and fault tolerant, there are naturally some conditions that would cause failures (for example, if all nodes are unreachable).
libcouchbase
This is the underlying layer that handles network communication and protocol handling between a client and a Couchbase node. Network connectivity issues tend to happen here.
Node.js C++ Binding Layer
This is the C++ code that provides the bulk of the SDK. It interfaces with the libcouchbase component, handles marshaling of information between libcouchbase and your application, performs input validation, and encoding and decoding of keys and values.
Node.js Layer
This is written in pure Javascript. For simple key-value operations these normally just dispatch to the C++ layer. Most of the view operations are handled here as well, with the C++ layer just performing the lower level network handling.
All Javascript data types that are able to be serialized via the JSON functions are supported by default. It is worth noting that recursive structures cannot be serialized. It is also possible to encode data using the raw Node.js formats or in UTF-8 as well by using the format option on the storage operations.
bucket.set(‘test-key’, 'test-value', {
format:couchbase.format.utf8
}, function(err, result) {
console.log(result);
});
The Node.js Couchbase driver employs a callback pattern to notify the application when results are ready. These callbacks are passed to the operation methods and the callbacks are invoked later once the results or errors are ready. All storage and retrieval operations follow the same callback pattern and have the following parameters:
error
For singular operations, this parameter will contain an Error object representing any error that occurred during the execution of the operation, or alternatively will contain null if no errors occurred.
For a batch operation, this will contain either 1 or 0 representing if an error occurred on any of the batched operations.
result/results
For a singular operation, this parameter will contain an object with the properties as listed below.
For a batched operation, this will be an object where the keys match the keys your operation targeted, and the values will contain an object containing the fields listed below.
value
This will contain the value of the key that was requested if the operation was a retrieval type operation.
error
This will contain an Error object representing any errors that occurred during execution of this particular batch operation.
cas
This will contain an opaque object representing the resulting CAS value of the key that was operated upon. This value is not meant to be user-facing, but should be passed directly to other operations for locking purposes.
In production deployments, it is possible that you will have more than a single instance of your application trying to modify the same key. In this case a race condition happens in which a modification one instance has made is immediately overridden.
Consider this code:
function add_friend(user_id, friend_id) {
bucket.get('user_id-' + user_id, function(err, result) {
result.value.friends[friend_id] = { 'added': new Date() };
bucket.set('user_id-' + user_id, result.value, function(err, result) { } );
});
}
In this case, friends is an array of friends the user has added, with the keys being the friend IDs, and the values being the time when they were added.
When the friend has been added to the array, the document is stored again on the server.
Assume that two users add the same friend at the same time, in this case there is a race condition where one version of the friends array ultimately wins.
Couchbase provides two means by which to solve for this problem. The first is called Opportunistic Locking and the second is called Pessimistic Locking.
Both forms of locking involve using a CAS value. This value indicates the state of a document at a specific time. Whenever a document is modified, this value changes. The contents of this value are not significant to the application, however it can be used to ensure consistency. You can pass the CAS of the value as it is known to the application and have the server make the operation fail if the current (server-side) CAS value differs.
When something goes wrong, an Error object will be returned into the callback passed to the operation. This Error object contains a lot of information that can be helpful to identify what went wrong. Logging this object will typically contain a stack trace as well as contain the error code and message that was generated within libcouchbase.
An Error object contains all of the default Javascript error properties, but is also enhanced with a numeric code value which is represents the error encountered within libcouchbase. This error code can be used for quick identification of errors and easier error handling.
The API reference for the Node.js client is available at http://www.couchbase.com/autodocs/couchbase-node-client-latest/index.html
The following sections provide release notes for individual release versions of Couchbase Node.js Client Library. To browse or submit new issues, see Couchbase Node.js Client Library Issues Tracker.
New Features and Behavior Changes in 1.2.4
New Features and Behavior Changes in 1.2.1
total_rows
available from view requests.New Features and Behavior Changes in 1.2.0
New Features and Behavior Changes in 1.1.1
New Features and Behavior Changes in 1.1.0
New Features and Behavior Changes in 1.0.1
New Features and Behavior Changes in 1.0.0
Known Issues in 1.0.0
New Features and Behavior Changes in 1.0.0-beta
New Features and Behavior Changes in 0.1.0