Leanback

Build Status Gem Version

Simple Ruby Interface to CouchDB. This current version is a complete re-write with a new API. I wrote this to use in my projects, I hope you find it useful.

Installation:

gem install leanback

Basic Operations:

Initialize leanback

my_database = Leanback::Couchdb.new(database: "my_database")
#=> #<Leanback::Couchdb:0x000000034a7b78
# @address="http://127.0.0.1",
# @database="my_database",
# @password=nil,
# @port="5984",
# @username=nil>

In the above code, my_database object will be used to perform operations on the couchDB database named "my_database". The above example assumes that the database is in admin party mode, since no username or password was provided.

If there is no admin party then a username and password is required to perform the operations. A username and password can be included during initialization:

my_database = Leanback::Couchdb.new(database: "my_database", username: "obi", password: "abc1234")
#=> #<Leanback::Couchdb:0x000000033fd970
# @address="http://127.0.0.1",
# @database="my_database",
# @password="abc1234",
# @port="5984",
# @username="obi">

By default, Leanback uses couchDB's default http address and port (http://127.0.0.1:5984), to use a different address and port, include it in the initialization:

my_database = Leanback::Couchdb.new(database: "my_database", address: "https://obi.iriscouch.com", port: "6984")
#=> #<Leanback::Couchdb:0x000000033ab5d0
# @address="https://obi.iriscouch.com",
# @database="my_database",
# @password=nil,
# @port="6984",
# @username=nil>

Create database:

my_database = Leanback::Couchdb.new(database: "my_database")
my_database.create
#=> {:ok=>true}

Delete database

my_database.delete
#=> {:ok=>true}

Create a document

my_database.create_doc("linda", firstname: "linda", lastname: "smith")
#=> {:ok=>true, :id=>"linda", :rev=>"1-ff286690ab5b446a727840ce7420843a"}

The created document inside couchDB will be:

{
   "_id": "linda",
   "_rev": "1-ff286690ab5b446a727840ce7420843a",
   "firstname": "linda",
   "lastname": "smith"
}

Delete the document with it's latest revision

my_database.delete_doc("linda", rev = "1-ff286690ab5b446a727840ce7420843a")
#=> {:ok=>true, :id=>"linda", :rev=>"2-d689d9b5b9f2ded6a2157fc9cc84a00f"}

Delete the document without providing a revision

my_database.delete_doc!("linda")
#=> {:ok=>true, :id=>"linda", :rev=>"4-5d1a6851ec7562378caa4ce4adef9ee4"}

delete_doc! with the bang, first fetches the document with the provided id, retrieves it's latest revision and then, deletes the document using the latest revision.

Fetch the document using its id

my_database.get_doc('linda')
#=> {:_id=>"linda",
# :_rev=>"5-74894db03ef6d22e6a0e4ef90b5a85fb",
# :firstname=>"linda",
# :lastname=>"smith"}

Update the document

my_database.update_doc("linda", firstname: "nancy", lastname: "drew", _rev: "5-74894db03ef6d22e6a0e4ef90b5a85fb")
#=> {:ok=>true, :id=>"linda", :rev=>"6-950d16c8c39daa77fad11de85b9467fc"}

update_doc replaces the old document with the new data. A revision (_rev) must be provided in the data. The resulting document after the update will be

{
   "_id": "linda",
   "_rev": "6-950d16c8c39daa77fad11de85b9467fc",
   "firstname": "nancy",
   "lastname": "drew"
}

Edit parts of a document

my_database.edit_doc!("linda", lastname: "brown", phone: "777-777-7777")
#=> {:ok=>true, :id=>"linda", :rev=>"7-e44206dd09d2740171576e5867fff7a1"}

The edited version of the document is now:

{
   "_id": "linda",
   "_rev": "7-e44206dd09d2740171576e5867fff7a1",
   "firstname": "nancy",
   "lastname": "brown",
   "phone": "777-777-7777"
}

Working with Desgin Documents and views

Create a design document

design_doc = {
 language: "javascript",
 views: {
   by_gender: {
     map: "function(doc){ if(doc.gender) emit(doc.gender); }"
   }
 }
}
my_database.create_doc "_design/my_doc", design_doc
#=> {:ok=>true, :id=>"_design/my_doc", :rev=>"1-4939535c4d51fb5bcbc9b32e2f58e755"}

