Pragmatism in the real world

Kitura tutorial part 2: CouchDB

In part one we set up Swift and build a “hello world” Kitura application, so we are well placed to build an API that actually does something. This API will manage a list of books. Clearly, we’ll need to store our books somewhere and I’ve chosen CouchDB for this tutorial.

To access CouchDB, you use HTTP which makes it very easy to work with curl or any other HTTP client. The docs are useful!

This part of the tutorial concentrates solely on setting up CouchDB, populating it and ensuring that we can operate with it.

The data

For this project we’re going to manage a list of books. Each book will have the following fields:

  • Title
  • Author
  • ISBN

This isn’t a complicated database!

Install CouchDB

On OS X, you can install CouchDB using homebrew:

$ brew install couchdb

On Linux, install via apt:

$ sudo add-apt-repository ppa:couchdb/stable -y
$ sudo apt-get update
$ sudo apt-get install couchdb

You now have a local CouchDB installation on http://localhost:5984.

The next step is to set up an admin user:

$ curl -i -X PUT http://localhost:5984/_config/admins/{username} -d '"{password}"'

Select any username and password that you like, but don’t forget it! I’m going to use rob/123456 throughout the rest of the tutorial. If the command succeeded, it returns an empty string.

Populate CouchDB with some data

To populate our database, we need some data, a design document and a bash script to do the work. We’ll create three files in a new directory called scripts:

  • scripts/books.json
  • scripts/main_design.json
  • scripts/seed_couchdb.sh

These are the files. Create them in scripts.

scripts/books.json

{
  "docs": [
    {"type": "book", "title": "Gregor the Overlander", "author": "Suzanne Collins", "isbn": "9780439678131"},
    {"type": "book", "title": "Gregor and the Prophecy of Bane", "author": "Suzanne Collins", "isbn": "9780439650762"},
    {"type": "book", "title": "Gregor and the Curse of the Warmbloods", "author": "Suzanne Collins", "isbn": "9780439656245"},
    {"type": "book", "title": "Gregor and the Marks of Secret", "author": "Suzanne Collins", "isbn": "9780439791465"},
    {"type": "book", "title": "Gregor and the Code of Claw", "author": "Suzanne Collins", "isbn": "9780439791441"},
    {"type": "book", "title": "The Hunger Games", "author": "Suzanne Collins", "isbn": "9780439023528"},
    {"type": "book", "title": "Catching Fire", "author": "Suzanne Collins", "isbn": "9780545227247"},
    {"type": "book", "title": "Mockingjay", "author": "Suzanne Collins", "isbn": "9780439023511"},
    {"type": "book", "author": "Anne McCaffrey", "title": "Dragonflight",  "isbn": "9780345335463"},
    {"type": "book", "author": "Anne McCaffrey", "title": "Dragonquest",  "isbn": "9780345022455"},
    {"type": "book", "author": "Anne McCaffrey", "title": "The White Dragon",  "isbn": "9780345275677"},
    {"type": "book", "author": "Anne McCaffrey", "title": "Dragonsong",  "isbn": "9780689305078"},
    {"type": "book", "author": "Anne McCaffrey", "title": "Dragonsinger",  "isbn": "9780689305702"},
    {"type": "book", "author": "Anne McCaffrey", "title": "Dragondrums",  "isbn": "9780689306853"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "Mindstar Rising",  "isbn": "9780330537742"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "A Quantum Murder",  "isbn": "9780330537759"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "The Nano Flower",  "isbn": "9780330537810"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "The Reality Dysfunction",  "isbn": "9781447208570"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "The Neutronium Alchemist",  "isbn": "9781447208587"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "The Naked God",  "isbn": "9781447208594"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "Misspent Youth",  "isbn": "9781447224082"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "Pandora's Star",  "isbn": "9781447279662"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "Judas Unchained",  "isbn": "9781447279679"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "The Dreaming Void",  "isbn": "9781447208563"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "The Temporal Void",  "isbn": "9780330507882"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "The Evolutionary Void",  "isbn": "9780330443173"},
    {"type": "book", "author": "Peter F. Hamilton", "title": "The Abyss Beyond Dreams",  "isbn": "9780230769465"}
  ]
}

This JSON file contains a ‘docs’ array containing a set of objects for each book in our database. As CouchDB is a document database, we don’t have tables are we would do in a standard RDBMS, so I’ve also chosen to have an additional property called type which is set to book. We don’t techncially need this, but it makes it easier to write our views as we can test for the type to ensure we have a book document.

scripts/main_design.json

{
  "id": "_design/main_design",
  "views": {
    "all_books": {
      "map" : "function(doc) {  if (doc.type == 'book') { emit([doc.author, doc.title], doc); } }"
    }
  },
  "language": "javascript"
}

The main design document contains our list of views. In this case we have a single view called all_books which will emit all records of type “book”. In CouchDB, each view is a JavaScript function that outputs the documents that match a given criteria. For all_books, we simply check that the type is book and if it is, emit the document using the compound key of author and title so that the list is ordered by author, then book title.

scripts/seed_couchdb.sh

#!/bin/bash -e

# This script is inspired from similar scripts in the Kitura BluePic project

# Find our current directory
current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Parse input parameters
database=bookshelf_db
url=http://localhost:5984

