├── .gitignore ├── .npmignore ├── README.md ├── lib ├── base64.js ├── couchdb.js ├── events.js └── sha1.js ├── package.json └── tests ├── couchclient.js └── settings.js.example /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.sublime-project 2 | *.sublime-workspace 3 | tests/* 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CouchDB Client 2 | 3 | CouchDB-CommonJS is a promise-based CouchDB client. It can be used in the browser or on the server 4 | via Node.js. 5 | 6 | ## Example Usage in Node.js 7 | 8 | Create a module named hello-couch.js and add the following code: 9 | 10 | var couchdb = require('couchdb'); 11 | var client = couchdb.createClient(5984, 'localhost'); 12 | var db = client.db('helloCouch'); 13 | 14 | var doc = { _id: 'helloCouch', text: 'Hello CouchDB!' }; 15 | 16 | db.saveDoc(doc).then(function() { 17 | console.log('document saved!'); 18 | 19 | db.openDoc('helloCouch').then(function(doc) { 20 | console.log('retrieved document!'); 21 | console.log(JSON.stringify(doc)); 22 | }); 23 | }); 24 | 25 | Before executing this module, create a database in your CouchDB named `helloCouch` 26 | using [futon](http://localhost:5984/_utils). 27 | 28 | This program creates a document in the CouchDB Database *helloCouch*. Next, 29 | it saves a document to the database with a key of *helloCouch* and a text 30 | property. Then, it retrieves the document from the database and prints the 31 | contents of the document to the console. 32 | 33 | ## Promises 34 | 35 | Methods on `CouchClient` and `Database` return [promises](http://wiki.commonjs.org/wiki/Promises). 36 | The promises are [Promises/A](http://wiki.commonjs.org/wiki/Promises/A) compliant. You may be familiar 37 | with [promises in jQuery](http://www.erichynds.com/jquery/using-deferreds-in-jquery/) as they have recently 38 | been added to make Ajax calls cleaner. -------------------------------------------------------------------------------- /lib/base64.js: -------------------------------------------------------------------------------- 1 | var Base64 = { 2 | 3 | // private property 4 | _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 5 | 6 | // public method for encoding 7 | encode : function (input) { 8 | var output = ""; 9 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 10 | var i = 0; 11 | 12 | input = Base64._utf8_encode(input); 13 | 14 | while (i < input.length) { 15 | 16 | chr1 = input.charCodeAt(i++); 17 | chr2 = input.charCodeAt(i++); 18 | chr3 = input.charCodeAt(i++); 19 | 20 | enc1 = chr1 >> 2; 21 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 22 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 23 | enc4 = chr3 & 63; 24 | 25 | if (isNaN(chr2)) { 26 | enc3 = enc4 = 64; 27 | } else if (isNaN(chr3)) { 28 | enc4 = 64; 29 | } 30 | 31 | output = output + 32 | this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + 33 | this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); 34 | 35 | } 36 | 37 | return output; 38 | }, 39 | 40 | // public method for decoding 41 | decode : function (input) { 42 | var output = ""; 43 | var chr1, chr2, chr3; 44 | var enc1, enc2, enc3, enc4; 45 | var i = 0; 46 | 47 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 48 | 49 | while (i < input.length) { 50 | 51 | enc1 = this._keyStr.indexOf(input.charAt(i++)); 52 | enc2 = this._keyStr.indexOf(input.charAt(i++)); 53 | enc3 = this._keyStr.indexOf(input.charAt(i++)); 54 | enc4 = this._keyStr.indexOf(input.charAt(i++)); 55 | 56 | chr1 = (enc1 << 2) | (enc2 >> 4); 57 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 58 | chr3 = ((enc3 & 3) << 6) | enc4; 59 | 60 | output = output + String.fromCharCode(chr1); 61 | 62 | if (enc3 != 64) { 63 | output = output + String.fromCharCode(chr2); 64 | } 65 | if (enc4 != 64) { 66 | output = output + String.fromCharCode(chr3); 67 | } 68 | 69 | } 70 | 71 | if (i != input.length) { 72 | //messages.addMessage(BASE64_BROKEN); 73 | throw "error"; 74 | } 75 | 76 | output = Base64._utf8_decode(output); 77 | 78 | return output; 79 | 80 | }, 81 | 82 | // private method for UTF-8 encoding 83 | _utf8_encode : function (string) { 84 | string = string.replace(/\r\n/g,"\n"); 85 | var utftext = ""; 86 | 87 | for (var n = 0; n < string.length; n++) { 88 | 89 | var c = string.charCodeAt(n); 90 | 91 | if (c < 128) { 92 | utftext += String.fromCharCode(c); 93 | } 94 | else if((c > 127) && (c < 2048)) { 95 | utftext += String.fromCharCode((c >> 6) | 192); 96 | utftext += String.fromCharCode((c & 63) | 128); 97 | } 98 | else { 99 | utftext += String.fromCharCode((c >> 12) | 224); 100 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 101 | utftext += String.fromCharCode((c & 63) | 128); 102 | }; 103 | 104 | }; 105 | 106 | return utftext; 107 | }, 108 | 109 | // private method for UTF-8 decoding 110 | _utf8_decode : function (utftext) { 111 | var string = ""; 112 | var i = 0; 113 | var c = c1 = c2 = 0; 114 | 115 | while ( i < utftext.length ) { 116 | 117 | c = utftext.charCodeAt(i); 118 | 119 | if (c < 128) { 120 | string += String.fromCharCode(c); 121 | i++; 122 | } 123 | else if((c > 191) && (c < 224)) { 124 | c2 = utftext.charCodeAt(i+1); 125 | string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); 126 | i += 2; 127 | } 128 | else { 129 | c2 = utftext.charCodeAt(i+1); 130 | c3 = utftext.charCodeAt(i+2); 131 | string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); 132 | i += 3; 133 | } 134 | 135 | } 136 | 137 | return string; 138 | } 139 | }; 140 | 141 | exports.encode = function() { return Base64.encode.apply(Base64, arguments) }; 142 | exports.decode = function() { return Base64.decode.apply(Base64, arguments) }; -------------------------------------------------------------------------------- /lib/couchdb.js: -------------------------------------------------------------------------------- 1 | var 2 | Q = require("promised-io/lib/promise"), 3 | base64 = require("./base64"), 4 | when = Q.when, 5 | request = require('request'), 6 | defineProperty = Object.defineProperty, 7 | url = require('url'), 8 | httpMethod = { 9 | POST: "POST", 10 | GET: "GET", 11 | DELETE: "DELETE", 12 | PUT: "PUT" 13 | }; 14 | 15 | exports.version = [0,3,7]; 16 | 17 | /* ----------------------------------------------------------------------------*/ 18 | exports.createClient = function() { 19 | var port 20 | , host 21 | , opts 22 | , couchClient 23 | , args = Array.prototype.slice.call(arguments) 24 | , oldRequest; 25 | 26 | if (args.length === 3) { 27 | opts = args.pop(); 28 | host = args.pop(); 29 | port = args.pop(); 30 | opts.host = host; 31 | opts.port = port; 32 | } else if (args.length === 2) { 33 | host = args.pop(); 34 | port = args.pop(); 35 | opts = { host: host, port: port }; 36 | } else if (args.length === 1) { 37 | opts = args.pop(); 38 | } else { 39 | throw "Invalid Argument List, createClient expects at least one argument and at most three"; 40 | } 41 | 42 | couchClient = new CouchClient(opts); 43 | 44 | return couchClient; 45 | }; 46 | 47 | /** 48 | * CouchClient 49 | * 50 | * @param {Object} opts 51 | * 52 | * @constructor 53 | */ 54 | function CouchClient(opts){ 55 | for (var k in opts) { 56 | this[k] = opts[k]; 57 | } 58 | } 59 | 60 | /* ----------------------------------------------------------------------------*/ 61 | /** 62 | * Get options for http client request 63 | * 64 | * @api private 65 | * @return {Object} Request options 66 | */ 67 | CouchClient.prototype.getRequestOptions = function(opts) { 68 | var defaults = { 69 | uri: { 70 | protocol: 'http:', 71 | hostname: this.host, 72 | port: this.port, 73 | pathname: '' 74 | }, 75 | headers: { "Content-Type": "application/json" } 76 | }; 77 | 78 | opts = complete(opts, defaults); 79 | opts.uri.href = url.format(opts.uri); 80 | opts.uri.pathname = removeAttr.call(opts, 'pathname') || ''; 81 | if (opts.query) { 82 | opts.uri.query = removeAttr.call(opts, 'query'); 83 | } 84 | if (opts.search) { 85 | opts.uri.search = removeAttr.call(opts, 'search'); 86 | } 87 | 88 | if (opts.headers["Content-Type"] === undefined || opts.headers["Content-Type"] === null) { 89 | opts.headers["Content-Type"] = "application/json"; 90 | } 91 | 92 | if (this.user) { 93 | opts.headers["Authorization"] = "Basic "+base64.encode(this.user+":"+this.password); 94 | } 95 | 96 | if (typeof opts.body === "string") { 97 | opts.body = [opts.body]; 98 | } 99 | if (opts.body) { 100 | opts.uri.body = opts.body; 101 | } 102 | 103 | return opts; 104 | }; 105 | 106 | /** 107 | * @api private 108 | */ 109 | CouchClient.prototype.requestRaw = function(opts) { 110 | var opts = this.getRequestOptions(opts) 111 | , deferred = Q.defer(); 112 | 113 | request(opts, function(err, response, body) { 114 | if (err) { 115 | return deferred.reject(err); 116 | } 117 | 118 | return deferred.resolve(body); 119 | }); 120 | 121 | return deferred.promise; 122 | }; 123 | 124 | /** 125 | * Request helper. 126 | * 127 | * @api private 128 | */ 129 | CouchClient.prototype.request = function(opts) { 130 | var opts = this.getRequestOptions(opts) 131 | , deferred = Q.defer(); 132 | 133 | request(opts, function(err, response, body) { 134 | var body; 135 | 136 | if (err) { 137 | return deferred.reject(err); 138 | } 139 | 140 | try { 141 | body = JSON.parse(body); 142 | } catch (err) { 143 | deferred.reject('Error parsing CouchDB response: '+body); 144 | } 145 | 146 | if (response.statusCode >= 400) { 147 | body.status = response.statusCode; 148 | return deferred.reject(body); 149 | } 150 | 151 | deferred.resolve(body); 152 | }); 153 | 154 | return deferred.promise; 155 | }; 156 | 157 | /* ----------------------------------------------------------------------------*/ 158 | CouchClient.prototype.prepareUserDoc = function(doc, password) { 159 | doc._id = doc._id || USER_PREFIX+doc.name; 160 | doc.type = "user"; 161 | doc.roles = doc.roles || []; 162 | 163 | if (password) { 164 | return when(this.uuids(1), function(uuids) { 165 | doc.salt = uuids[0]; 166 | doc.password_sha = require("./sha1").hex_sha1(password + doc.salt); 167 | 168 | return doc; 169 | }); 170 | 171 | } 172 | return doc; 173 | }; 174 | 175 | /** 176 | * Get the users DB for this CouchDB Server 177 | * 178 | * @returns {Promise} A promise that resolves to the user database 179 | */ 180 | CouchClient.prototype.userDb = function() { 181 | var self = this; 182 | 183 | return when(this.session(), function(resp) { 184 | var 185 | userDbName = resp.info.authentication_db, 186 | userDb = self.db(userDbName); 187 | 188 | userDb.hasUser = function(name) { 189 | return this.exists(USER_PREFIX+name); 190 | }; 191 | 192 | return userDb; 193 | }); 194 | }; 195 | 196 | /* ----------------------------------------------------------------------------*/ 197 | CouchClient.prototype.signup = function(user, newPassword, opts) { 198 | var self = this; 199 | 200 | opts = opts || {}; 201 | 202 | return when(this.userDb(), function(db) { 203 | return when(self.prepareUserDoc(user, newPassword), function(doc) { 204 | return db.saveDoc(doc); 205 | }); 206 | }); 207 | }; 208 | 209 | /* ----------------------------------------------------------------------------*/ 210 | CouchClient.prototype.allDbs = function() { 211 | return this.request({ 212 | pathname: "/_all_dbs" 213 | }); 214 | }; 215 | 216 | /* ----------------------------------------------------------------------------*/ 217 | CouchClient.prototype.config = function() { 218 | return this.request({ 219 | pathname: "/_config" 220 | }); 221 | }; 222 | 223 | /* ----------------------------------------------------------------------------*/ 224 | CouchClient.prototype.session = function() { 225 | return this.request({ 226 | pathname: "/_session" 227 | }); 228 | }; 229 | 230 | /* ----------------------------------------------------------------------------*/ 231 | /** 232 | * Retrieve unique identifiers generated by CouchDB 233 | * 234 | * @param {Number|Function} count If this is a function, it becomes the callback. If it is a number, it is the number of unique identifiers to retrieve 235 | * @param {Function} cb Callback 236 | * @api public 237 | */ 238 | CouchClient.prototype.uuids = function(count) { 239 | var opts = { 240 | pathname: "/_uuids" 241 | }; 242 | 243 | if (count) { 244 | opts.search = '?count=' + count; 245 | } 246 | 247 | return when(this.request(opts), function(resp) { 248 | return resp.uuids; 249 | }); 250 | }; 251 | 252 | /* ----------------------------------------------------------------------------*/ 253 | CouchClient.prototype.replicate = function(source, target, opts) { 254 | opts = complete({}, { 255 | source: source, 256 | target: target 257 | }, opts); 258 | 259 | var req = { 260 | method: httpMethod.POST, 261 | pathname: "/_replicate", 262 | body: [JSON.stringify(opts)] 263 | }; 264 | 265 | if (opts.queryString) { 266 | req.queryString = opts.queryString; 267 | delete opts.queryString; 268 | } 269 | 270 | return this.request(req); 271 | }; 272 | 273 | /* ----------------------------------------------------------------------------*/ 274 | CouchClient.prototype.stats = function() { 275 | var args = Array.prototype.slice.call(arguments) 276 | , pathname = "/_stats" + ((args && args.length > 0) ? "/" + args.join("/") : ""); 277 | console.log('pathname', pathname); 278 | return this.request({ 279 | pathname: pathname 280 | }); 281 | }; 282 | 283 | /* ----------------------------------------------------------------------------*/ 284 | CouchClient.prototype.activeTasks = function() { 285 | return this.request({ 286 | pathname: "/_active_tasks" 287 | }); 288 | }; 289 | 290 | /* ----------------------------------------------------------------------------*/ 291 | CouchClient.prototype.session = function() { 292 | return this.request({ 293 | pathname: "/_session" 294 | }); 295 | }; 296 | 297 | /* ----------------------------------------------------------------------------*/ 298 | CouchClient.prototype.db = function(name) { 299 | if (name === undefined || name === null || name === "") { 300 | throw new Error("Name must contain a value"); 301 | } 302 | 303 | var couchClient = this, 304 | db = Object.create(new Database(), 305 | { 306 | "name": { 307 | get: function() { return name; } 308 | }, 309 | "client": { 310 | get: function() { return couchClient; } 311 | } 312 | }); 313 | 314 | db.request = function(opts) { 315 | opts = opts || {}; 316 | opts.pathname = "/"+name+(opts.appendName || "")+(opts.pathname || ""); 317 | 318 | return couchClient.request(opts); 319 | }; 320 | 321 | db.view = function(designDoc, viewName, opts){ 322 | opts = opts || {}; 323 | opts.pathname = "/" + this.name +"/_design/" + designDoc + "/_view/" + viewName + encodeOptions(opts); 324 | return couchClient.request(opts); 325 | }; 326 | 327 | return db; 328 | }; 329 | 330 | /* ----------------------------------------------------------------------------*/ 331 | var Database = exports.Database = function() {}; 332 | 333 | /* ----------------------------------------------------------------------------*/ 334 | Database.prototype.exists = function() { 335 | return when(this.request({ pathname: "" }), function(resp) { 336 | return true; 337 | }, function(err) { 338 | if (err.error && err.error === "not_found") { 339 | return false; 340 | } 341 | 342 | throw err; 343 | }); 344 | }; 345 | 346 | /* ----------------------------------------------------------------------------*/ 347 | Database.prototype.info = function() { 348 | return this.request({}, cb); 349 | }; 350 | 351 | /* ----------------------------------------------------------------------------*/ 352 | Database.prototype.create = function() { 353 | return this.request({ 354 | method: httpMethod.PUT 355 | }); 356 | }; 357 | 358 | /* ----------------------------------------------------------------------------*/ 359 | /** 360 | * Permanently delete database 361 | * 362 | * @return {Promise} a promise 363 | * @api public 364 | */ 365 | Database.prototype.remove = function() { 366 | if (arguments.length > 0) { 367 | return reject('Database.prototype.remove deletes the database. You passed arguments when none are used. '+ 368 | 'Are you sure you did not mean to use removeDoc?'); 369 | } 370 | 371 | return this.request({ 372 | method: httpMethod.DELETE 373 | }); 374 | }; 375 | 376 | Database.prototype.allDocs = function(opts) { 377 | return this.request(complete({ 378 | pathname: "/_all_docs" 379 | }, opts)); 380 | }; 381 | 382 | /** 383 | * Retrieve document by unique identifier 384 | * 385 | * @param {String} id Unique identifier of the document 386 | * @param {Function} cb Callback 387 | * @api public 388 | */ 389 | 390 | Database.prototype.getDoc = Database.prototype.openDoc = function(id, options) { 391 | if (Array.isArray(id)) { 392 | return Q.when(this.allDocs({ 393 | search: '?include_docs=true', 394 | body: JSON.stringify({ keys: id }), 395 | method: httpMethod.POST 396 | }), function(res) { 397 | return res.rows.map(function(x) { return x.doc; }); 398 | }); 399 | } 400 | 401 | return when(this.request({ 402 | headers: { 403 | 'Content-Type': 'application/json', 404 | 'Accept': 'application/json' 405 | }, 406 | pathname: "/"+id + encodeOptions(options) 407 | }), function(doc) { 408 | return doc; 409 | }, function(err) { 410 | if (err.status === 404) { 411 | return null; 412 | } 413 | 414 | throw err; 415 | }); 416 | }; 417 | 418 | /* ----------------------------------------------------------------------------*/ 419 | Database.prototype.getAttachmentResponse = function(doc, attachmentName){ 420 | return this.request({ 421 | method:httpMethod.GET, 422 | pathname:"/"+this.name + "/" + doc._id + "/" + attachmentName 423 | }) 424 | }; 425 | 426 | /** 427 | * Note: If the doc is an array or if a 'bulk: true' is included in opts, bulk 428 | * processing of the document(s) is performed. 429 | * 430 | * @param {Object|Array} doc Document to save. A promise for a document or Array is also accepted. 431 | */ 432 | Database.prototype.saveDoc = function(doc, opts) { 433 | var self = this 434 | , method = httpMethod.PUT 435 | , path = "/" 436 | , buf; 437 | 438 | return Q.when(doc, function(doc) { 439 | if (doc._id === undefined) { 440 | method = httpMethod.POST; 441 | } else { 442 | path += doc._id; 443 | } 444 | 445 | // Test the doc to see if it is an array. 446 | if(Array.isArray(doc)){ 447 | // It is an array, so wrap the array for bulk processing. 448 | doc = {"docs":doc}; 449 | // Create a 'bulk' option to force 'bulk' processing. 450 | if(opts) { 451 | opts.bulk = true; 452 | } else { 453 | opts = { bulk : true }; 454 | } 455 | } 456 | 457 | buf = new Buffer(JSON.stringify(doc), 'utf-8'); 458 | 459 | return self.request({ 460 | appendName: (opts && opts.bulk) ? "/_bulk_docs" : "", // Define factor appended to DB name. 461 | method: method, 462 | pathname: path, 463 | body: typeof doc === "string" ? [doc] : buf 464 | }); 465 | }); 466 | }; 467 | 468 | /** 469 | * Delete a document. 470 | * 471 | * @param {String} id Unique identifier of the document. 472 | * @param {String} rev Revision of the document. 473 | * @returns {Promise} 474 | */ 475 | Database.prototype.removeDoc = function(id, rev) { 476 | if (!id) { 477 | var deferred = Q.defer(); 478 | deferred.reject('when calling removeDoc, id must not be null or undefined'); 479 | return deferred.promise; 480 | } 481 | 482 | return this.request({ 483 | method: httpMethod.DELETE, 484 | pathname: "/"+id, 485 | search: "?rev="+rev 486 | }); 487 | }; 488 | 489 | /* ----------------------------------------------------------------------------*/ 490 | Database.prototype.security = function(obj) { 491 | if (obj === undefined || obj === null) { 492 | return this.request({ 493 | method: httpMethod.GET, 494 | pathname: "/_security" 495 | }); 496 | } 497 | 498 | return this.request({ 499 | method: httpMethod.PUT, 500 | pathname: "/_security", 501 | body: [ JSON.stringify(obj) ] 502 | }); 503 | }; 504 | 505 | /* ----------------------------------------------------------------------------*/ 506 | /** 507 | * @api private 508 | */ 509 | function getRequestOptions(args) { 510 | args = Array.prototype.slice.call(args); 511 | 512 | var 513 | cb = args.pop(), 514 | method = args.shift(), 515 | path = args.shift(), 516 | data = args.shift(), 517 | opts; 518 | 519 | 520 | if (typeof method === "object") { 521 | opts = method; 522 | } else if (typeof method === "string" && typeof path !== "string") { 523 | opts = { 524 | pathname: method, 525 | query: path 526 | }; 527 | } else { 528 | opts = { 529 | method: method, 530 | pathname: path, 531 | data: data 532 | }; 533 | } 534 | 535 | opts.cb = cb; 536 | opts.jar = false; 537 | 538 | return opts; 539 | } 540 | 541 | /* ----------------------------------------------------------------------------*/ 542 | function removeAttr(attr) { 543 | var val = this[attr]; 544 | delete this[attr]; 545 | 546 | return val; 547 | } 548 | 549 | /* ----------------------------------------------------------------------------*/ 550 | /** 551 | * Stringify function embedded inside of objects. Useful for couch views. 552 | * @api private 553 | */ 554 | function toJSON(data) { 555 | return JSON.stringify(data, function(key, val) { 556 | if (typeof val == 'function') { 557 | return val.toString(); 558 | } 559 | return val; 560 | }); 561 | } 562 | 563 | /* ----------------------------------------------------------------------------*/ 564 | // Convert a options object to an url query string. 565 | // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"' 566 | function encodeOptions(options) { 567 | var buf = []; 568 | if (typeof(options) == "object" && options !== null) { 569 | for (var name in options) { 570 | if (options.hasOwnProperty(name)) { 571 | var value = options[name]; 572 | if (name == "key" || name == "startkey" || name == "endkey") { 573 | value = toJSON(value); 574 | } 575 | buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value)); 576 | } 577 | } 578 | } 579 | if (!buf.length) { 580 | return ""; 581 | } 582 | return "?" + buf.join("&"); 583 | } 584 | 585 | /** 586 | * Updates an object with the properties of another object(s) if those 587 | * properties are not already defined for the target object. First argument is 588 | * the object to complete, the remaining arguments are considered sources to 589 | * complete from. If multiple sources contain the same property, the value of 590 | * the first source with that property will be the one inserted in to the 591 | * target. 592 | * 593 | * example usage: 594 | * util.complete({}, { hello: "world" }); // -> { hello: "world" } 595 | * util.complete({ hello: "narwhal" }, { hello: "world" }); // -> { hello: "narwhal" } 596 | * util.complete({}, { hello: "world" }, { hello: "le monde" }); // -> { hello: "world" } 597 | * 598 | * @returns Completed object 599 | * @type Object 600 | * @api private 601 | */ 602 | function complete() { 603 | return variadicHelper(arguments, function(target, source) { 604 | var key; 605 | for (key in source) { 606 | if ( 607 | Object.prototype.hasOwnProperty.call(source, key) && 608 | !Object.prototype.hasOwnProperty.call(target, key) 609 | ) { 610 | target[key] = source[key]; 611 | } 612 | } 613 | }); 614 | } 615 | 616 | /** 617 | * @param args Arguments list of the calling function 618 | * First argument should be a callback that takes target and source parameters. 619 | * Second argument should be target. 620 | * Remaining arguments are treated a sources. 621 | * 622 | * @returns Target 623 | * @type Object 624 | * @api private 625 | */ 626 | function variadicHelper(args, callback) { 627 | var sources = Array.prototype.slice.call(args); 628 | var target = sources.shift(); 629 | 630 | sources.forEach(function(source) { 631 | callback(target, source); 632 | }); 633 | 634 | return target; 635 | } 636 | 637 | function reject(reason) { 638 | var deferred = Q.defer(); 639 | deferred.reject(reason); 640 | return deferred.promise; 641 | } 642 | 643 | /* ----------------------------------------------------------------------------*/ 644 | var USER_PREFIX = exports.USER_PREFIX = "org.couchdb.user:"; 645 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | // Adapted from node.js 2 | var EventEmitter; 3 | 4 | if (process.EventEmitter) { 5 | EventEmitter = process.EventEmitter; 6 | } else { 7 | 8 | EventEmitter = function() { 9 | }; 10 | 11 | EventEmitter.prototype.emit = function (type) { 12 | // If there is no 'error' event listener then throw. 13 | if (type === 'error') { 14 | if (!this._events || !this._events.error || 15 | (this._events.error instanceof Array && !this._events.error.length)) 16 | { 17 | if (arguments[1] instanceof Error) { 18 | throw arguments[1]; 19 | } else { 20 | throw new Error("Uncaught, unspecified 'error' event."); 21 | } 22 | return false; 23 | } 24 | } 25 | 26 | if (!this._events) return false; 27 | if (!this._events[type]) return false; 28 | 29 | if (typeof this._events[type] == 'function') { 30 | if (arguments.length < 3) { 31 | // fast case 32 | this._events[type].call( this 33 | , arguments[1] 34 | , arguments[2] 35 | ); 36 | } else { 37 | // slower 38 | var args = Array.prototype.slice.call(arguments, 1); 39 | this._events[type].apply(this, args); 40 | } 41 | return true; 42 | 43 | } else if (this._events[type] instanceof Array) { 44 | var args = Array.prototype.slice.call(arguments, 1); 45 | 46 | 47 | var listeners = this._events[type].slice(0); 48 | for (var i = 0, l = listeners.length; i < l; i++) { 49 | listeners[i].apply(this, args); 50 | } 51 | return true; 52 | 53 | } else { 54 | return false; 55 | } 56 | }; 57 | 58 | EventEmitter.prototype.addListener = function (type, listener) { 59 | if ('function' !== typeof listener) { 60 | throw new Error('addListener only takes instances of Function'); 61 | } 62 | 63 | if (!this._events) this._events = {}; 64 | 65 | // To avoid recursion in the case that type == "newListeners"! Before 66 | // adding it to the listeners, first emit "newListeners". 67 | this.emit("newListener", type, listener); 68 | 69 | if (!this._events[type]) { 70 | // Optimize the case of one listener. Don't need the extra array object. 71 | this._events[type] = listener; 72 | } else if (this._events[type] instanceof Array) { 73 | // If we've already got an array, just append. 74 | this._events[type].push(listener); 75 | } else { 76 | // Adding the second element, need to change to array. 77 | this._events[type] = [this._events[type], listener]; 78 | } 79 | 80 | return this; 81 | }; 82 | 83 | 84 | EventEmitter.prototype.removeListener = function (type, listener) { 85 | if ('function' !== typeof listener) { 86 | throw new Error('removeListener only takes instances of Function'); 87 | } 88 | 89 | // does not use listeners(), so no side effect of creating _events[type] 90 | if (!this._events || !this._events[type]) return this; 91 | 92 | var list = this._events[type]; 93 | 94 | if (list instanceof Array) { 95 | var i = list.indexOf(listener); 96 | if (i < 0) return this; 97 | list.splice(i, 1); 98 | } else if (this._events[type] === listener) { 99 | this._events[type] = null; 100 | } 101 | 102 | return this; 103 | }; 104 | 105 | EventEmitter.prototype.removeAllListeners = function (type) { 106 | // does not use listeners(), so no side effect of creating _events[type] 107 | if (type && this._events && this._events[type]) this._events[type] = null; 108 | return this; 109 | }; 110 | 111 | EventEmitter.prototype.listeners = function (type) { 112 | if (!this._events) this._events = {}; 113 | if (!this._events[type]) this._events[type] = []; 114 | if (!(this._events[type] instanceof Array)) { 115 | this._events[type] = [this._events[type]]; 116 | } 117 | return this._events[type]; 118 | }; 119 | } 120 | 121 | exports.EventEmitter = EventEmitter; 122 | -------------------------------------------------------------------------------- /lib/sha1.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 3 | * in FIPS PUB 180-1 4 | * Version 2.1a Copyright Paul Johnston 2000 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for details. 8 | */ 9 | 10 | /* 11 | * Configurable variables. You may need to tweak these to be compatible with 12 | * the server-side, but the defaults work in most cases. 13 | */ 14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 15 | var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */ 16 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 17 | 18 | /* 19 | * These are the functions you'll usually want to call 20 | * They take string arguments and return either hex or base-64 encoded strings 21 | */ 22 | function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} 23 | function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} 24 | function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} 25 | function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));} 26 | function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} 27 | function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} 28 | 29 | exports.hex_sha1 = hex_sha1; 30 | exports.b64_sha1 = b64_sha1; 31 | exports.str_sha1 = str_sha1; 32 | exports.hex_hmac_sha1 = hex_hmac_sha1; 33 | exports.b64_hmac_sha1 = b64_hmac_sha1; 34 | exports.str_hmac_sha1 = str_hmac_sha1; 35 | 36 | /* 37 | * Perform a simple self-test to see if the VM is working 38 | */ 39 | function sha1_vm_test() 40 | { 41 | return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; 42 | } 43 | 44 | /* 45 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 46 | */ 47 | function core_sha1(x, len) 48 | { 49 | /* append padding */ 50 | x[len >> 5] |= 0x80 << (24 - len % 32); 51 | x[((len + 64 >> 9) << 4) + 15] = len; 52 | 53 | var w = Array(80); 54 | var a = 1732584193; 55 | var b = -271733879; 56 | var c = -1732584194; 57 | var d = 271733878; 58 | var e = -1009589776; 59 | 60 | for(var i = 0; i < x.length; i += 16) 61 | { 62 | var olda = a; 63 | var oldb = b; 64 | var oldc = c; 65 | var oldd = d; 66 | var olde = e; 67 | 68 | for(var j = 0; j < 80; j++) 69 | { 70 | if(j < 16) w[j] = x[i + j]; 71 | else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); 72 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 73 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 74 | e = d; 75 | d = c; 76 | c = rol(b, 30); 77 | b = a; 78 | a = t; 79 | } 80 | 81 | a = safe_add(a, olda); 82 | b = safe_add(b, oldb); 83 | c = safe_add(c, oldc); 84 | d = safe_add(d, oldd); 85 | e = safe_add(e, olde); 86 | } 87 | return Array(a, b, c, d, e); 88 | 89 | } 90 | 91 | /* 92 | * Perform the appropriate triplet combination function for the current 93 | * iteration 94 | */ 95 | function sha1_ft(t, b, c, d) 96 | { 97 | if(t < 20) return (b & c) | ((~b) & d); 98 | if(t < 40) return b ^ c ^ d; 99 | if(t < 60) return (b & c) | (b & d) | (c & d); 100 | return b ^ c ^ d; 101 | } 102 | 103 | /* 104 | * Determine the appropriate additive constant for the current iteration 105 | */ 106 | function sha1_kt(t) 107 | { 108 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 109 | (t < 60) ? -1894007588 : -899497514; 110 | } 111 | 112 | /* 113 | * Calculate the HMAC-SHA1 of a key and some data 114 | */ 115 | function core_hmac_sha1(key, data) 116 | { 117 | var bkey = str2binb(key); 118 | if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz); 119 | 120 | var ipad = Array(16), opad = Array(16); 121 | for(var i = 0; i < 16; i++) 122 | { 123 | ipad[i] = bkey[i] ^ 0x36363636; 124 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 125 | } 126 | 127 | var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); 128 | return core_sha1(opad.concat(hash), 512 + 160); 129 | } 130 | 131 | /* 132 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 133 | * to work around bugs in some JS interpreters. 134 | */ 135 | function safe_add(x, y) 136 | { 137 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 138 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 139 | return (msw << 16) | (lsw & 0xFFFF); 140 | } 141 | 142 | /* 143 | * Bitwise rotate a 32-bit number to the left. 144 | */ 145 | function rol(num, cnt) 146 | { 147 | return (num << cnt) | (num >>> (32 - cnt)); 148 | } 149 | 150 | /* 151 | * Convert an 8-bit or 16-bit string to an array of big-endian words 152 | * In 8-bit function, characters >255 have their hi-byte silently ignored. 153 | */ 154 | function str2binb(str) 155 | { 156 | var bin = Array(); 157 | var mask = (1 << chrsz) - 1; 158 | for(var i = 0; i < str.length * chrsz; i += chrsz) 159 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); 160 | return bin; 161 | } 162 | 163 | /* 164 | * Convert an array of big-endian words to a string 165 | */ 166 | function binb2str(bin) 167 | { 168 | var str = ""; 169 | var mask = (1 << chrsz) - 1; 170 | for(var i = 0; i < bin.length * 32; i += chrsz) 171 | str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); 172 | return str; 173 | } 174 | 175 | /* 176 | * Convert an array of big-endian words to a hex string. 177 | */ 178 | function binb2hex(binarray) 179 | { 180 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 181 | var str = ""; 182 | for(var i = 0; i < binarray.length * 4; i++) 183 | { 184 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + 185 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); 186 | } 187 | return str; 188 | } 189 | 190 | /* 191 | * Convert an array of big-endian words to a base-64 string 192 | */ 193 | function binb2b64(binarray) 194 | { 195 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 196 | var str = ""; 197 | for(var i = 0; i < binarray.length * 4; i += 3) 198 | { 199 | var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) 200 | | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) 201 | | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); 202 | for(var j = 0; j < 4; j++) 203 | { 204 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 205 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 206 | } 207 | } 208 | return str; 209 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "couchdb", 3 | "version": "0.3.7", 4 | "directories": { "lib": "./lib" }, 5 | "main": "./lib/couchdb.js", 6 | "author": "Nathan Stott", 7 | "dependencies": { 8 | "promised-io": "v0.2.3", 9 | "request": "=v2.2.9" 10 | }, 11 | "devDependencies": { 12 | "expresso": ">=0.8.1" 13 | }, 14 | "scripts": { 15 | "test": "expresso tests/couchclient.js --serial --timeout 20000" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/couchclient.js: -------------------------------------------------------------------------------- 1 | var 2 | DB_NAME = "commonjs-couchdb-client-test", 3 | DB_NAME2 = "commonjs-couchdb-client-test-mirror", 4 | TEST_ID = "ABC123", 5 | TEST_DOC = { hello: "world" }, 6 | assert = require("assert"), 7 | couchdb = require("../lib/couchdb"), 8 | Q = require("promised-io/lib/promise"), 9 | when = Q.when, 10 | settings = require("./settings").couchdb, 11 | client = couchdb.createClient(settings.port, settings.host, settings), 12 | sys = require("sys"), 13 | db, db2; 14 | 15 | function identity() {} 16 | 17 | function before() { 18 | return db.exists().then(function(exists) { 19 | if (!exists) { 20 | return db.create(); 21 | } 22 | return db; 23 | }, function(err) { 24 | console.log('exists err', err); 25 | }); 26 | }; 27 | 28 | function test(callback) { 29 | return function(done) { 30 | callback().then(function() { 31 | done(); 32 | }, function(err) { 33 | console.log(err); 34 | assert.ok(false, err.stack ? err.stack : err); 35 | done(); 36 | }); 37 | }; 38 | } 39 | 40 | exports.setup = function(done) { 41 | db = client.db(DB_NAME); 42 | db2 = client.db(DB_NAME2); 43 | 44 | var db2Promise = db2.exists().then(function(exists) { 45 | if (exists) { 46 | return db2.remove().then(null, function(err) { 47 | if (err.error !== 'not_found') { 48 | throw err; 49 | } 50 | }); 51 | } 52 | }, function(err) { 53 | console.log('unable to delete db', err.message, err.stack); 54 | }); 55 | 56 | var dbPromise = db.exists().then(function(exists) { 57 | if (exists) { 58 | return db.remove().then(null, function(err) { 59 | if (err.error !== 'not_found') { 60 | throw err; 61 | } 62 | }); 63 | } 64 | }, function(err) { 65 | console.log('unable to delete db', err.message, err.stack); 66 | }); 67 | 68 | Q.all([ db2Promise, dbPromise ]).then(done, function(err) { 69 | done(); 70 | }); 71 | }; 72 | 73 | exports["test should get all dbs as an array"] = test(function() { 74 | var hasRun = false; 75 | 76 | return client.allDbs().then(function(resp) { 77 | assert.notEqual(null, resp); 78 | assert.ok(Array.isArray(resp), 'Response should be an array, was "'+typeof(resp)+'"'); 79 | }); 80 | }); 81 | 82 | exports["test should get UUIDs"] = test(function() { 83 | var count = 5; 84 | 85 | return client.uuids(count).then(function(uuids) { 86 | assert.notEqual(null, uuids, "uuids should not be null."); 87 | assert.equal(count, uuids.length, "Expected "+count+" uuids. "+JSON.stringify(uuids)); 88 | }); 89 | }); 90 | 91 | exports["test should get config"] = test(function() { 92 | var response = null; 93 | 94 | return client.config().then(function(resp) { 95 | assert.isNotNull(resp, 'Response should not be null'); 96 | }); 97 | }); 98 | 99 | exports["test should create and remove db"] = test(function() { 100 | var db = client.db(DB_NAME) 101 | , createResponse = null 102 | , removeResponse = db; 103 | 104 | return db.create().then(function(resp) { 105 | createResponse = resp; 106 | 107 | assert.isNotNull(createResponse, 'Create response should not be null'); 108 | assert.ok(createResponse.ok, 'createResponse.ok should be truthy'); 109 | 110 | db.remove().then(function(resp) { 111 | removeResponse = resp; 112 | 113 | assert.isNotNull(removeResponse, 'Remove response should not be null'); 114 | assert.ok(removeResponse.ok, 'removeResponse.ok should be truthy'); 115 | }); 116 | }); 117 | }); 118 | 119 | 120 | exports["test should signup user"] = test(function() { 121 | var name = "couchdb-commonjs-test" 122 | , response = null; 123 | 124 | return client.signup({ name: name }, "asdfasdf").then(function(resp) { 125 | assert.isNotNull(resp, 'response should not be null'); 126 | assert.ok(resp.ok, 'response.ok should be truthy'); 127 | 128 | return client.userDb().then(function(userDb) { 129 | return userDb.openDoc('org.couchdb.user:'+name).then(function(doc) { 130 | return userDb.removeDoc(doc._id, doc._rev); 131 | }); 132 | }); 133 | }); 134 | }); 135 | 136 | exports["test should get stats"] = test(function() { 137 | var response = null; 138 | 139 | return client.stats().then(function(resp) { 140 | assert.isNotNull(resp, 'response should not be null'); 141 | assert.isNotNull(resp.couchdb, 'response.couchdb should not be null'); 142 | }); 143 | }); 144 | 145 | exports["should get session"] = test(function() { 146 | var response = null; 147 | 148 | return client.session().then(function(resp) { 149 | assert.notEqual(null, resp); 150 | assert.ok(resp.ok); 151 | assert.ok(resp.info); 152 | }); 153 | }); 154 | 155 | exports["test should replicate"] = test(function() { 156 | var response 157 | , db1 = client.db(DB_NAME) 158 | , db2 = client.db(DB_NAME2); 159 | 160 | return Q.all([ db1.create(), db2.create() ]).then(function() { 161 | 162 | return client.replicate(DB_NAME, DB_NAME2) 163 | .then(function(resp) { 164 | assert.ok(resp, 'Should have received a response'); 165 | assert.ok(!resp.error, 'Response should not contain an error: '+JSON.stringify(response)); 166 | }) 167 | .then(function() { 168 | db1.remove(); 169 | db2.remove(); 170 | }); 171 | 172 | }, function(err) { 173 | var reason = ''; 174 | if (err && err.reason) { 175 | reason = err.reason; 176 | } 177 | 178 | throw 'Error creating databases: '+reason; 179 | }); 180 | }); 181 | 182 | exports["test should have no documents"] = test(function() { 183 | var response = null; 184 | 185 | return before().then(function() { 186 | db.allDocs().then(function(resp) { 187 | assert.ok(Array.isArray(resp.rows), "Expected resp.rows to be an array but it is '"+typeof(resp.rows)+"'"); 188 | assert.equal(0, resp.rows.length, "Expected 0 rows, found '"+resp.rows.length+"'"); 189 | }); 190 | }); 191 | }); 192 | 193 | exports["test should remove document"] = test(function() { 194 | var docId = "ABCZZZ" 195 | , response = null; 196 | 197 | return before().then(function() { 198 | return when(db.saveDoc({ _id: docId }), function(resp) { 199 | return when(db.removeDoc(resp.id, resp.rev), function(resp) { 200 | assert.ok(resp.ok); 201 | }); 202 | }); 203 | }); 204 | }); 205 | 206 | exports["test should create document with id"] = function(beforeExit) { 207 | 208 | before().then(function() { 209 | var response = null 210 | , docId = "ABC123" 211 | , saveDocPromise = db.saveDoc({ _id: docId, hello: "world" }); 212 | 213 | saveDocPromise.then(function() { 214 | db.openDoc(docId).then(function(doc) { 215 | if (doc !== null) { 216 | db.removeDoc(doc._id, doc._rev); 217 | } 218 | }); 219 | }); 220 | 221 | beforeExit(function(resp) { 222 | assert.notEqual(null, response); 223 | assert.ok(response.ok); 224 | }); 225 | 226 | when(saveDocPromise, function(resp) { 227 | response = resp; 228 | }); 229 | }); 230 | }; 231 | 232 | 233 | exports["test should create document without id"] = function(beforeExit) { 234 | 235 | before().then(function() { 236 | var response = null; 237 | 238 | when(db.saveDoc({ hello: "world" }), function(resp) { 239 | response = resp; 240 | }); 241 | 242 | beforeExit(function() { 243 | assert.ok(response.ok); 244 | }); 245 | }); 246 | }; 247 | 248 | exports["test should get security object"] = function(beforeExit) { 249 | 250 | before().then(function() { 251 | var response = null; 252 | 253 | when(db.security(), function(resp) { 254 | response = resp; 255 | }); 256 | 257 | beforeExit(function() { 258 | assert.deepEqual({}, response); 259 | }); 260 | 261 | }); 262 | }; 263 | 264 | 265 | exports["test should set security object"] = function(beforeExit) { 266 | before().then(function() { 267 | var securityObj = { readers: { names: ["tester"], roles: ["test_reader"] } } 268 | , response = null; 269 | 270 | beforeExit(function() { 271 | assert.ok(response.ok); 272 | assert.deepEqual(securityObj, response); 273 | }); 274 | 275 | return when(db.security(securityObj), function(resp) { 276 | return when(db.security(), function(resp) { 277 | response = resp; 278 | }) 279 | }); 280 | }); 281 | }; 282 | 283 | 284 | exports["test should be conflicted"] = function(beforeExit) { 285 | before().then(function() { 286 | var isConflicted = false; 287 | 288 | when(db.saveDoc( { _id: "hello-world" }), function() { 289 | var conflictPromise = db.saveDoc({ _id: "hello-world" }) 290 | , removeDoc; 291 | 292 | removeDoc = function() { 293 | return db.openDoc("hello-world").then(function(doc) { 294 | return db.removeDoc(doc._id, doc._rev); 295 | }); 296 | } 297 | 298 | return when(conflictPromise, function() { 299 | return removeDoc(); 300 | }, function(err) { 301 | isConflicted = true; 302 | return removeDoc(); 303 | }); 304 | }); 305 | 306 | beforeExit(function() { 307 | assert.ok(isConflicted, 'Should have been conflicted'); 308 | }); 309 | }); 310 | }; 311 | 312 | exports["test should get view"] = function(beforeExit){ 313 | before().then(function() { 314 | var db = client.db(DB_NAME) 315 | , response = null; 316 | 317 | var saveDocPromise = db.saveDoc({ 318 | _id: "_design/test", 319 | views: { "test": { map: "function(){emit(doc._id, doc)}" } } 320 | }); 321 | 322 | beforeExit(function() { 323 | assert.ok(response.rows, "rows should be defined"); 324 | }); 325 | 326 | return when(saveDocPromise, function() { 327 | return when(db.view("test", "test"), function success(resp){ 328 | response = resp; 329 | }); 330 | }); 331 | }); 332 | } 333 | 334 | exports["test getDoc should get multiple documents"] = function(beforeExit) { 335 | var docs; 336 | 337 | before().then(function() { 338 | db.saveDoc({ 339 | _id: 'doc1' 340 | }).then(function() { 341 | return db.saveDoc({ 342 | _id: 'doc2' 343 | }); 344 | }).then(function() { 345 | return db.getDoc(['doc1', 'doc2']); 346 | }).then(function(res) { 347 | console.log('res', res); 348 | docs = res; 349 | }); 350 | }, function(err) { 351 | console.log('err', err); 352 | }); 353 | 354 | beforeExit(function() { 355 | assert.ok(Array.isArray(docs)); 356 | assert.ok(docs[0]._id === 'doc1'); 357 | assert.ok(docs[1]._id === 'doc2'); 358 | }); 359 | }; 360 | -------------------------------------------------------------------------------- /tests/settings.js.example: -------------------------------------------------------------------------------- 1 | exports.couchdb = { 2 | user: 'username', 3 | password: 'password', 4 | port: 5984, 5 | host: 'localhost' 6 | }; 7 | --------------------------------------------------------------------------------