couchapps - o'reilly mediaassets.en.oreilly.com/1/event/61/couchapps with... · can run on...
TRANSCRIPT
CouchApps
+
=Blog: http://bradley-holt.com
@BradleyHolt (http://twitter.com/BradleyHolt)[email protected]
Schema-less “self-contained” JSON documents—no tables, just documents
RESTful HTTP API is simple to integrate with
Custom-written MapReduce “views” index your data for fast retrieval
Multi-master replication can happen continuously, in any direction, and/or whenever nodes can connect
Can run on anything from a server to a mobile device (CouchDB can be embedded in Android and iOS apps)
Benefits
Trade-OffsNo ad-hoc queries (you can use temporary views, but they’re slow on large data sets)
Thinking in terms of MapReduce views can take some time to get used to
No transactions across document boundaries
Your data will be “eventually consistent” and conflicts can occur (but can be resolved)
Mapping Document Titlesfunction(doc) { // JSON object representing a doc to be mapped if (doc.title) { // make sure this doc has a title emit(doc.title); // emit the doc’s title as the key }}
key id value
"Building iPhone Apps with HTML, CSS, and JavaScript" "978-0-596-80579-1" null
"CouchDB: The Definitive Guide"
"978-0-596-15589-6" null
"DocBook: The Definitive Guide"
"978-1-565-92580-9" null
"RESTful Web Services" "978-0-596-52926-0" null
Mapped Document Titles
Mapping Document Authorsfunction(doc) { // JSON object representing a doc to be mapped if (doc.authors) { // make sure this doc has an authors field for (var i in doc.authors) { emit(doc.authors[i]); // emit each author as the key } }}
Mapped Authorskey id value
"J. Chris Anderson" "978-0-596-15589-6" null
"Jan Lehnardt" "978-0-596-15589-6" null
"Jonathan Stark" "978-0-596-80579-1" null
"Leonard Muellner" "978-1-565-92580-9" null
"Leonard Richardson" "978-0-596-52926-0" null
"Noah Slater" "978-0-596-15589-6" null
"Norman Walsh" "978-1-565-92580-9" null
"Sam Ruby" "978-0-596-52926-0" null
The same-origin policy dictates that your HTML and AJAX data (JSON) must be retrieved from the same origin.
Create Database$.couch.db("mydb").create({ success: function(data) { console.log(data); }});
PUT http://localhost:5984/mydb 201 Created
{"ok":true}
Save New Document$.couch.db("mydb").saveDoc({}, { success: function(data) { console.log(data); }});
POST http://localhost:5984/mydb 201 Created
{ "ok":true, "id":"e15d848571c8a0352e94738ba6018790", "rev":"1-967a00dff5e02add41819138abb3284d"}
Save Updated Documentvar doc = { _id: "0a72c9c36bd169818dc97ed18b000aa4", _rev: "1-967a00dff5e02add41819138abb3284d", title: "CouchApps"};$.couch.db("mydb").saveDoc(doc, { success: function(data) { console.log(data); }});
Save Updated Document (cont’d)PUT http://localhost:5984/mydb/0a72c9c36bd169818dc97ed18b000aa4 201 Created
{ "ok":true, "id":"0a72c9c36bd169818dc97ed18b000aa4", "rev":"2-516027e3179a22a22e06874c374e8ef0"}
Remove Documentvar doc = { _id: "0a72c9c36bd169818dc97ed18b000aa4", _rev: "2-516027e3179a22a22e06874c374e8ef0"};$.couch.db("mydb").removeDoc(doc, { success: function(data) { console.log(data); }});
Remove Document (cont’d)DELETE http://localhost:5984/mydb/0a72c9c36bd169818dc97ed18b000aa4?rev=2-516027e3179a22a22e06874c374e8ef0 200 OK
{ "id":0a72c9c36bd169818dc97ed18b000aa4 "ok":true, "rev":"3-e9a5aa1c486eee23c84fa028bc904991"}
Changes (Long Polling)$.couch.db("mydb").changes().onChange(function(data) { console.log(data);});
GET http://localhost:5984/mydb/_changes?heartbeat=10000&feed=longpoll&since=34 200 OK
{ "results":[ { "seq":35, "id":"d12ee5ea1df6baa2b06451f44a01b7b5", "changes":[ { "rev":"1-967a00dff5e02add41819138abb3284d" } ] } ], "last_seq":35}
BenefitsStreamlining of your codebase (no middle tier)
Same language on both the client and server (JavaScript)
Show and list functions let you generate HTML from within CouchDB, if you don’t want to rely on JavaScript
Replication of both data and code together
Deploy/replicate an application along with its data
Goes “with the grain” of the web
“Ground Computing”Replication filters allow you to replicate relevant data to a user
Local data means faster access to the user’s data
Offline access
Data portability
Decentralization—no need for a canonical database
Potentially gives more control to the user over his or her own data
What about the HTML5 Web Storage API?Allows persistent storage of key/value pairs
Enjoys significant cross-browser support
Lacks indexed queries
No replication features
IndexedDB might help, but is not part of the HTML5 specification and is only implemented in a limited number of browsers
Need to look outside the HTML5 specification if you need more than just a key/value storage
Trade-OffsTooling could use some improvement
Terminology still unclear—CouchApp can mean many different things
Must use JavaScript for the presentation tier
Pure JavaScript applications may have SEO issues
Show and list functions must be side-effect free, which can be limiting
UsesMobile applications that require offline access
Multi-device applications (e.g. address book, tasks)
Peer-to-peer collaboration applications
Distributed social networking
Any application that stores “documents”—CMS, wiki, etc.
Geospatial applications (via GeoCouch)
Unescaped VariablesTemplate:<p>Hello, {{{name}}}.</p>
Hash:{ "name": "<em>Bradley</em>"}
Output:<p>Hello, <em>Bradley</em>.</p>
False ValuesTemplate:<p>Shown.</p>{{#show}}<p>Not shown.</p>{{/show}}
Hash:{ "show": false}
Output:<p>Shown.</p>
Empty ListsTemplate:<p>Shown.</p>{{#show}}<p>Not shown.</p>{{/show}}
Hash:{ "role": "admin"}
Output:<p>Shown.</p>
Non-Empty ListsTemplate:{{#users}} <p>{{name}}</p>{{/users}}
Hash:{ "users": [ { "name": "Bradley" }, { "name": "Jason" }, ]}
Output: <p>Bradley</p> <p>Jason</p>
Non-False (and Not a List) ValuesTemplate:{{#user}} <p>Logged in as {{name}}</p>{{/user}}
Hash:{ "user": { "name": "Bradley" }}
Output: <p>Logged in as Bradley</p>
Descends into the “user” context for the section
Inverted SectionsTemplate:{{^user}} <p>Not logged in</p>{{/user}}
Hash:{ "user": false}
Output: <p>Not logged in</p>
<p id="foo"></p><script type="text/javascript">$("#foo").evently({ click: function() { $(this).text("Clicked"); }})</script>
DOM Events
Custom Events<p id="user"></p><script type="text/javascript">$("#user").evently({ login: function(e, user) { $(this).text(user); }})$("#user").trigger("login", "Bradley");</script>
$("#user").evently({ _init: { async: function(callback) { $.couch.session({ success: function(data) { callback(data); } }); }, mustache: "<span>{{#name}}Logged in as {{name}}{{/name}}{{^name}}Not logged in{{/name}}</span>", data: function(data) { return data.userCtx; } }});
Widgets
jQuery.data Syntactic Sugar// $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/function $$(node) { var data = $(node).data("$$"); if (data) { return data; } else { data = {}; $(node).data("$$", data); return data; }};
$$ ExamplesThis:$("#my-node").data("key", "value");
Becomes:$$("#my-node").key = "value";
This:var value = $("#my-node").data("key");
Becomes:var value = $$("#my-node").key;
Each CouchApp is entirely self-contained in one design document
Design documents live alongside other documents, but have a special naming convention of _design/<name>
A database can have multiple CouchApps (and/or design documents) and CouchDB server can host multiple databases
CouchApps & Design Documents
Can contain:•View definitions
•Map functions•Reduce functions
•Show functions•List functions•Document update handlers•Document update validation functions•Rewrite definitions•Any other fields you want, just like with any other document•Attachments, just like with any other document
ID follows the form of: _design/<design_name>
What’s in a design document?
function(doc) { if (doc.type == "comment") { emit([doc.topic, doc.at], doc); }};
views/recent-comments/map.js
The recent comments view’s map function
Wiring the Evently Widget <div id="comments"></div> … <script type="text/javascript" charset="utf-8"> var opts = {}; … $.couch.app(function(app) { … $$("#wiki").docid = {{docid}}; $$("#wiki").title = {{title_json}}; $("#comments").evently("comments", app); … }, opts); </script>
Note that this template gets rendered server-side
evently/comments/_init/query.jsfunction() { var docid = $$("#wiki").docid; return { view : "recent-comments", endkey : [docid, {}], startkey : [docid] };};
Builds a query to a CouchDB view
evently/comments/_init/data.jsfunction(view) { var docid = $$("#wiki").docid, linkup = $$("#wiki").app.require("vendor/couchapp/lib/linkup"); return { topic : docid, title : $$("#wiki").title, comments : view.rows.map(function(r) { var by = r.value.by || {}; return { gravatar_url : by.gravatar_url, by : by.nickname, at : r.key[1], comment : linkup.encode(r.value.comment) // move to view } }) }};
Handles the resultant data from the CouchDB view and prepares it for use by the Mustache template
evently/comments/_init/mustache.html<h3>Comments about {{title}}</h3><ul> {{#comments}} <li> <div class="avatar"> {{#gravatar_url}}<img src="{{gravatar_url}}"/>{{/gravatar_url}} <div class="name"> {{by}} </div> </div> <p>{{{comment}}}</p> <em><span class="date">{{at}}</span></em> <div class="clear"></div> </li> {{/comments}}</ul>
The widget’s Mustache template
evently/comments/_init/mustache.html (cont’d)<form> <label>Comment (w/ <a href="http://github.com/couchapp/couchapp" target="_new">linkup</a>*)</label> <br/> <!-- <input type="text" size="60" name="comment"> --> <textarea name="comment" rows="4" cols="60"></textarea> <input type="hidden" name="topic" value="{{topic}}"> <p><input type="submit" value="Save Comment"></p></form>
evently/comments/_init/after.jsfunction() { $(".date").prettyDate();};
This is executed after everything else is done
evently/comments/_init/selectors/form/submit.jsfunction() { var form = $(this), app = $$(form).app, f = form.serializeObject(); f.type = "comment"; f.at = new Date(); f.by = $$("#profile").profile; app.db.saveDoc(f, { success : function() { $("#comments").trigger("_init"); } }) return false;};
Creates a selector based on the file path, and calls the function when its namesake event is fired
[ … { "from" : "/pages/comments", "to" : "_list/comments/all-comments", "query" : { "descending" : true, "limit" : 20 } }, …]
rewrites.json
Rewrites to the comments list, using the all-comments view
CouchDB supports virtual hosts via the “vhosts” server configuration section. This lets you route to a rewrite handler based on the Host HTTP header.{ "example.com": "/db/_design/tutorial/_rewrite", "www.example.com": "/db/_design/tutorial/_rewrite"}
function(doc) { if (doc.type == "comment" && doc.comment) { emit(doc.at, doc); }};
views/all-comments/map.js
lists/comments.jsfunction() { var row, ddoc = this, mustache = require("vendor/couchapp/lib/mustache"), markdown = require("vendor/couchapp/lib/markdown"), data = { title : "All Comments", site_title : this.couchapp.name, path : "/pages/comments", comments : [] }; provides("html", function() { while (row = getRow()) { log(row); data.comments.push(row.value) } send(mustache.to_html(ddoc.templates.comments, data, ddoc.templates.partials)); });};
templates/comments.html{{>header}}<ul> {{#comments}} <li> <span class="date">{{at}}</span>: {{#by}}{{name}} {{/by}} {{^by}}Unknown {{/by}} (<a href="../page/{{topic}}">{{topic}}</a>) <span class="idontknow">{{comment}}</span> </li> {{/comments}} </ul> </body> …</html>
The {{>header}} syntax is for partials
templates/partials/header.html<!DOCTYPE html><html> <head> <title>{{title}} - {{site_title}}</title> <link rel="stylesheet" href="../style/base-min.css" type="text/css"> <link rel="stylesheet" href="../style/main.css" type="text/css"> </head> <body> <div id="header"> <div id="account"></div> <h1><a href="../page/index">{{site_title}}</a>: <a href="{{path}}">{{title}}</a></h1> </div> <div id="profile"></div>
function (newDoc, oldDoc, userCtx, secObj) { var v = require("vendor/couchapp/lib/validate").init(newDoc, oldDoc, userCtx, secObj); …}
Function Definition
Require Login to Make Changes if (!userCtx.name) { // this could be configurable based on secObj v.unauthorized("please login to make changes"); }
Only Let Admins Delete // only admin may delete if (newDoc._deleted) { v.unauthorized("only admin may delete docs"); }
$ couchapp generate tutorial2011-06-15 16:25:18 [INFO] /tutorial generated.
$ cd tutorial
Generate a CouchApp
Generated Files$ tree -FL 1.├── README.md├── _attachments/├── _id├── couchapp.json├── evently/├── language├── lists/├── shows/├── updates/├── vendor/└── views/
7 directories, 4 files
README.mdGenerated by CouchAppCouchApps are web applications which can be served directly from CouchDB. This gives them the nice property of replicating just like any other data stored in CouchDB. They are also simple to write as they can use the built-in jQuery libraries and plugins that ship with CouchDB.More info about CouchApps here.Deploying this appAssuming you just cloned this app from git, and you have changed into the app directory in your terminal, you want to push it to your CouchDB with the CouchApp command line tool…
Update this file to be a README for your CouchApp
_attachments$ tree -F _attachments_attachments├── index.html└── style/ └── main.css
1 directory, 2 files
_attachments/index.html will be the starting point for your CouchApp_attachments/style/main.css should be self-explanatory
_id$ cat _id_design/tutorial
This is the name of the design document in which your CouchApp will live
couchapp.json$ cat couchapp.json{ "name": "Name of your CouchApp", "description": "CouchApp"}
Metadata about the CouchApp
evently$ tree -F eventlyevently├── items/│ └── _changes/│ ├── data.js│ ├── mustache.html│ └── query.json└── profile/ └── profileReady/ ├── mustache.html └── selectors/ └── form/ └── submit.js
6 directories, 5 files
User made widgets, which an override vendor widgets
updates$ tree -F updatesupdates
0 directories, 0 files
Contains CouchDB update functions, which can transform arbitrary HTTP requests into document updates
vendor$ tree -FL 2 vendorvendor└── couchapp/ ├── _attachments/ ├── evently/ ├── lib/ └── metadata.json
4 directories, 1 file
Third-party “vendor” code that ships with CouchApp as a convenience, as well as some useful Evently widgets
views$ tree -F viewsviews└── recent-items/ └── map.js
1 directory, 1 file
Function definitions for CouchDB MapReduce views
views/recent-items/map.js$ cat views/recent-items/map.jsfunction(doc) { if (doc.created_at) { emit(doc.created_at, doc); }};
Deploy to Localhost$ couchapp push tutorial2011-06-16 11:16:05 [INFO] Visit your CouchApp here:http://127.0.0.1:5984/tutorial/_design/tutorial/index.html
<div id="profile"></div><script type="text/javascript">$("#profile").html('<p><a href="#/profile">View Profile</a></p>');$("#profile").bind("profile", function() { $(this).html("<p>Profile goes here…</p>");});$("#profile").pathbinder("profile", "/profile");</script>
Basic Usage
Path Parameters<div id="profile"></div><script type="text/javascript">$("#profile").html('<p><a href="#/profile/Bradley">View Bradley\'s Profile</a></p>');$("#profile").bind("profile", function(e, params) { $(this).html("<p>Viewing " + params.id + "'s profile</p>");});$("#profile").pathbinder("profile", "/profile/:id");</script>
Pathbinder, Evently & Mustache<div id="profile"></div><script type="text/javascript">$("#profile").evently({ _init: { path: "/", mustache: '<p><a href="#/profile/Bradley">View Profile</a></p>' }, viewProfile: { path: "/profile/:id", mustache: '<p>Viewing {{id}}\'s profile; <a href="#/">Back</a></p>', data: function(e, params) { return params; } }});</script>
Command-line tool, written for Node.js
Simpler folder structure
Not compatible with the Python version
Links:•https://github.com/mikeal/node.couchapp.js•http://japhr.blogspot.com/2010/04/quick-intro-to-
nodecouchappjs.html•http://vimeo.com/26147136
node.couchapp.js
soca“Sammy On Couch App” or “Sittin’ on a Couch App”
Command-line tool written in Ruby
Uses Sammy.js
https://github.com/quirkey/soca
ReupholsterA simple way to get started developing CouchApps
http://reupholster.iriscouch.com/reupholster/_design/app/index.html
CouchApp Wikihttp://couchapp.org/
CouchDB Wiki http://wiki.apache.org/couchdb/
CouchDB: The Definitive Guide by J. Chris Anderson, Jan Lehnardt, and Noah Slater (O’Reilly) 978-0-596-15589-6
Writing and Querying MapReduce Views in CouchDB by Bradley Holt (O’Reilly)978-1-449-30312-9
Scaling CouchDB by Bradley Holt (O’Reilly)063-6-920-01840-7
Beginning CouchDB by Joe Lennon (Apress) 978-1-430-27237-3
CouchDB & CouchApp Resources
Thank YouBlog: http://bradley-holt.com
@BradleyHolt (http://twitter.com/BradleyHolt)[email protected]
Copyright © 2011 Bradley Holt. All rights reserved.