for i in "$@"
do
case $i in
    --username=*)
    username="${i#*=}"
    shift
    ;;
    --password=*)
    password="${i#*=}"
    shift
    ;;
    --url=*)
    url="${i#*=}"
    shift
    ;;
    *)
    ;;
esac
done

if [ -z $username ]; then
  echo "Usage:"
  echo "seed_couchdb.sh --username= --password= [--url=]"
  echo "    default for --url is '$url'"
  exit
fi


# delete and create database to ensure it's empty
curl -X DELETE $url/$database -u $username:$password
curl -X PUT $url/$database -u $username:$password

# Upload design document
curl -X PUT "$url/$database/_design/main_design" -u $username:$password \
    -d @$current_dir/main_design.json

# Create data
curl -H "Content-Type: application/json" -d @$current_dir/books.json \
    -X POST $url/$database/_bulk_docs -u $username:$password

echo
echo "Finished populating couchdb database '$database' on '$url'"

This is quite a long script as half of it concerned with parsing the command line to collect the username, password and url to our CouchDB instance. While we could hard-code these details, it’s convient to be able to re-use this script with other CouchDB instances hosted on the Internet.

We also need to make the seed_couchdb.sh file executable:

$ chmod a+x scripts/seed_couchdb.sh

Seed the database

Now that we have our JSON data files and a bash script to use, we can populate our CouchDB database:

$ scripts/seed_couchdb.sh --username=rob --password=123456

(use the username and password that you set up earlier!)

The script will output some JSON for each curl command run and finish with this line:

Finished populating couchdb database 'bookshelf_db' on 'http://localhost:5984'

Test that the data exists

You can check the data has been populated into your database either by using Futon by opening http://localhost:5984/_utils in your browser. More easily, we can also use curl:

$ curl -s http://rob:123456@localhost:5984/bookshelf_db | jq

which will output something like:

{
  "db_name": "bookshelf_db",
  "doc_count": 28,
  "doc_del_count": 0,
  "update_seq": 28,
  "purge_seq": 0,
  "compact_running": false,
  "disk_size": 8296,
  "data_size": 7130,
  "instance_start_time": "1470989118158273",
  "disk_format_version": 6,
  "committed_update_seq": 28
}

(The jq tool nicely formats the JSON to make it readable)

The key information is that the doc_count is 28. This is our 27 books plus one design document.

Database access via curl

We can also retrieve the output of the design document’s view:

$ curl -s http://rob:123456@127.0.0.1:5984/bookshelf_db/_design/main_design/_view/all_books | jq

This will return all the books in the database.

Similarly we can add a new book by POSTing to the database:

$ curl -s -X POST -H "Content-type: application/json" http://rob:123456@127.0.0.1:5984/bookshelf_db \
    -d '{"type": "book", "author": "Julie Czerneda", "title": "Beholder\u0027s Eye",  "isbn": "9780886779863"}' | jq

which will output something like:

{
  "ok": true,
  "id": "feab44ebffb57f3e32c93c6e06065a62",
  "rev": "1-0794e8aebf9381475611b6e5bf385dc4"
}

We can edit a book, by PUTing a complete representation to the record’s URL and including the current revision in the body as the _rev key:

$ curl -s -X PUT -H "Content-type: application/json" http://rob:123456@127.0.0.1:5984/bookshelf_db/feab44ebffb57f3e32c93c6e06065a62 \
    -d '{"type": "book", "author": "Julie E. Czerneda", "title": "Beholder\u0027s Eye",  "isbn": "9780886779863", "_rev": "1-0794e8aebf9381475611b6e5bf385dc4"}' | jq

The output is the same as for creating a record, except that the revision number is different:

{
  "ok": true,
  "id": "feab44ebffb57f3e32c93c6e06065a62",
  "rev": "2-f413d2fdaa416a92e66f94f3767b07ae"
}

To delete the a book, we use the DELETE HTTP verb:

$ curl -s -X DELETE -H "Content-type: application/json" http://rob:123456@127.0.0.1:5984/bookshelf_db \
    -d '{"id": "feab44ebffb57f3e32c93c6e06065a62", "rev": "2-f413d2fdaa416a92e66f94f3767b07ae"}' | jq

We now have a working CouchDB instance with some data in it so we can move along to part 3 and retrieve it in our Swift application!

Aside: Update the docker-composer file

If you’re using docker compose, then update the docker-compose.yml file to start up a CouchDB container and also seed the database:

docker-compose.yml

db:
  image: couchdb
  ports:
    - "5984:5984"
  environment:
    - COUCHDB_USER=rob
    - COUCHDB_PASSWORD=123456

seed-db:
  image: ibmcom/swift-ubuntu
  volumes:
    - .:/root/BookshelfAPI
  command: BookshelfAPI/scripts/seed_couchdb.sh --username=rob --password=123456 --url=http://db:5984
  links:
    - db

app:
  image: ibmcom/swift-ubuntu
  ports:
    - "8090:8090"
  volumes:
    - .:/root/BookshelfAPI
  command: bash -c "make clean -C BookshelfAPI && make -C BookshelfAPI && BookshelfAPI/.build/debug/BookshelfAPI"
  links:
      - db:localhost

Tutorial navigation

GitHub repository: kitura_bookshelfapi