Query a permanent view

my_database.view("_design/my_doc", "by_gender")
#=> {:total_rows=>7,
# :offset=>0,
# :rows=>
#  [{:id=>"christina", :key=>"female", :value=>nil},
#  {:id=>"lisa", :key=>"female", :value=>nil},
#  {:id=>"nancy", :key=>"female", :value=>nil},
#  {:id=>"susan", :key=>"female", :value=>nil},
#  {:id=>"james", :key=>"male", :value=>nil},
#  {:id=>"kevin", :key=>"male", :value=>nil},
# {:id=>"martin", :key=>"male", :value=>nil}]}

The view() method is used to query parmanent views. It takes the design document name and view name as arguments. It can also optionally take the following CouchDB query options in a hash as arguments: key, limit, skip, descending, include_docs, reduce, startkey, starkey_docid, endkey, endkey_docid, inclusive_end, stale, group, group_level.

To query a permanent view by key

my_database.view("_design/my_doc", "by_gender", key: '"male"')
#=> {:total_rows=>7,
# :offset=>4,
# :rows=>
#  [{:id=>"james", :key=>"male", :value=>nil},
#   {:id=>"kevin", :key=>"male", :value=>nil},
#   {:id=>"martin", :key=>"male", :value=>nil}]}

The above example sends a query to the view using the key "male" and returns all documents with "gender" equal to "male".

To include actual documents in the query results, we can add include_docs to the query options

my_database.view("_design/my_doc", "by_gender", key: '"male"', include_docs: true)
#=> {:total_rows=>7,
# :offset=>4,
# :rows=>
#  [{:id=>"james",
#    :key=>"male",
#    :value=>nil,
#    :doc=>
#     {:_id=>"james",
#      :_rev=>"1-56ff4f73369bdf8350615a58e12e4c3b",
#      :firstname=>"james",
#      :state=>"new york",
#      :gender=>"male",
#      :city=>"manhattan",
#      :age=>23}},
#   {:id=>"kevin",
#    :key=>"male",
#    :value=>nil,
#    :doc=>
#     {:_id=>"kevin",
#      :_rev=>"1-3c6381603d9f15cb966948eb29218cf7",
#      :firstname=>"kevin",
#      :state=>"new york",
#      :gender=>"male",
#      :city=>"bronx",
#      :age=>37}},
#   {:id=>"martin",
#    :key=>"male",
#    :value=>nil,
#    :doc=>
#     {:_id=>"martin",
#      :_rev=>"1-41956cd527d75643171919731abd97c0",
#      :firstname=>"martin",
#      :state=>"new york",
#      :gender=>"male",
#      :city=>"manhattan",
#      :age=>29}}]}

To return results in descending order:

my_database.view("_design/my_doc", "by_gender", key: '"male"', descending: true)
#=> {:total_rows=>7,
# :offset=>0,
# :rows=>
#  [{:id=>"martin", :key=>"male", :value=>nil},
#   {:id=>"kevin", :key=>"male", :value=>nil},
#   {:id=>"james", :key=>"male", :value=>nil}]}

To limit the number of documents returned from the query

my_database.view("_design/my_doc", "by_gender", limit: 4)
#=> {:total_rows=>7,
# :offset=>0,
# :rows=>
#  [{:id=>"christina", :key=>"female", :value=>nil},
#   {:id=>"lisa", :key=>"female", :value=>nil},
#   {:id=>"nancy", :key=>"female", :value=>nil},
#   {:id=>"susan", :key=>"female", :value=>nil}]}

Skip some documents in the query

my_database.view("_design/my_doc", "by_gender", skip: 2)
#=> {:total_rows=>7,
# :offset=>2,
# :rows=>
#  [{:id=>"nancy", :key=>"female", :value=>nil},
#   {:id=>"susan", :key=>"female", :value=>nil},
#   {:id=>"james", :key=>"male", :value=>nil},
#   {:id=>"kevin", :key=>"male", :value=>nil},
#   {:id=>"martin", :key=>"male", :value=>nil}]}

