├── package.json ├── test └── mjsunit │ ├── test-classmethods.js │ ├── test-activeTasks.js │ ├── test-allDbs.js │ ├── test-generateUUIDs.js │ ├── test-create-compact-info-drop.js │ ├── test-doc-create-update-delete.js │ └── mjsunit.js ├── README.md ├── examples ├── blog.js └── blog-support │ └── index.js └── lib ├── base64.js └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-couch", 3 | "version": "0.1.0" 4 | } -------------------------------------------------------------------------------- /test/mjsunit/test-classmethods.js: -------------------------------------------------------------------------------- 1 | var jslint = require("mjsunit"), 2 | couch = require("../../lib").CouchDB; 3 | 4 | jslint.assertEquals("http://localhost:5984", couch.defaultHost, "default host"); 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-couch - A CouchDB client for node.js 2 | ========================================= 3 | 4 | If the header is not enough for you: 5 | 6 | * [CouchDB](http://couchdb.org/) 7 | * [node.js](http://tinyclouds.org/node/) 8 | 9 | The API is inspired by jquery.node.js. 10 | 11 | This is a very initial check-in, please bear with me :-) 12 | 13 | Feedback welcome. -------------------------------------------------------------------------------- /test/mjsunit/test-activeTasks.js: -------------------------------------------------------------------------------- 1 | var jslint = require("mjsunit"), 2 | couch = require("../../lib").CouchDB; 3 | 4 | function unwantedError(result) { 5 | throw("Unwanted error" + JSON.stringify(result)); 6 | } 7 | 8 | couch.activeTasks({ 9 | success : function(response) { 10 | jslint.assertInstanceof(response, Array); 11 | }, 12 | error : unwantedError 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /test/mjsunit/test-allDbs.js: -------------------------------------------------------------------------------- 1 | var jslint = require("mjsunit"), 2 | couch = require("../../lib").CouchDB; 3 | 4 | function unwantedError(result) { 5 | throw("Unwanted error" + JSON.stringify(result)); 6 | } 7 | 8 | couch.allDbs({ 9 | success : function(response) { 10 | var result = response; 11 | 12 | jslint.assertInstanceof(result, Array); 13 | for (var ii = 0; ii < result.length; ii++) { 14 | jslint.assertEquals("string", typeof result[ii]); 15 | } 16 | 17 | }, 18 | error : unwantedError 19 | }); 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/mjsunit/test-generateUUIDs.js: -------------------------------------------------------------------------------- 1 | var jslint = require("mjsunit"), 2 | couch = require("../../lib").CouchDB; 3 | 4 | function unwantedError(result) { 5 | throw("Unwanted error" + JSON.stringify(result)); 6 | } 7 | 8 | var result = 0; 9 | 10 | couch.generateUUIDs({ 11 | count : 10, 12 | success : function(response) { 13 | result++; 14 | jslint.assertEquals(10, response.length, "not honoring count"); 15 | }, 16 | error : unwantedError 17 | }); 18 | couch.generateUUIDs({ 19 | success : function(response) { 20 | result++; 21 | jslint.assertEquals(100, response.length, "not honoring default count"); 22 | }, 23 | error : unwantedError 24 | }); 25 | 26 | 27 | function onExit() { 28 | assertEquals(2, result, "Number of callbacks mismatch"); 29 | } 30 | -------------------------------------------------------------------------------- /test/mjsunit/test-create-compact-info-drop.js: -------------------------------------------------------------------------------- 1 | var jslint = require("mjsunit"), 2 | couch = require("../../lib").CouchDB; 3 | 4 | function unwantedError(result) { 5 | throw("Unwanted error" + JSON.stringify(result)); 6 | } 7 | 8 | var db; 9 | 10 | couch.generateUUIDs({ 11 | count : 1, 12 | success : withUUIDs, 13 | error : unwantedError 14 | }); 15 | 16 | 17 | function withUUIDs(uuids) { 18 | db = couch.db("test" + uuids[0]); 19 | db.create({ 20 | success : withDB, 21 | error : unwantedError 22 | }); 23 | } 24 | 25 | function withDB() { 26 | db.compact({ 27 | success: afterCompact, 28 | error : unwantedError 29 | }); 30 | } 31 | 32 | function afterCompact() { 33 | db.info({ 34 | success : withInfo, 35 | error : unwantedError 36 | }); 37 | } 38 | 39 | function withInfo(info) { 40 | jslint.assertEquals(db.name, info.db_name); 41 | 42 | db.drop({ 43 | success : afterDrop, 44 | error : unwantedError 45 | }); 46 | } 47 | 48 | function afterDrop() { 49 | db = "success"; 50 | } 51 | 52 | function onExit() { 53 | jslint.assertEquals("success", db, "Please check the chain, last callback was never reached"); 54 | } 55 | -------------------------------------------------------------------------------- /test/mjsunit/test-doc-create-update-delete.js: -------------------------------------------------------------------------------- 1 | var jslint = require("mjsunit"), 2 | couch = require("../../lib").CouchDB; 3 | 4 | function unwantedError(result) { 5 | throw("Unwanted error" + JSON.stringify(result)); 6 | } 7 | 8 | var db; 9 | var doc; 10 | var id; 11 | var rev; 12 | 13 | couch.generateUUIDs({ 14 | count : 1, 15 | success : withUUIDs, 16 | error : unwantedError 17 | }); 18 | 19 | 20 | function withUUIDs(uuids) { 21 | db = couch.db("test" + uuids[0]); 22 | db.create({ 23 | success : withDB, 24 | error : unwantedError 25 | }); 26 | } 27 | 28 | function withDB() { 29 | doc = {}; 30 | db.saveDoc(doc, { 31 | success : afterSave, 32 | error : unwantedError 33 | }); 34 | } 35 | 36 | function afterSave(returnVal) { 37 | jslint.assertEquals(doc, returnVal, "should return the doc"); 38 | jslint.assertEquals(typeof doc._id, "string"); 39 | 40 | id = doc._id; 41 | rev = doc._rev; 42 | doc.foo = "bar"; 43 | db.saveDoc(doc, { 44 | success : afterUpdate, 45 | error : unwantedError 46 | }); 47 | } 48 | 49 | function afterUpdate(returnVal) { 50 | jslint.assertEquals(doc, returnVal, "should return the doc"); 51 | jslint.assertEquals(typeof doc._id, "string"); 52 | jslint.assertFalse(doc._rev === rev, "rev did not update"); 53 | jslint.assertEquals(id, doc._id, "doc id changed"); 54 | 55 | db.removeDoc(doc, { 56 | success : afterRemove, 57 | error : unwantedError 58 | }); 59 | } 60 | 61 | function afterRemove(returnVal) { 62 | jslint.assertEquals(doc, returnVal, "did not return the doc"); 63 | 64 | jslint.assertTrue(doc._rev === undefined, "did not remove the rev"); 65 | jslint.assertEquals(id, doc._id, "changed the id"); 66 | 67 | db.drop({ 68 | success : afterDrop, 69 | error : unwantedError 70 | }); 71 | } 72 | 73 | function afterDrop() { 74 | db = "success"; 75 | } 76 | 77 | function onExit() { 78 | jslint.assertEquals("success", db, "Please check the chain, last callback was never reached"); 79 | } 80 | -------------------------------------------------------------------------------- /examples/blog.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | var sys = require('sys'); 4 | var http = require('http'); 5 | var url = require('url'); 6 | var repl = require('repl'); 7 | 8 | var CouchDB = require('../lib').CouchDB; 9 | var blogsupport = require('./blog-support'); 10 | 11 | var blogTitle = "node-couch example blog"; 12 | var dbName = "node_couch_blog"; 13 | var blogPort = 8080; 14 | var renderer = new blogsupport.CouchBlogRenderer(blogTitle); 15 | var designDoc = blogsupport.designDoc; 16 | var postRegex = /^\/posts\/([a-f0-9]*)$/; 17 | var feedRegex = /^\/feed$/; 18 | 19 | var db = CouchDB.db(dbName); 20 | db.create({ 21 | success: function(res){ 22 | db.saveDoc(designDoc, { 23 | success: startBlog 24 | }); 25 | } 26 | }); 27 | 28 | function startBlog(){ 29 | for(var i = 0, len = blogsupport.posts.length; i < len; i++){ 30 | db.saveDoc(blogsupport.posts[i]); 31 | } 32 | 33 | var server = http.createServer(function (req, res) { 34 | var pathDetails = url.parse(req.url, true); 35 | 36 | if(pathDetails.pathname === "/"){ 37 | db.view("blog/posts_by_date", { 38 | success: function(result){ 39 | renderer.renderMain(req, res, result.rows); 40 | } 41 | }); 42 | } else if(postRegex.test(pathDetails.pathname)){ 43 | var match = postRegex.exec(pathDetails.pathname); 44 | 45 | var postURI = pathDetails.pathname; 46 | 47 | var docId = match[1]; 48 | db.openDoc(docId, { 49 | success: function(doc){ 50 | renderer.renderPostPage(req, res, doc); 51 | } 52 | }); 53 | } else if(feedRegex.test(pathDetails.pathname)){ 54 | db.view("blog/posts_by_date", { 55 | success: function(result){ 56 | renderer.renderFeed(req, res, result.rows); 57 | } 58 | }); 59 | } else { 60 | res.writeHead(404, {'Content-Type': 'text/plain'}); 61 | res.end("Cannot find specified page: "+req.url); 62 | } 63 | }).listen(blogPort); 64 | 65 | sys.puts("Blog started."); 66 | } -------------------------------------------------------------------------------- /lib/base64.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Refactored variant in order to properly encapsulate private/protected 3 | * methods within a functional scope and make things look a lot cleaner 4 | * and lintier for greater good. 5 | * 6 | * @voodootikigod 7 | */ 8 | 9 | 10 | /** 11 | * 12 | * Base64 encode / decode 13 | * Adapted from: http://www.webtoolkit.info/ 14 | * 15 | **/ 16 | 17 | var Base64 = (function() { 18 | 19 | // private property 20 | var _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 21 | 22 | // private method for UTF-8 encoding 23 | var _utf8_encode = function (string) { 24 | string = string.replace(/\r\n/g,"\n"); 25 | var utftext = ""; 26 | for (var n = 0; n < string.length; n++) { 27 | var c = string.charCodeAt(n); 28 | if (c < 128) { 29 | utftext += String.fromCharCode(c); 30 | } 31 | else if((c > 127) && (c < 2048)) { 32 | utftext += String.fromCharCode((c >> 6) | 192); 33 | utftext += String.fromCharCode((c & 63) | 128); 34 | } 35 | else { 36 | utftext += String.fromCharCode((c >> 12) | 224); 37 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 38 | utftext += String.fromCharCode((c & 63) | 128); 39 | } 40 | } 41 | return utftext; 42 | }; 43 | 44 | // private method for UTF-8 decoding 45 | var _utf8_decode = function (utftext) { 46 | var string = ""; 47 | var i = 0; 48 | var c = c1 = c2 = 0; 49 | while ( i < utftext.length ) { 50 | c = utftext.charCodeAt(i); 51 | if (c < 128) { 52 | string += String.fromCharCode(c); 53 | i++; 54 | } 55 | else if((c > 191) && (c < 224)) { 56 | c2 = utftext.charCodeAt(i+1); 57 | string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); 58 | i += 2; 59 | } 60 | else { 61 | c2 = utftext.charCodeAt(i+1); 62 | c3 = utftext.charCodeAt(i+2); 63 | string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); 64 | i += 3; 65 | } 66 | } 67 | return string; 68 | }; 69 | 70 | 71 | return { 72 | encode : function (input) { 73 | var output = ""; 74 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 75 | var i = 0; 76 | 77 | input = _utf8_encode(input); 78 | 79 | while (i < input.length) { 80 | 81 | chr1 = input.charCodeAt(i++); 82 | chr2 = input.charCodeAt(i++); 83 | chr3 = input.charCodeAt(i++); 84 | 85 | enc1 = chr1 >> 2; 86 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 87 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 88 | enc4 = chr3 & 63; 89 | 90 | if (isNaN(chr2)) { 91 | enc3 = enc4 = 64; 92 | } else if (isNaN(chr3)) { 93 | enc4 = 64; 94 | } 95 | 96 | output = output + 97 | _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + 98 | _keyStr.charAt(enc3) + _keyStr.charAt(enc4); 99 | 100 | } 101 | 102 | return output; 103 | }, 104 | 105 | // public method for decoding 106 | decode : function (input) { 107 | var output = ""; 108 | var chr1, chr2, chr3; 109 | var enc1, enc2, enc3, enc4; 110 | var i = 0; 111 | 112 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 113 | 114 | while (i < input.length) { 115 | 116 | enc1 = this._keyStr.indexOf(input.charAt(i++)); 117 | enc2 = this._keyStr.indexOf(input.charAt(i++)); 118 | enc3 = this._keyStr.indexOf(input.charAt(i++)); 119 | enc4 = this._keyStr.indexOf(input.charAt(i++)); 120 | 121 | chr1 = (enc1 << 2) | (enc2 >> 4); 122 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 123 | chr3 = ((enc3 & 3) << 6) | enc4; 124 | 125 | output = output + String.fromCharCode(chr1); 126 | 127 | if (enc3 != 64) { 128 | output = output + String.fromCharCode(chr2); 129 | } 130 | if (enc4 != 64) { 131 | output = output + String.fromCharCode(chr3); 132 | } 133 | 134 | } 135 | 136 | output = exports.base64._utf8_decode(output); 137 | 138 | return output; 139 | 140 | } 141 | } 142 | })() 143 | 144 | exports.encode = Base64.encode; 145 | exports.decode = Base64.decode; 146 | 147 | -------------------------------------------------------------------------------- /test/mjsunit/mjsunit.js: -------------------------------------------------------------------------------- 1 | // Copyright 2008 the V8 project authors. All rights reserved. 2 | // Redistribution and use in source and binary forms, with or without 3 | // modification, are permitted provided that the following conditions are 4 | // met: 5 | // 6 | // * Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // * Redistributions in binary form must reproduce the above 9 | // copyright notice, this list of conditions and the following 10 | // disclaimer in the documentation and/or other materials provided 11 | // with the distribution. 12 | // * Neither the name of Google Inc. nor the names of its 13 | // contributors may be used to endorse or promote products derived 14 | // from this software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | // 28 | // 29 | 30 | /*jslint 31 | evil: true 32 | */ 33 | 34 | function MjsUnitAssertionError(message) { 35 | this.message = message; 36 | } 37 | 38 | MjsUnitAssertionError.prototype.toString = function () { 39 | return this.message; 40 | }; 41 | 42 | /* 43 | * This file is included in all mini jsunit test cases. The test 44 | * framework expects lines that signal failed tests to start with 45 | * the f-word and ignore all other lines. 46 | */ 47 | 48 | function fail (expected, found, name_opt) { 49 | var start; 50 | if (name_opt) { 51 | // Fix this when we ditch the old test runner. 52 | start = "Fail" + "ure (" + name_opt + "): "; 53 | } else { 54 | start = "Fail" + "ure:"; 55 | } 56 | throw new MjsUnitAssertionError(start + " expected <" + expected + "> found <" + found + ">"); 57 | }; 58 | 59 | 60 | function deepEquals (a, b) { 61 | if (a == b) { 62 | return true; 63 | } 64 | if ((typeof a) !== 'object' || (typeof b) !== 'object' || 65 | (a === null) || (b === null)) { 66 | return false; 67 | } 68 | if (a.constructor === Array) { 69 | if (b.constructor !== Array) { 70 | return false; 71 | } 72 | if (a.length != b.length) { 73 | return false; 74 | } 75 | for (var i = 0; i < a.length; i++) { 76 | if (i in a) { 77 | if (!(i in b) || !(deepEquals(a[i], b[i]))) { 78 | return false; 79 | } 80 | } else if (i in b) { 81 | return false; 82 | } 83 | } 84 | return true; 85 | } 86 | return false; 87 | }; 88 | 89 | 90 | exports.assertEquals = function (expected, found, name_opt) { 91 | if (!deepEquals(found, expected)) { 92 | fail(expected, found, name_opt); 93 | } 94 | }; 95 | 96 | 97 | exports.assertArrayEquals = function (expected, found, name_opt) { 98 | var start = ""; 99 | if (name_opt) { 100 | start = name_opt + " - "; 101 | } 102 | exports.assertEquals(expected.length, found.length, start + "array length"); 103 | if (expected.length == found.length) { 104 | for (var i = 0; i < expected.length; ++i) { 105 | exports.assertEquals(expected[i], found[i], start + "array element at index " + i); 106 | } 107 | } 108 | }; 109 | 110 | 111 | exports.assertTrue = function (value, name_opt) { 112 | exports.assertEquals(true, value, name_opt); 113 | }; 114 | 115 | 116 | exports.assertFalse = function (value, name_opt) { 117 | exports.assertEquals(false, value, name_opt); 118 | }; 119 | 120 | 121 | exports.assertNaN = function (value, name_opt) { 122 | if (!isNaN(value)) { 123 | fail("NaN", value, name_opt); 124 | } 125 | }; 126 | 127 | 128 | exports.assertThrows = function (code) { 129 | var threwException = true; 130 | try { 131 | eval(code); 132 | threwException = false; 133 | } catch (e) { 134 | // Do nothing. 135 | } 136 | if (!threwException) { 137 | exports.assertTrue(false, "did not throw exception"); 138 | } 139 | }; 140 | 141 | 142 | exports.assertInstanceof = function (obj, type) { 143 | if (!(obj instanceof type)) { 144 | exports.assertTrue(false, "Object <" + obj + "> is not an instance of <" + type + ">"); 145 | } 146 | }; 147 | 148 | 149 | exports.assertDoesNotThrow = function (code) { 150 | try { 151 | eval(code); 152 | } catch (e) { 153 | exports.assertTrue(false, "threw an exception"); 154 | } 155 | }; 156 | 157 | 158 | exports.assertUnreachable = function (name_opt) { 159 | // Fix this when we ditch the old test runner. 160 | var message = "Fail" + "ure: unreachable"; 161 | if (name_opt) { 162 | message += " - " + name_opt; 163 | } 164 | throw new MjsUnitAssertionError(message); 165 | }; 166 | -------------------------------------------------------------------------------- /examples/blog-support/index.js: -------------------------------------------------------------------------------- 1 | // posts: 2 | // Array of posts to seed the database with. (taken from http://jwf.us, 3 | // by Jason Feinstein with permission) 4 | exports.posts = [ 5 | { 6 | _id: '1', 7 | title: 'Firefox Heads-up Display for Developers', 8 | body: '
Looks like we might have a potential competitor to Firebug coming out soon… This is exciting!
… an interactive Console to help web developers understand everything that happens during the creation of a web-page. Console entries will be time-stamped objects representing errors, network traffic, javascript events, DOM/HTML mutation and logging output. The Console will also have an interactive command line for evaluating javascript against the current webpage and an interactive Workspace window for entering and evaluating larger blocks of code.
I know it’s far-fetched, and kinda beside the point for Mozilla/Firefox developers to make it cross-platform, but I’d love to see something along the lines of Firebug Lite with this too.
', 9 | type: 'post', 10 | date: 'Sun Apr 4 2010 01:37:38 GMT-0400 (EDT)' 11 | }, 12 | { 13 | _id: '2', 14 | title: 'My latest GitHub project: willow', 15 | body: 'Willow is a JavaScript logging tool which uses the “magic” of JavaScript’s arguments object to introduce valuable information to logging and trace messages sent to Firebug’s console.
It’s not ready to be officially released yet, which is why I’m not really going to describe it too much here (yet). I’m curious to see what kind of additional features people would be interested in using, beyond the ones I’ve implemented.
If you download the source from the link, and build it (see the README for how to build, it’s very easy) - you’ll be able to see some example applications of willow.
', 16 | type: 'post', 17 | date: 'Sun Feb 7 2010 01:37:38 GMT-0400 (EDT)' 18 | }, 19 | { 20 | _id: '3', 21 | title: 'Make alert() behave like console.log(), when possible..', 22 | body: 'The other day, @mrspeaker tweeted this:
c’mon alert() - just magically be a _bit_ more like console.log and you’d save me a lot of time
This got the creative juices flowing a little and I coded up a small script to overwrite how alert() operates and, if available, it will choose to behave exactly like console.log() by writing to the console instead of popping up the dialog (if it’s available). It still needs some work to support formatted strings in alert() dialogs, for when console.log() isn’t available.
var oldAlert = alert;\nalert = function(){\n if(window.console != null){\n console.log.apply(null, arguments);\n } else {\n oldAlert.apply(null, arguments);\n }\n}; What this means now is that you can exclusively use alert() in your code and not have to use console.log() and worry about your JavaScript breaking in browsers that don’t have Firebug or a console.
Find it on GitHub, here.
', 23 | type: 'post', 24 | date: 'Fri Dec 18 2009 01:37:38 GMT-0400 (EDT)' 25 | } 26 | ]; 27 | 28 | // designDoc: 29 | // The CouchDB view page that will be used to get a reverse 30 | // chronologically-ordered list of posts for rendering. 31 | exports.designDoc = { 32 | _id: "_design/blog", 33 | language: "javascript", 34 | views: { 35 | posts_by_date: { 36 | map: function(doc){ 37 | if(doc.type == "post"){ 38 | emit(-Date.parse(doc.date), doc); 39 | } 40 | } 41 | } 42 | } 43 | }; 44 | 45 | 46 | function CouchBlogRenderer(title){ 47 | // summary: 48 | // Tool used to render the blog to HTML and RSS. 49 | // description: 50 | // Assumes blog posts will contain a format similar to those above 51 | // and can render them to an RSS Feed, a main page (listing posts 52 | // in reverse-chronological order), and individual pages. 53 | // title: String 54 | // Title of the blog, defaults to "Blog". 55 | this.title = title || "Blog"; 56 | } 57 | CouchBlogRenderer.prototype = { 58 | renderFeed: function(req, res, posts){ 59 | res.writeHead(200, { 60 | 'Content-Type': 'application/rss+xml' 61 | }); 62 | 63 | res.write('\n'); 64 | res.write('Check out the RSS feed.
'); 118 | res.write('\n'); 119 | res.write('\n'); 120 | res.end(); 121 | } 122 | }; 123 | exports.CouchBlogRenderer = CouchBlogRenderer; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2009 Hagen Overdick 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | var sys = require('sys'), 24 | http = require('http'), 25 | url = require('url'), 26 | base64 = require('./base64'); 27 | 28 | function createClient(host) { 29 | // summary: 30 | // Creates an HTTP Client for the specified host. 31 | // description: 32 | // Create a new HTTPClient object and return it 33 | // host: String 34 | // CouchDB Host URL. 35 | // returns: 36 | // An HTTPClient object associated with the specified host. 37 | 38 | var uri = url.parse(host); 39 | return http.createClient(uri.port, uri.hostname); 40 | } 41 | 42 | function _interact(verb, path, successStatus, options, host) { 43 | // summary: 44 | // Interacts with the CouchDB server. 45 | // description: 46 | // Allows for interaction with the CouchDB server given the 47 | // appropriate REST request details as specified in the arguments 48 | // to this function. 49 | // verb: String 50 | // HTTP request type (or verb). E.g. GET, PUT, POST, DELETE 51 | // path: String 52 | // CouchDB resource path. 53 | // successStatus: Integer|Array 54 | // The HTTP response code(s) we expect on a successful interaction 55 | // with the server. 56 | // options: Object 57 | // Options for the interaction with the server. 58 | // { 59 | // body: Object, 60 | // keys: ?, 61 | // key: 62 | // startKey: 63 | // endKey: 64 | // request: Function, 65 | // success: Function, 66 | // error: Function 67 | // } 68 | // host: String 69 | // CouchDB host to connect to. 70 | 71 | // Sanitize the verb and options parameters. 72 | verb = verb.toUpperCase(); 73 | options = options || {}; 74 | 75 | // placeholder for the HTTP request object. 76 | var request; 77 | 78 | var client = createClient(host); 79 | var requestPath = path + encodeOptions(options); 80 | if (CouchDB.debug) { 81 | sys.puts("COUCHING " + requestPath + " -> " + verb); 82 | } 83 | 84 | var uri = url.parse(host); 85 | var headers = {}; 86 | headers["Host"] = uri.hostname; 87 | if (uri.auth) { 88 | headers["Authorization"] = "Basic "+base64.encode(uri.auth); 89 | } 90 | 91 | if (options.keys) { 92 | options.body = {keys: options.keys}; 93 | } 94 | 95 | if (options.body) { 96 | if (verb === "get") { 97 | verb = "post"; 98 | } 99 | var requestBody = toJSON(options.body); 100 | 101 | headers["Content-Length"] = requestBody.length; 102 | headers["Content-Type"] = "application/json"; 103 | 104 | request = client.request(verb, requestPath, headers); 105 | request.write(requestBody, "utf8"); 106 | } else { 107 | request = client.request(verb, requestPath, headers); 108 | } 109 | 110 | request.addListener('response', function(response) { 111 | var responseBody = "" 112 | 113 | response.setBodyEncoding("utf8"); 114 | 115 | response.addListener("data", function(chunk) { 116 | responseBody += chunk 117 | }); 118 | 119 | response.addListener("end", function() { 120 | if (CouchDB.debug) { 121 | sys.puts("COMPLETED " + requestPath + " -> " + verb); 122 | sys.puts(responseBody) 123 | } 124 | responseBody = JSON.parse(responseBody); 125 | 126 | if (isAcceptableStatus(response.statusCode,successStatus)) { 127 | if (options.success) { 128 | options.success(responseBody); 129 | } 130 | } else if (options.error) { 131 | options.error(responseBody); 132 | } 133 | }); 134 | }); 135 | 136 | request.end(); 137 | 138 | } 139 | 140 | function isAcceptableStatus(testCode, acceptable){ 141 | // summary: 142 | // Quick utility function to test if an HTTP response code is 143 | // acceptable for a purpose. 144 | // testCode: Integer 145 | // Code we're testing. 146 | // acceptable: Integer|Array 147 | // Either an individual status code, or an array of acceptable 148 | // status codes. 149 | // returns: 150 | // True if testCode is an acceptable status code. False otherwise. 151 | 152 | if (acceptable.constructor.toString().indexOf("Array") == -1) { 153 | acceptable = [acceptable]; 154 | } 155 | 156 | for(var i = 0, len = acceptable.length; i < len; i++){ 157 | if(acceptable[i] == testCode){ 158 | return true; 159 | } 160 | } 161 | return false; 162 | } 163 | 164 | function encodeOptions(options) { 165 | // summary: 166 | // Builds an HTTP query string given an object containing options 167 | // for an interaction with the CouchDB server. 168 | // options: 169 | // See description in _interact(...) above. 170 | // returns: 171 | // Query string for the HTTP request to the CouchDB server. 172 | 173 | var result = []; 174 | 175 | if (typeof(options) === "object" && options !== null) { 176 | // Iterate through all of the options, and add an entry into the result 177 | // array if it's a key. 178 | for (var name in options) { 179 | if (options.hasOwnProperty(name)) { 180 | if (name === "request" || name === "error" || name === "success" || name === "body" || name === "keys") { 181 | continue; 182 | } 183 | 184 | var value = options[name]; 185 | 186 | if (name == "key" || name == "startkey" || name == "endkey") { 187 | value = toJSON(value); 188 | } 189 | 190 | result.push(encodeURIComponent(name) + "=" + encodeURIComponent(value)); 191 | } 192 | } 193 | } 194 | 195 | return result.length ? ("?" + result.join("&")) : ""; 196 | } 197 | 198 | function toJSON(obj) { 199 | // summary: 200 | // Builds a JSON string from an object. 201 | // description: 202 | // Sanitizes the object, if it's null we'll return null, otherwise 203 | // we'll use JSON.stringify to convert it. 204 | // obj: Object 205 | // Object to convert to JSON 206 | // returns: 207 | // JSON string representation of obj. 208 | if(obj !== null){ 209 | return JSON.stringify(obj, function(key, val) { 210 | if (typeof val == 'function') { 211 | return val.toString(); 212 | } 213 | return val; 214 | }); 215 | } 216 | return null; 217 | } 218 | 219 | var CouchDB = { 220 | // summary: 221 | // The main node-couch utility object. 222 | // description: 223 | // 224 | 225 | // defaultHost: String 226 | // The default hostname to try and connect to a CouchDB instance 227 | // on. Hostnames can include Basic Authentication parameters as 228 | // well: 229 | // "http://admin:password@127.0.0.1:5984" 230 | defaultHost : "http://localhost:5984", 231 | 232 | // debug: Boolean 233 | // Flag to tell the node-couch module whether or not it should log 234 | // certain actions. 235 | debug : true, 236 | 237 | activeTasks: function(options) { 238 | // summary: 239 | // Gets all of the active tasks from CouchDB. 240 | // description: 241 | // Makes a GET request to [host]/_active_tasks with the 242 | // specified options. 243 | // options: 244 | // Common options object. 245 | _interact("get", "/_active_tasks", 200, options, CouchDB.defaultHost); 246 | }, 247 | 248 | allDbs : function(options) { 249 | // summary: 250 | // Gets a list of all the databases from CouchDB. 251 | // description: 252 | // Makes a GET request to [host]/_all_dbs with the specified 253 | // options. 254 | // options: 255 | // Common options object. 256 | _interact("get", "/_all_dbs", 200, options, CouchDB.defaultHost); 257 | }, 258 | 259 | generateUUIDs : function(options) { 260 | // summary: 261 | // Generates a bunch of UUIDs from the server. 262 | // description: 263 | // Makes a GET request to [host]/_uuids with the specified 264 | // options. Note: the result object that gets passed to 265 | // options.success will only contain the UUIDs 266 | // options: 267 | // Common options object. 268 | options = options || {}; 269 | if (!options.count) { 270 | options.count = 100; 271 | } 272 | var callback = options.success; 273 | options.success = function(result) { 274 | callback(result.uuids); 275 | }; 276 | _interact("get", "/_uuids", 200, options, CouchDB.defaultHost); 277 | }, 278 | 279 | db : function(name, host) { 280 | // summary: 281 | // Creates and returns an object that allows the user to 282 | // communicate with the specified database. 283 | // description: 284 | // Returns an object that can communicate with a CouchDB 285 | // database of the specified name at the specified host. 286 | // name: String 287 | // Name of the database to connect to. 288 | // host: String - Optional 289 | // Host where the database is located. If no host is 290 | // specified, CouchDB.defaultHost will be used. 291 | // returns: 292 | // A connection object for the database with the specified name 293 | // at the specified host. 294 | // { 295 | // name: String 296 | // uri: String 297 | // host: String 298 | // interact: Function 299 | // compact: Function 300 | // create: Function 301 | // drop: Function 302 | // info: Function 303 | // allDocs: Function 304 | // openDoc: Function 305 | // saveDoc: Function 306 | // removeDoc: Function 307 | // view: Function 308 | // } 309 | // See the documentation on each of the functions in the object 310 | // itself for more info. 311 | return { 312 | // name: String 313 | // Name of the database. 314 | name : name, 315 | 316 | // uri: String 317 | // URI for the database - after the host string. 318 | uri : "/" + encodeURIComponent(name) + "/", 319 | 320 | // host: String 321 | // Hostname where the database with name 'name' is located. 322 | host : host || CouchDB.defaultHost, 323 | 324 | interact : function(verb, path, successStatus, options, suppressPrefix) { 325 | // summary: 326 | // Interacts with the the database. 327 | // description: 328 | // Given the verb, path, expected success status, etc. 329 | // This will make a connection to the database and 330 | // perform some action. This function should be used 331 | // when node-couch doesn't explicitly define what 332 | // you're trying to do with your database as a node- 333 | // couch function. 334 | // verb: String 335 | // HTTP Verb 336 | // path: String 337 | // If suppressPrefix is true, it's the Post-URI path. 338 | // That is, it's the stuff that comes after 339 | // [host]/[name]/ in a URL. Otherwise, it's a full 340 | // path. 341 | // successStatus: Integer|Array 342 | // HTTP status code(s) that indicate successful 343 | // interaction. 344 | // options: Object 345 | // Standard options object. 346 | // suppressPrefix: Boolean 347 | // Whether or not to interpret the path as the segment 348 | // after the database name. 349 | if (!suppressPrefix) { 350 | path = this.uri + path; 351 | } 352 | _interact(verb, path, successStatus, options, this.host); 353 | }, 354 | 355 | compact : function(options) { 356 | // summary: 357 | // Compacts the database. 358 | // description: 359 | // Performs compaction on the database. See more at the 360 | // CouchDB wiki: 361 | // http://wiki.apache.org/couchdb/Compaction 362 | // options: Object 363 | // Standard options object. 364 | this.interact("post", "_compact", 202, options); 365 | }, 366 | 367 | create : function(options, failOnDuplicate) { 368 | // summary: 369 | // Creates the database. 370 | // description: 371 | // Will create the database with the name of this 372 | // object. See more at the CouchDB wiki: 373 | // http://wiki.apache.org/couchdb/HTTP_database_API 374 | // options: Object 375 | // Standard options object. 376 | // failOnDuplicate: Boolean - Optional 377 | // If set to true, this function will fail and the 378 | // error option callback will be run if the database 379 | // already exists. If set to false, or left unset, the 380 | // success function will be called even if the database 381 | // exists. 382 | 383 | var acceptableStatuses = [201, 412]; 384 | if(failOnDuplicate){ 385 | acceptableStatuses.pop(); 386 | } 387 | 388 | this.interact("put", "", acceptableStatuses, options); 389 | }, 390 | 391 | drop : function(options) { 392 | // summary: 393 | // Deletes the database. 394 | // description: 395 | // Will delete the database with the name of this 396 | // object. See more at the CouchDB wiki: 397 | // http://wiki.apache.org/couchdb/HTTP_database_API 398 | // options: Object 399 | // Standard options object. 400 | this.interact("del", "", 200, options); 401 | }, 402 | 403 | info : function(options) { 404 | // summary: 405 | // Gets information about the database. 406 | // description: 407 | // Will get information about the database. See more 408 | // at the CouchDB wiki: 409 | // http://wiki.apache.org/couchdb/HTTP_database_API 410 | // options: Object 411 | // Standard options object. 412 | this.interact("get", "", 200, options); 413 | }, 414 | 415 | allDocs : function(options) { 416 | // summary: 417 | // Bulk document fetch function. 418 | // description: 419 | // Makes a GET request to [host]/[name]/_all_docs and 420 | // gets a bulk amount of documents back. For more 421 | // information, see the CouchDB wiki: 422 | // http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API 423 | // options: Object 424 | // Standard options object. 425 | this.interact("get", "_all_docs", 200, options); 426 | }, 427 | 428 | openDoc : function(docId, options) { 429 | // summary: 430 | // Fetches a particular document by its ID. 431 | // description: 432 | // Makes a GET request to the database to fetch the 433 | // document specified by the docId parameter. 434 | // Depending on the type of the docId parameter, it 435 | // does one of two things: 436 | // 1. Gets the document directly (if the docId is a 437 | // string) 438 | // 2. Assumes the docId is an Array containing keys 439 | // and filters a bulk request of _all_docs to 440 | // retrieve those documents. 441 | // See more at the CouchDB wiki: 442 | // http://wiki.apache.org/couchdb/HTTP_Document_API 443 | // http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API 444 | // docId: String|Object 445 | // If a string, it is interpreted as the id of the 446 | // specific document to retrieve. Else, it's assumed to 447 | // be an array containing several IDs. 448 | // options: Object 449 | // Standard options object. 450 | var path; 451 | if (typeof docId === "string") { 452 | path = docId; 453 | } else { 454 | path = "_all_docs"; 455 | options.body = { 456 | keys : docId 457 | }; 458 | } 459 | this.interact("get", path, 200, options); // interact will override get to post when needed 460 | }, 461 | 462 | saveDoc : function(doc, options, enforceRevision) { 463 | // summary: 464 | // Creates or updates a particular document. 465 | // description: 466 | // Given the document to save, this function determines 467 | // whether or not it's a new document and either 468 | // creates the document in the database or updates an 469 | // existing one with the same ID. 470 | // See more a the CouchDB wiki: 471 | // http://wiki.apache.org/couchdb/HTTP_Document_API 472 | // doc: Object 473 | // Document to save. 474 | // options: Object 475 | // Standard options object. 476 | // enforceRevision: Boolean - Optional 477 | // If true, we will enforce revision-checking when 478 | // saving the document. Otherwise, if the document 479 | // doesn't have a valid _rev attribute, we'll retrieve 480 | // it and then re-try the PUT request. 481 | options = options || {}; 482 | doc = doc || {}; 483 | 484 | var me = this; 485 | 486 | var success = options.success; 487 | options.success = function(result) { 488 | if (!result.ok) { 489 | if(enforceRevision){ 490 | options.error(result); 491 | } else { 492 | me.openDoc(doc._id, { 493 | success: function(existingDoc){ 494 | doc._rev = existingDoc._rev; 495 | var opts = options; 496 | opts.success = success; 497 | me.saveDoc(doc,opts); 498 | }, 499 | error: options.error 500 | }); 501 | } 502 | return; 503 | } else { 504 | doc._id = result.id; 505 | doc._rev = result.rev; 506 | } 507 | if (success) { 508 | success(doc); 509 | } 510 | }; 511 | 512 | options.body = doc; 513 | 514 | if (doc._id === undefined) { 515 | this.interact("post", "", 201, options); 516 | } else { 517 | var successCodes = [201, 409]; 518 | if(enforceRevision){ 519 | successCodes.pop(); 520 | } 521 | this.interact("put", doc._id, successCodes, options); 522 | } 523 | }, 524 | 525 | removeDoc : function(doc, options) { 526 | // summary: 527 | // Deletes a document from the database. 528 | // description: 529 | // Makes a DELETE http request to the database with the 530 | // specified document's ID to delete the document. See 531 | // more at the CouchDB wiki: 532 | // http://wiki.apache.org/couchdb/HTTP_Document_API 533 | // doc: Object 534 | // Document to delete. Must have an _id and a _rev. 535 | // options: Object 536 | // Standard options object. 537 | options = options || {}; 538 | options.rev = doc._rev; 539 | 540 | var success = options.success; 541 | options.success = function(result) { 542 | if (!result.ok) { 543 | options.error(result); // should this return, so success(doc) doesn't get called? 544 | } 545 | delete doc._rev; 546 | if (success) { 547 | success(doc); 548 | } 549 | }; 550 | 551 | this.interact("del", doc._id, 200, options); 552 | }, 553 | 554 | view : function(name, options) { 555 | // summary: 556 | // Gets a view of the database. 557 | // description: 558 | // Given a name where the first part is the design and 559 | // the second is the view, will get a view of the 560 | // database. For more information see the CouchDB 561 | // wiki: 562 | // http://wiki.apache.org/couchdb/HTTP_view_API 563 | // name: String 564 | // Design and view names. Separated by a slash. 565 | // E.g. company/all 566 | // options: Object 567 | // Standard options object. 568 | name = name.split('/'); 569 | this.interact("get", "_design/" + name[0] + "/_view/" + name[1], 200, options); 570 | } 571 | } 572 | } 573 | }; 574 | 575 | exports.CouchDB = CouchDB; 576 | --------------------------------------------------------------------------------