Permalink
Browse files

couchdb api now dynamically generated.

  • Loading branch information...
1 parent 47d505f commit 110f842b31f633b8c3c5a904860d37a8aae687f6 @dgreisen committed Nov 8, 2011
Showing with 533 additions and 423 deletions.
  1. +66 −52 README.md
  2. +0 −159 bin/generate.js
  3. +1 −6 index.js
  4. +219 −0 lib/api_factory.js
  5. +52 −0 lib/db_factory.js
  6. +194 −0 lib/factory.js
  7. +0 −28 lib/factory_factory.js
  8. +0 −176 lib/template.js
  9. +1 −2 package.json
View
@@ -1,18 +1,19 @@
# YACA #
-## Custom-generated CouchDB api through introspection ##
+## Custom-generated CouchDB api through database introspection ##
-You do not use YACA directly. Instead, you run the script `generate_couch_api` which introspects your CouchDB instance and generates a custom api. You then import that custom API into your node project and make api calls on it. This allows you to make intuitive and easy calls to your DB. for example, to request all documents from the 'master' view of the '_design/app' design document of your 'primary' database you simply make the following call:
+Simply import YACA, and call the factory function. The factory function introspects your CouchDB instance and returns a custom api. This allows you to make intuitive and easy calls to your database, and inspect your database from the commandline. For example, to request all documents from the 'master' view of the '_design/app' design document of your 'primary_db' database you simply make the following call:
- couchdb = require('couchdb') // import our generated api
- couchdb.primary.app.views.master(callback)
- callback = function(error, response, body) {
- console.log(body)
- }
+ api_factory = require('YACA');
+ api_factory(function(error, couchdb_api) {
+ couchdb_api.primary_db.app.views.master(function(error, response, body) {
+ console.log(body)
+ })
+ })
It is that simple.
## General Design ##
-YACA was designed for CouchDB databases where the number of databases and the design documents remain relatively stable. In the future, YACA will be able to introspect DB changes at runtime; however, currently, you must run YACA every time a DB is added/removed, or a design document is modified.
+YACA was designed for CouchDB databases where the number of databases and the design documents remain relatively stable. As such, the structure of the database is cached, and the api is generated from this cached structure unless `update: true` is explicitly passed to the factory. In a future release, YACA will be able to introspect DB changes at will; however, currently, you must create a new couchdb_api instance with the api_factory every time the database is added/removed, or a design document is modified.
YACA is based on Mikeal's [request](https://github.com/mikeal/request/) module. Therefore, with a few exceptions listed below, api commands accept an options
argument and a callback. They return an error, the original response, and the parsed JSON body. Because it is based on request, you can do fancy things like piping, etc. made possible by Mikeal's module. See <https://github.com/mikeal/request> for more info.
@@ -26,22 +27,13 @@ argument and a callback. They return an error, the original response, and the pa
### 1. Generate API ###
First, generate the API by calling:
- generate_couch_api [-f PATH] [-d DB_URL] [-a USERNAME:PASSWORD]
-
- * `PATH` is the location of the file into which to write the couchdb api. If not specified, it will be placed in path/to/YACA/lib/couchdb.js, and will be imported when you `require('YACA')`.
- * `DB_URL` is the url to your CouchDB instance. It defaults to `http://127.0.0.1:5984`. If privileges are needed to view design documents or the root instance, provide username and password as: `http://username:password@host_name`
- * provide `USERNAME:PASSWORD` where `USERNAME` is an admin username and `PASSWORD` is the admin's password, if you wish to be able to access admin-restricted content through the api. *The username and password will be stored in the generated API code.* See [options reference](#options) for more.
-
-You must regenerate the API whenever you add or remove a database, or you modify a design document.
+ api_factory = require('YACA');
+ api_factory(factory_options, callback)
-### 2. Import the generated API ###
-If you used the defaults to generate your custom API, then you can import that api by:
+ * `factory_options` - an optional hash. See [factory options](#factory_options) reference for more.
+ * `callback` - a callback function of the form `callback(error, couchdb_api)`.
- couchdb = require("YACA");
-
-If you specified a different `PATH`, simply import from that file. The rest of this document assumes you have imported the generated api into a variable called`couchdb`.
-
-### 3. Use the API ###
+### 2. Use the API ###
There are four primary API methods, corresponding to the http methods:
* `get`
* `put`
@@ -52,17 +44,17 @@ To make a call against a database:
couchdb.{{database_name}}.{{http_method}}(options, callback)
-or against a particular
+or against a particular handler:
couchdb.{{database_name}}
.{{design_doc_name}}
- .{{handler_name>
+ .{{handler_name}}
.{{method}}
.{{http_method}}(options, callback)
See [options](#options) for what is allowed in options. callback is of the form: `callback(error, response, body)` where `response` is the unadulterated response from the CouchDB server and `body` is a javascript object parsed from the JSON response body.
-There are some [helper functions] described below that have slightly different usage patterns.
+There are some [helper functions](#helpers) described below that have slightly different usage patterns.
### 4. Example ###
Assume we have a CouchDB instance with the following structure:
@@ -76,28 +68,38 @@ Assume we have a CouchDB instance with the following structure:
* _design/app
* doca
-After we have generated the API with defaults:
+We use the api thus:
- couchdb = require('YACA');
+ couchdb_factory = require('YACA');
- callback = function(error, response, body) {console.log(error, body)}
- // create new document
- couchdb.primarydb.put({uri:'doc2',json:{field1:'hello'}}, callback)
+ couchdb_factory( { db_url:'127.0.0.1:5984'
+ , admin :'administrator:password'
+ , update:true
+ }
+ , handle_api
+ )
+
+ function handle_api(error, couchdb) {
+ callback = function(error, response, body) {console.log(error, body)}
- // view existing document
- couchdb.primarydb.get('doc1', callback);
+ // create new document
+ couchdb.primarydb.put({uri:'doc2',json:{field1:'hello'}}, callback)
+
+ // view existing document
+ couchdb.primarydb.get('doc1', callback);
- // edit existing document
- couchdb.primarydb.put( { uri:'doc2'
- , json:{ field1:'goodbye'
- , _rev:'34_434c5f645'
- }
- }
- , callback
- )
-
- // view all docs from a view
- couchdb.secondarydb.app.views.by_date.get(callback)
+ // edit existing document
+ couchdb.primarydb.put( { uri:'doc2'
+ , json:{ field1:'goodbye'
+ , _rev:'34_434c5f645'
+ }
+ }
+ , callback
+ )
+
+ // view all docs from a view
+ couchdb.secondarydb.app.views.by_date.get(callback)
+ }
Hopefully this gives you a general idea of how the API works.
@@ -106,6 +108,17 @@ Hopefully this gives you a general idea of how the API works.
### Errors ###
When couchdb returns any code greater than 299, the api returns a javascript option parsed from the CouchDB error response with the added field 'statusCode,' with the http StatusCode.
+<a name='factory_options' />
+### Factory Options ###
+The couchdb_factory, takes an optional options hash. If no hash is given, the defaults will be used. The defaults usually work just fine. However it is important to use `update` when the database structure has changed.
+ * `db_url` - the url to your CouchDB instance. It defaults to `http://127.0.0.1:5984`. If privileges are needed to view design documents or the root instance, provide username and password as: `http(s)://username:password@host_name`
+ * `admin` - basic auth string, `USERNAME:PASSWORD`, where `USERNAME` is an admin username and `PASSWORD` is the admin password, if you wish to be able to access admin-restricted content through the api. *The username and password will be stored in plain text in the cache.* See [options] reference(#options) for more.
+ * `file` is the location of the cached database configuration. YACA caches thedatabase structure so it does not have to introspect the database every time it generates an api. If set to `false`, no cache will be created. If not specified, the cache will be placed in path/to/YACA/lib/couchdb.js.
+ * `update` controls introspection. If `update: true`, then the database will be introspected when an api is created even if a cache exists.
+
+You must create a new API whenever you add or remove a database, or you modify a design document.
+
+
<a name='options' />
### Options ###
YACA is derived from request, and so it supports most of the options supported by request, and a few more
@@ -117,7 +130,7 @@ The valid keys in the options object are:
* `body` - entity body for POST and PUT requests. Must be buffer or string.
* `json` - sets `body` but to JSON representation of value and adds `Content-type: application/json` header.
* `parse` - defaults to `true`, returned `body` will be a javascript object parsed from the JSON body.
-* `admin` - provide basic auth admin credentials if provided to api generator.
+* `admin` - provide basic auth admin credentials in the request to the database. Only provided if admin credentials were provided to couchdb_factory.
* `query` - hash object of query parameters. api uses JSON.stringify on any non-string hash values. Then uses querystring.stringify on the entire hash.
* `auth` - *not yet supported.* provide the given basic auth credentials. value must be a string: `username:password`.
* `onResponse` - *not yet supported.*
@@ -128,23 +141,23 @@ The valid keys in the options object are:
All of the following API calls conform are passed an options argument and a callback. The callback is then called with three arguments: an error, the CouchDB http response, and a javascript object parsed from the CouchDB response
#### CouchDB Root ####
-* couchdb(options, callback) - same as .get
+* couchdb(options, callback) - shortcut for .get
* couchdb.get(options, callback)
* couchdb.post(options, callback)
* couchdb.put(options, callback)
* couchdb.del(options, callback)
#### Database ####
-* couchdb.{{db}}(options, callback) - same as .get
+* couchdb.{{db}}(options, callback) - shortcut for .get
* couchdb.{{db}}.get(options, callback)
* couchdb.{{db}}.post(options, callback)
* couchdb.{{db}}.put(options, callback)
* couchdb.{{db}}.del(options, callback)
-Where {{db}} is the name of the database. if your database name is a protected word, the YACA generator will notify you, and {{db}} will be your database name prepended with '__' (two underscores).
+Where {{db}} is the name of the database. if your database name is a protected word, the YACA database_fatory will notify you, and {{db}} will be your database name prepended with '__' (two underscores).
#### Design Documents ####
-* couchdb.{{db}}.{{ddoc}}(options, callback) - same as .get
+* couchdb.{{db}}.{{ddoc}}(options, callback) - shortcut for .get
* couchdb.{{db}}.{{ddoc}}.get(options, callback)
* couchdb.{{db}}.{{ddoc}}.post(options, callback)
* couchdb.{{db}}.{{ddoc}}.put(options, callback)
@@ -157,26 +170,27 @@ Where {{db}} is as described above, and {{ddoc}} is the _id of your design doc s
* couchdb.{{db}}.{{ddoc}}.views.{{method}}.post(options, callback)
#### Shows ####
-* couchdb.{{db}}.{{ddoc}}.shows.{{method}}(options, callback) - same as .get
+* couchdb.{{db}}.{{ddoc}}.shows.{{method}}(options, callback) - shortcut for .get
* couchdb.{{db}}.{{ddoc}}.shows.{{method}}.get(options, callback)
* couchdb.{{db}}.{{ddoc}}.shows.{{method}}.post(options, callback)
#### Lists ####
-* couchdb.{{db}}.{{ddoc}}.lists.{{method}}.get(options, callback) - same as .get
+* couchdb.{{db}}.{{ddoc}}.lists.{{method}}.get(options, callback) - shortcut for .get
* couchdb.{{db}}.{{ddoc}}.lists.{{method}}.post(options, callback)
#### Updates ####
-* couchdb.{{db}}.{{ddoc}}.updates.{{method}}(options, callback) - same as .put
+* couchdb.{{db}}.{{ddoc}}.updates.{{method}}(options, callback) - shortcut for .put
* couchdb.{{db}}.{{ddoc}}.updates.{{method}}.put(options, callback)
* couchdb.{{db}}.{{ddoc}}.updates.{{method}}.post(options, callback)
#### Rewrites ####
-* couchdb.{{db}}.{{ddoc}}.rewrites.{{method}}(options, callback) - same as .get
+* couchdb.{{db}}.{{ddoc}}.rewrites.{{method}}(options, callback) - shortcut for .get
* couchdb.{{db}}.{{ddoc}}.rewrites.{{method}}.get(options, callback)
* couchdb.{{db}}.{{ddoc}}.rewrites.{{method}}.pust(options, callback)
* couchdb.{{db}}.{{ddoc}}.rewrites.{{method}}.post(options, callback)
* couchdb.{{db}}.{{ddoc}}.rewrites.{{method}}.del(options, callback)
+<a name='helpers' />
### Helper API ###
Anything you can do with the helper APIs you can do with the standard API. For example, you could get uuids in either of the two following ways:
View
@@ -1,159 +0,0 @@
-#!/usr/bin/env node
-
-request = require('request');
-querystring = require('querystring')
-fs = require('fs')
-url = require('url')
-async = require('async')
-cli = require('cli')
-path = require('path')
-
-path_rx = /{{path}}/g
-database_rx = /{{db}}/g
-ddoc_rx = /{{ddoc}}/g
-handler_rx = /{{handler}}/g
-method_rx = /{{method}}/g
-
-database_code = "\ncouchdb['{{db}}'] = db_factory('{{path}}')"
-
-ddoc_code = "\ncouchdb['{{db}}']['{{ddoc}}'] = db_factory('{{path}}')"
-
-handler_code = "\ncouchdb['{{db}}']['{{ddoc}}'].{{handler}} = {}"
-
-method_code = "\n\
-couchdb['{{db}}']['{{ddoc}}'].{{handler}}['{{method}}'] = method_factories['{{handler}}']('{{path}}')"
-
-handlers = {views:'_view/',shows:'_show/',lists:'_list/',updates:'_update/',rewrites:'_rewrite/'};
-
-function gen_interface(uri, filename, admin) {
- host = uri || 'http://127.0.0.1:5984/'
- host += (host.slice(-1) == '/') ? '' : '/' // ensure ends in slash
- uri = url.parse(host)
- filename = filename || path.join(__dirname, '/../lib/couchdb.js')
-
- // the results of parsing our db
- parsed_db = {_path:''};
-
- console.log('using host:', host)
- console.log('writing to file:', filename)
- if (admin) {
- console.log('making admin credentials available:', admin)
- }
- else {
- console.log('warning: no admin credentials provided')
- }
-
- var source = fs.readFileSync(path.join(__dirname,'/../lib/template.js'), 'utf8');
-
- function write_line(line) {
- source += line + '\n'
- }
-
-
- // generate and append root uris
- var ROOT = '"'
- ROOT += uri.protocol || 'http:'
- ROOT += '//'
- ROOT += uri.hostname
- ROOT += (uri.port) ? ':'+uri.port : ''
- ROOT += '/"'
-
- write_line('couchdb.ROOT = ' + ROOT)
-
- if (admin) {
- var ADMIN_ROOT ='"'
- ADMIN_ROOT += uri.protocol || 'http:'
- ADMIN_ROOT += '//'
- ADMIN_ROOT += admin+'@'
- ADMIN_ROOT += uri.hostname
- ADMIN_ROOT += (uri.port) ? ':'+uri.port : ''
- ADMIN_ROOT += '/"'
- write_line('couchdb.ADMIN_ROOT = ' + ADMIN_ROOT)
- }
-
- return request(host+'_all_dbs', handle_dbs);
-
- function handle_dbs(error, response, body) {
- body = JSON.parse(body);
- return async.forEach(body, parse_db, handle_db_complete)
- }
-
- function parse_db(db, db_callback) {
- // generate database api
- db_name = db
- if (['get', 'post', 'put', 'del', '_has_error', '_request_generator'].indexOf(db) >= 0) {
- db_name = '__'+db
- console.log(db, 'is a protected word. remapped to:', db_name)
- }
-
- db_path = db + '/';
- write_line(database_code.replace(database_rx, db_name).replace(path_rx, db_path))
-
- // get the design docs for this db
- query = querystring.stringify({startkey:'"_design/"',endkey:'"_design0"',include_docs:true})
- return request(host+db+'/_all_docs?'+query, handle_ddocs)
-
- function handle_ddocs(error, response, body) {
- db_name = response.request.path.split('/')[1];
- db_path = db_name + '/';
- db_name = (['get', 'post', 'put', 'del', '_has_error', '_request_generator'].indexOf(db) >= 0) ? '__'+db_name : db_name;
- body = JSON.parse(body);
- for (i in body.rows) {
- // generate ddoc api
- ddoc = body.rows[i].doc
- ddoc_name = ddoc._id.split('/')[1];
- if (['get', 'post', 'put', 'del', '_path'].indexOf(ddoc_name) >= 0) {
- console.log(ddoc_name, 'is a protected word. remapped to:', '__'+ddoc_name)
- ddoc_name = '__'+ddoc_name;
- }
-
- ddoc_path = db_path + ddoc._id + '/'
- write_line(ddoc_code.replace(ddoc_rx, ddoc_name).replace(database_rx, db_name).replace(path_rx, ddoc_path))
-
- // generate handler (eg views, updates) apis contained in ddoc
- for (handler_name in ddoc) {
- if (handler_name in handlers && JSON.stringify(ddoc[handler_name]) != JSON.stringify({})) {
- handler = ddoc[handler_name]
- handler_path = ddoc_path + handlers[handler_name];
- write_line(handler_code.replace(handler_rx, handler_name)
- .replace(ddoc_rx, ddoc_name)
- .replace(database_rx, db_name) )
-
- // put any method calls (eg _view/_by_id) in target handler
- for (method_name in handler) {
- method_path = handler_path + method_name + '/'
- write_line(method_code.replace(method_rx, method_name)
- .replace(ddoc_rx, ddoc_name)
- .replace(handler_rx, handler_name)
- .replace(database_rx, db_name)
- .replace(path_rx, method_path) )
- }
- }
- }
- }
- return db_callback();
- }
- }
-
- function handle_db_complete(error) {
- fs.writeFileSync(filename, source)
- console.log('Done')
- }
-}
-
-
-
-cli.parse( { 'file': ['f', 'filename for output code', 'path', path.join(__dirname,'../lib/couchdb.js')]
- , 'admin': ['a', 'admin basic auth in the form of: username:password', 'string']
- , 'db': ['d', 'db root url, include basic-auth admin privileges if needed to modify db.', 'url', 'http://127.0.0.1:5984']
- } )
-
-cli.main(function(args, options) {
- gen_interface(options.db, options.file, options.admin);
- })
-//if (!module.parent) {
-//console.log(process)
-// gen_interface(process.argv[2], process.argv[3], process.argv[4])
-//}
-
-
View
@@ -1,6 +1 @@
-try {
- module.exports = require('./lib/couchdb');
-}
-catch(err) {
- module.exports = require('./lib/template.js');
-}
+module.exports = require('./lib/api_factory.js');
Oops, something went wrong.

0 comments on commit 110f842

Please sign in to comment.