Query views by startkey and endkey

design_doc = {
 language: "javascript",
 views: {
   people_by_age: {
     map: "function(doc){ if(doc.age) emit(doc.age); }"
   }
 }
}
my_database.create_doc "_design/ages", design_doc

my_database.view("_design/ages", "people_by_age", startkey: 20, endkey: 29)
#=> {:total_rows=>7,
# :offset=>0,
# :rows=>
#  [{:id=>"christina", :key=>22, :value=>nil},
#   {:id=>"james", :key=>23, :value=>nil},
#   {:id=>"nancy", :key=>25, :value=>nil},
#   {:id=>"martin", :key=>29, :value=>nil}]}

The above returns documents with age between 20 and 29.

Another example to return documents with age 31 and over

my_database.view("_design/ages", "people_by_age", startkey: 31)
#=> {:total_rows=>7,
# :offset=>4,
# :rows=>
#  [{:id=>"lisa", :key=>31, :value=>nil},
#   {:id=>"susan", :key=>35, :value=>nil},
#   {:id=>"kevin", :key=>37, :value=>nil}]}

Working with compound startkey and endkey

my_database.view("_design/gender_city", "people_by_gender_and_city", startkey: ["female", "bronx", 25].to_s, endkey: ["female", "bronx", 25].to_s)
#=> {:total_rows=>6,
# :offset=>1,
# :rows=>[{:id=>"nancy", :key=>["female", "bronx", 25], :value=>nil}]}

Dynamic Queries

Dynamic queries can be performed on documents using the where() helper method, example to fetch all documents that match the key/value pairs "bronx", gender: "female"

my_database.where(city: "bronx", gender: "female")
#=> [{:_id=>"christina",
#  :_rev=>"1-e9782aa92f7d88eb5dc5e1a878c8e193",
#  :firstname=>"christina",
#  :state=>"new york",
#  :gender=>"female",
#  :city=>"bronx",
#  :age=>22},
# {:_id=>"nancy",
#  :_rev=>"1-44ac471d9e6433eaa6e67607c7a175c9",
#  :firstname=>"nancy",
#  :state=>"new york",
#  :gender=>"female",
#  :city=>"bronx",
#  :age=>25}]

To fetch all documents that match the key/value pairs "new york", fullname: ["susan", "Lee"]

my_database.where(state: "new york", fullname: ["susan", "Lee"])
#=> [{:_id=>"susan",
#  :_rev=>"1-11b05eacc247b8541fa6c659f26447de",
#  :firstname=>"susan",
#  :state=>"new york",
#  :gender=>"female",
#  :age=>35,
#  :fullname=>["susan", "Lee"]}]

Similar to view(), the where() method also supports options skip, limit, descending. Example to return documents in descending order;

my_database.where({city: "bronx", gender: "female"}, descending: true)
#=> [{:_id=>"nancy",
#  :_rev=>"1-44ac471d9e6433eaa6e67607c7a175c9",
#  :firstname=>"nancy",
#  :state=>"new york",
#  :gender=>"female",
#  :city=>"bronx",
#  :age=>25},
# {:_id=>"christina",
#  :_rev=>"1-e9782aa92f7d88eb5dc5e1a878c8e193",
#  :firstname=>"christina",
#  :state=>"new york",
#  :gender=>"female",
#  :city=>"bronx",
#  :age=>22}]

Limit to one result

my_database.where({city: "bronx", gender: "female"}, limit: 1)
#=> [{:_id=>"christina",
#  :_rev=>"1-e9782aa92f7d88eb5dc5e1a878c8e193",
#  :firstname=>"christina",
#  :state=>"new york",
#  :gender=>"female",
#  :city=>"bronx",
#  :age=>22}]

Calling the where() method, for the first time creates a view in the database for provided keys. Subsequent calls to where() will simply query the previously created view and return the documents. For example calling the method below:

my_database.where(city: "bronx", gender: "female")

Will add the following document to the database,

{
   "_id": "_design/city_gender_keys_finder",
   "_rev": "1-41fb3b17c8b99be176928e3ea5588935",
   "language": "javascript",
   "views": {
       "find_by_keys_city_gender": {
           "map": "function(doc){ if(doc.city && doc.gender) emit([doc.city,doc.gender]);}"
       }
   }
}

And then query it with:

/_design/city_gender_keys_finder/_view/find_by_keys_city_gender?endkey=["bronx", "female"]&include_docs=true&startkey=["bronx", "female"]

Subsequent method calls will simply query the view and return the documents. where() is just a convienient helper method.

Security Object:

Please note that this does not work with CouchDB 2, only works with CouchDB v1.6.1 and less. To set the security object for the database:

security_settings = {
  admins: {names: ["david"], roles: ["admin"]},
  readers: {names: ["david"],roles: ["admin"]}
}

my_database.security_object = security_settings
#=> {:admins=>{:names=>["david"], :roles=>["admin"]},
# :readers=>{:names=>["david"], :roles=>["admin"]}}

To retrieve the security object at anytime:

my_database.security_object
#=> {:admins=>{:names=>["david"], :roles=>["admin"]},
# :readers=>{:names=>["david"], :roles=>["admin"]}}

CouchDB Configuration

Please note that this does not work with CouchDB 2, only works with CouchDB v1.6.1 and less. CouchDB's configuration settings can be set using the set_config() method:

config = Leanback::Couchdb.new
config.set_config("section", "option", '"value"')

For example to set couchDB's couch_httpd_auth timeout value:

config.set_config("couch_httpd_auth", "timeout", '"1600"')
#=> true

This sets the CouchDB auth timeout to 1600 seconds.

To retrieve the configuration:

config.get_config("couch_httpd_auth", "timeout")
#=> "\"1600\"\n"

To delete a configuration:

config.delete_config("section", "option")
#=> true

A more useful example to add an admin user to couchDB with username and password;

config.set_config("admins", username = "james", password = '"abc123"')
#=> true

This will add a CouchDB admin with username james, and password abc123. If couchDB was in admin party mode, this would end the party.

Exception handling

For CouchDB errors Leanback raises a Leanback::Exception object. This object has a response attribute which is a hash that contains the details of the error response from CouchDB. See an example below

begin
  my_database.get_doc('dontexist')
rescue Leanback::CouchdbException => e
  puts e.response.inspect
#=> {:error=>"not_found", :reason=>"missing"}
  puts e.response[:error].inspect
#=> "not_found"
  puts e.response[:reason].inspect
#=> "missing"
end

API Specification

#JSON result keys are automatically symoblized:
#returns data directly as couchdb returns them unaltered as ruby hash
c = Leanback::Couchdb.new database: xxxxx, username: xxxxx, password: xxxx, address: xxxxx, port: xxxxx

c.create
c.delete
c.create_doc id, {}
c.delete_doc id, rev
c.delete_doc! id
c.update_doc id, {} #hash includes rev
c.edit_doc! id, {}
c.get_doc id

options = { limit: x, skip: x, descending: x}
c.where {}, options

#create a design doc
design_doc = {
 language: "javascript",
 views: {
   get_emails: {
     map: "function(doc){ if(doc.firstname && doc.email) emit(doc.id,{Name: doc.firstname, Email: doc.email}); }"
   }
 }
}
c.create_doc "_design/my_doc", design_doc

#query a view
options = { limit: x, key: x, start_key: x, end_key: x, skip: x, descending: x, include_docs: boolean}
design_doc_name = "_design/my_doc"
view_name = "get_emails"
c.view design_doc_name, view_name, options

security_settings = {
  admins: {names: ["david"], roles: ["admin"]},
  readers: {names: ["david"],roles: ["admin"]}
}

c.security_object = security_settings

c.security_object
#=> {:admins=>{:names=>["david"], :roles=>["admin"]},
#    :readers=>{:names=>["david"], :roles=>["admin"]}}

c = Leanback::Couchdb.new
c.set_config("couch_httpd_auth", "timeout", '"900"')
c.get_config("couch_httpd_auth", "timeout")
#=> "\"900\"\n"
c.delete_config("section", "option")

Supported Rubies

jruby-19mode, MRI 1.9.3 - 2.x

License

MIT License.

Copyright (c) 2014 Obi Akubue.