Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

major api refactor

  • Loading branch information...
commit 75db060931e760bdd730712d70fdef629bc5892c 1 parent edd3f66
Nathan authored
View
203 lib/couchdb.js
@@ -1,38 +1,158 @@
+"use strict"
+
var HttpClient = require("./couchdb/http_client").HttpClient,
Database = require("./couchdb/database").Database,
DesignDocument = require("./couchdb/design_document");
-function CouchDb(uri){
- this.uri = uri;
- this.httpClient = new HttpClient();
+/**
+ * Connect to a CouchDb Server.
+ * @param {String} uri Location of the couch server.
+ * @param {Object} httpClient Optional override for the default http client.
+ * @return an instance of CouchServer
+ * @type CouchServer
+ */
+exports.connect = function(uri, httpClient){
+ var version;
+ var couchServer;
+
+ if (!/\/$/.test(uri)){
+ uri = uri + "/";
+ }
+
+ if (httpClient === undefined){
+ httpClient = new HttpClient();
+ }
+
+ httpClient.get(uri, {
+ async: false,
+ success: function(data, textStatus) {
+ if (data && data.couchdb) {
+ if (data.couchdb === "Welcome") {
+ version = data.version;
+ }
+ }
+ }
+ });
+
+ if (version === undefined) {
+ return null;
+ }
+
+ couchServer = Object.create(CouchServer, {
+ /**
+ * Location of the couch server
+ */
+ uri: {
+ get: function() {
+ return uri;
+ }
+ },
+ /**
+ * Version of the couch server to which this instance of CouchServer is connected.
+ */
+ version: {
+ get: function() {
+ return version;
+ }
+ },
+ httpClient: {
+ get: function() {
+ return httpClient;
+ },
+ set: function(client) {
+ httpClient = client;
+ }
+ }
+ });
+
+ return couchServer;
+};
- return this;
+function getOptions(success, failure){
+ if (typeof success !== "function") {
+ throw new Error("Argument error: 'success' must be a function");
+ }
+ if (failure === undefined) {
+ failure = function(client, textStatus, errorThrown){
+ throw new Error(client.status + ": " + textStatus);
+ };
+ }
+ return { success: success, error: failure, async: false, dataType: "json" };
}
-CouchDb.prototype = {
- allDbs: function(options){
- var uri = this.uri + "_all_dbs";
- this.httpClient.get(uri, options);
+/**
+ * A session with a couch server.
+ */
+var CouchServer = {
+ allDbs: function(){
+ var dbs;
+
+ this.httpClient.get(this.uri + "_all_dbs", getOptions(function(data) {
+ dbs = data;
+ }));
+
+ return dbs;
+ },
+
+ /**
+ * Determines if this couch server has a database with specified name.
+ * @param name Name of database to search for.
+ * @return Whether database with name exists.
+ * @type Boolean
+ */
+ hasDb: function(name) {
+ var dbs = this.allDbs();
+ return this.allDbs().indexOf(name) !== -1;
},
- createDb: function(name, options){
- if (typeof name == undefined || name == null || name == "")
+
+ /**
+ *
+ * @param {String} name Database name
+ */
+ createDb: function(name){
+ var created = false;
+
+ if (typeof name == undefined || name == null || name == "") {
throw new Error("Argument Expected: name");
+ }
var uri = this.uri + name;
- this.httpClient.put(uri, options);
+ this.httpClient.put(uri, getOptions(function() { created = true; }, function() { created = false; }));
+
+ return created;
},
- deleteDb: function(name, options){
- if (name == undefined || name == null || name == "")
+
+ /**
+ * Delete a database
+ * @param {String} name Database name
+ */
+ deleteDb: function(name){
+ var deleted = false;
+
+ if (name == undefined || name == null || name == "") {
throw new Error("Argument Expected: name");
+ }
var uri = this.uri + name;
- this.httpClient.del(uri, options);
+ this.httpClient.del(uri, {
+ success: function() {
+ deleted = true;
+ },
+ error: function() {
+ deleted = false;
+ },
+ async: false
+ });
+
+ return deleted;
},
+
/**
- * returns a guranteed unique universal identifier
- * will never return the same identifier twice
+ * Retrieve a universal identifier generated by CouchDb.
+ * @return a guranteed unique universal identifier
+ * @type String
*/
uuid: function(){
var self = this;
@@ -53,12 +173,53 @@ CouchDb.prototype = {
});
return self.uuidCache.shift();
}
+ },
+ /**
+ * Get a Database object to work with a database that belongs to this CouchServer.
+ * Creates the database if it does not exist.
+ * @param name Name of the database.
+ * @return Database
+ * @type Object
+ */
+ database: function(name) {
+ var db;
+ var getUri = (function() { return this.uri; }).bind(this);
+ var getHttpClient = (function() { return this.httpClient; }).bind(this);
+ var getUuid = (function() { return this.uuid(); }).bind(this);
+ name = encodeURIComponent(name);
+
+ if (!this.hasDb(name)){
+ this.createDb(name);
+ }
+
+ if (!/\/$/.test(name)){
+ name += "/";
+ }
+
+ db = Object.create(Database, {
+ /**
+ * Name of database
+ * @type String
+ */
+ name: {
+ get: function() { return name; }
+ },
+ uri: {
+ get: function() { return getUri() + name; }
+ },
+ httpClient: {
+ get: function() { return getHttpClient(); }
+ }
+ });
+
+ db.uuid = function() {
+ return getUuid();
+ };
+
+ return db;
}
};
-/**
- * Exports
- */
-exports.CouchDb = CouchDb;
exports.Database = Database;
-exports.DesignDocument = DesignDocument;
+exports.DesignDocument = DesignDocument;
+
View
194 lib/couchdb/database.js
@@ -1,86 +1,97 @@
var util = require("util");
-function Database(server, name) {
- var self = this;
- this.name = encodeURIComponent(name);
- this.server = server;
- this.httpClient = this.server.httpClient;
- this.uri = function() { return self.server.uri + self.name + "/"; };
-
- server.allDbs({
- async: false,
- success: function(data, textStatus) {
- if (data.indexOf(self.name) == -1)
- self.server.createDb(self.name);
- }
- });
-
- return this;
-}
-
-Database.prototype = {
+Database = {
+ /**
+ * List of all documents in the database.
+ * @param {Object} options
+ * @returns Rows from the _all_docs query
+ * @type Array
+ */
allDocs: function(options){
var results = [];
options = options || {};
options.include_docs = options.include_docs || true;
- this.httpClient.get(this.uri() + encodeURIComponent("_all_docs") + encodeOptions(options), {
+ this.httpClient.get(this.uri + encodeURIComponent("_all_docs") + encodeOptions(options), {
async: false,
success: function(data, textStatus){
results = data.rows || [];
+ },
+ error: function(client, textStatus, errorThrown) {
+ var msg = client.status + ": " + textStatus;
+ if (errorThrown !== undefined) {
+ msg += " " + errorThrown;
+ }
+ throw new Error(msg);
}
});
return results;
},
- save: function(doc, success, error){
- if (doc._id == undefined)
- doc._id = this.server.uuid();
+ save: function(doc, shouldThrow){
+ var saved = false;
+ if (shouldThrow === undefined){
+ shouldThrow = false;
+ }
- var settings = { success: success, error: error };
+ if (doc._id === undefined) {
+ doc._id = this.uuid();
+ }
- this.httpClient.put(this.uri() + encodeURIComponent(doc._id), {
+ this.httpClient.put(this.uri + encodeURIComponent(doc._id), {
data: JSON.stringify(doc),
async: false,
success: function(data, statusText){
doc._id = data.id;
doc._rev = data.rev;
- if (settings.success instanceof Function) {
- settings.success(doc);
- }
+ saved = true;
},
error: function(xhr, textStatus, errorThrown) {
- if (settings.error instanceof Function) {
- settings.error(textStatus, errorThrown);
+ if (shouldThrow) {
+ throw new Error(xhr.status + ": " + textStatus);
}
+ saved = false;
}
});
+
+ return saved;
},
- bulkSave: function(docs, success, error){
+ /**
+ * Bulk save documents with the couchdb bulk save API
+ * @param {Object} docs Documents to save
+ * @param {Boolean} shouldThrow Should throw an error if fail?
+ * @returns True if save succeded, false if shouldThrow is false and bulkSave failed.
+ * @type Boolean
+ */
+ bulkSave: function(docs, shouldThrow){
var self = this;
-
+ var saved = false;
+ var err;
+
docs.forEach(function(doc){
- if (doc._id == undefined)
- doc._id = self.server.uuid();
+ if (doc._id == undefined) {
+ doc._id = self.uuid();
+ }
});
- var settings = { success: success, error: error };
-
- var err = null;
-
- this.httpClient.post(this.uri() + "_bulk_docs", {
+ this.httpClient.post(this.uri + "_bulk_docs", {
async: false,
data: JSON.stringify({ "docs": docs }),
success: function(data, textStatus) {
+ var doc;
+ saved = true;
for (var i=0;i<data.length;++i) {
- docs[i]._rev = data[i]._rev;
+ doc = docs.filter(function(x) {
+ return x._id === data[i].id;
+ });
+ if (doc && doc.length === 1) {
+ doc = doc[0];
+ doc._rev = data[i].rev;
+ }
}
- if (settings.success instanceof Function)
- settings.success(doc);
},
error: function(xhr, textStatus, errorThrown){
- print("BulkSave Failure: " + xhr.status + " " + textStatus);
err = { status: xhr.status,
textStatus: textStatus,
errorThrown: errorThrown
@@ -88,15 +99,20 @@ Database.prototype = {
}
});
- if (err) {
- throw new Error(err.status + ": " + err.textStatus || "" + err.errorThrown || "");
+ if (err !== undefined) {
+ saved = false;
+ if (shouldThrow) {
+ throw new Error(err.status + ": " + err.textStatus || "" + err.errorThrown || "");
+ }
}
+
+ return saved;
},
_findSingleDoc: function(id) {
var doc = null,
err = null;
- this.httpClient.get(this.uri() + encodeURIComponent(id), {
+ this.httpClient.get(this.uri + encodeURIComponent(id), {
async: false,
success: function(data, textStatus) {
doc = data;
@@ -119,7 +135,7 @@ Database.prototype = {
var docs = [],
err;
- this.httpClient.post(this.uri() + encodeURIComponent("_all_docs") + encodeOptions({ include_docs: true }), {
+ this.httpClient.post(this.uri + encodeURIComponent("_all_docs") + encodeOptions({ include_docs: true }), {
async: false,
data: JSON.stringify({ "keys" : ids }),
success: function(data, textStatus) {
@@ -147,14 +163,25 @@ Database.prototype = {
}
},
view: function(designDocName, viewName, options) {
- var viewResult = null;
+ var viewResult;
+ var keys;
+ var viewPath;
+ var verb = "get";
- designDocName = designDocName.replace(/^_design\//g, "");
+ options = options || {};
- var viewPath = this.uri() + "_design/" + designDocName + "/_view/" + viewName + encodeOptions(options);
+ if (options.keys) {
+ keys = JSON.stringify({ keys: options.keys });
+ delete options.keys;
+ verb = "post";
+ }
- this.httpClient.get(viewPath, {
+ designDocName = designDocName.replace(/^_design\//g, "");
+ viewPath = this.uri + "_design/" + designDocName + "/_view/" + viewName + encodeOptions(options);
+
+ this.httpClient[verb](viewPath, {
async: false,
+ data: keys,
success: function(data, textStatus) {
viewResult = data;
viewResult.totalRows = viewResult.total_rows;
@@ -167,12 +194,23 @@ Database.prototype = {
return viewResult;
},
list: function(designDocName, listName, viewName, options){
- var listResult = null;
+ var listResult;
+ var keys;
+ var verb = "get";
- var listUri = this.uri() + "_design/" + designDocName + "/_list/" + listName + "/" + viewName + encodeOptions(options);
+ options = options || {};
+ if (options.keys) {
+ keys = JSON.stringify({ "keys" : options.keys });
+ verb = "post";
+ delete options.keys;
+ }
+
+ var listUri = this.uri + "_design/" + designDocName + "/_list/" + listName + "/" + viewName + encodeOptions(options);
+ print("list: " + listUri);
- this.httpClient.get(listUri, {
+ this.httpClient[verb](listUri, {
async: false,
+ data: keys,
dataType: "json",
success: function(data, textStatus) {
listResult = data;
@@ -186,7 +224,7 @@ Database.prototype = {
},
fullTextSearch: function(designDocName, fullTextViewName, options) {
var operator;
-
+
options = options || {};
options = util.deepCopy(options);
options.limit = options.limit || 5;
@@ -209,7 +247,7 @@ Database.prototype = {
var fullTextResult = [];
- var uri = this.uri() + "_fti/" + designDocName + "/" + fullTextViewName + encodeOptions(options);
+ var uri = this.uri + "_fti/" + designDocName + "/" + fullTextViewName + encodeOptions(options);
this.httpClient.get(uri, {
async: false,
@@ -225,14 +263,16 @@ Database.prototype = {
},
addAttachmentToDoc: function(doc, attachmentName, contentType, attachmentData) {
- if (doc._rev == undefined) {
+ var response = true;
+
+ if (doc._rev === undefined) {
throw new Error("Argument 'doc' must have a '_rev' property in order to add attachments.");
}
- if (attachmentName == undefined || attachmentName == null || attachmentName == ""){
+ if (attachmentName === undefined || attachmentName === null || attachmentName === ""){
throw new Error("Argument 'attachmentName' is required");
}
- var uri = this.uri() + doc._id + "/" + attachmentName + "?rev=" + doc._rev;
+ var uri = this.uri + doc._id + "/" + attachmentName + "?rev=" + doc._rev;
this.httpClient.put(uri, {
headers: [ { "label" : "Content-Type", "value": contentType }],
data: attachmentData.toArray(),
@@ -241,7 +281,8 @@ Database.prototype = {
dataType: "json",
success: function(data, textStatus) {
if (!data.ok) {
- return false;
+ response = false;
+ return;
}
doc._rev = data.rev;
},
@@ -250,7 +291,35 @@ Database.prototype = {
}
});
- return true;
+ return response;
+ },
+ removeAttachmentFromDoc: function(doc, attachmentName) {
+ var response = true;
+
+ if (doc._id === undefined) {
+ throw new Error("Can not delete attachments from a doc that does not have an id");
+ }
+ if (doc._rev === undefined) {
+ throw new Error("Can not delete attachments from a doc that does not have a revision number");
+ }
+
+ var uri = this.uri + doc._id + "/" + attachmentName + "?rev=" + doc._rev;
+ this.httpClient.del(uri, {
+ async: false,
+ dataType: "json",
+ success: function(data, textStatus) {
+ if (!data.ok) {
+ response = false;
+ return;
+ }
+ doc._rev = data.rev;
+ },
+ error: function(xhr) {
+ throw new Error("Failed to delete attachment: " + xhr.status);
+ }
+ });
+
+ return response;
}
};
@@ -274,4 +343,5 @@ function encodeOptions(options) {
return "?" + buf.join("&");
}
-exports.Database = Database;
+exports.Database = Database;
+
View
9 tests/couchdb.tests.js
@@ -1,8 +1,9 @@
version(170);
-var fs = require("file");
-
+exports.config = require("./test-config");
exports.testCouchDb = require("./couchdb/all-tests");
-if (require.main == module.id)
- require('test/runner').run(exports);
+if (require.main == module.id) {
+ require('test/runner').run(exports);
+}
+
View
6 tests/couchdb/all-tests.js
@@ -1,3 +1,5 @@
exports.testServer = require("./server.tests");
-exports.testDatabase = require("./database.tests");
-exports.testAttachments = require("./attachment_tests");
+exports.testDatabase = require("./database/all-tests");
+exports.testAttachments = require("./attachment_tests");
+exports.testDesignDocument = require("./design_document_tests");
+
View
3  tests/couchdb/database.tests.js
@@ -42,4 +42,5 @@ with(require("../test_helper")) {
}
}
];
-}
+}
+
View
9 tests/couchdb/database/all-tests.js
@@ -0,0 +1,9 @@
+
+exports["test database with slash in name"] = require("./database-with-slash-in-name");
+exports["test empty database"] = require("./empty-database-tests");
+exports["test database with one doc"] = require("./database-with-one-doc");
+
+if (require.main === module.id) {
+ require("test/runner").run(exports);
+}
+
View
18 tests/couchdb/database/database-with-one-doc.js
@@ -0,0 +1,18 @@
+var db;
+var doc;
+
+with(require("../../test_helper")){
+
+ exports.setup = function() {
+ db = recreateDb();
+ doc = {
+ _id: "org.whiteboard-it.com/tester_mcgee",
+ name: "Tester"
+ };
+ db.save(doc);
+ };
+
+ exports["test get the document"] = function() {
+ assert.isTrue(db.find(doc._id).name === "Tester");
+ };
+}
View
18 tests/couchdb/database/database-with-slash-in-name.js
@@ -0,0 +1,18 @@
+var couchdb = require("couchdb");
+var config = require("../../test-config");
+var assert = require("test/assert");
+var db;
+var server;
+
+exports.setup = function() {
+ server = couchdb.connect(config.uri);
+ db = server.database("with/slash");
+};
+
+exports["test escapes databasename in uri"] = function() {
+ assert.isEqual("with%2Fslash/", db.name);
+};
+
+exports.teardown = function() {
+ server.deleteDb(db.name);
+};
View
64 tests/couchdb/database/empty-database-tests.js
@@ -0,0 +1,64 @@
+var helper = require("../../test_helper");
+var db;
+
+with(helper) {
+ exports.setup = function() {
+ db = recreateDb();
+ };
+
+ exports["test has no documents"] = function() {
+ var docs = db.allDocs();
+ assert.equal(0, docs.length);
+ };
+
+ exports["test save document"] = function() {
+ db.save({ hello: "world" });
+ };
+
+ exports["test bulk saving docs"] = function() {
+ var docs = [
+ {
+ name: "Nathan"
+ },
+ {
+ name: "Martin"
+ },
+ {
+ name: "David"
+ }
+ ];
+ db.bulkSave(docs);
+
+ assert.equal(3, db.allDocs().length, "Should have three documents");
+ docs.forEach(function(doc) {
+ assert.equal(db.find(doc._id)._rev, doc._rev);
+ });
+ };
+
+ exports["test bulk save uses uuids when ids are not defined"] = function() {
+ var docs = [
+ {},
+ {}
+ ];
+ var uuid = "ijasdjasdfj";
+ var post = {};
+ db.uuid = function() {
+ return uuid;
+ };
+ Object.defineProperty(db, "httpClient", {
+ get: function() {
+ return {
+ post: function(uri, options) {
+ post.uri = uri;
+ post.options = options;
+ }
+ };
+ }
+ });
+
+ db.bulkSave(docs);
+
+ assert.equal(uuid, JSON.parse(post.options.data).docs[0]._id);
+ };
+}
+
View
6 tests/couchdb/design_document_tests.js
@@ -1,4 +1,4 @@
-var DesignDocument = require("../../packages/models/design_document");
+var DesignDocument = require("couchdb").DesignDocument;
var assert = require("test/assert");
exports["test create without id raises error"] = function(){
@@ -17,7 +17,7 @@ exports["test toDocument returns a document with id modified with _design/"] = f
assert.isEqual("_design/mydd", dd.toDocument()._id);
};
-exports["test viewBy with one member"] = function(){
+exports["test viewBy one member"] = function(){
var dd = DesignDocument.create("mydd", function(){
this.viewBy("name");
});
@@ -27,7 +27,7 @@ exports["test viewBy with one member"] = function(){
assert.isEqual("function(doc){if(doc['name']){emit(doc['name'],doc);}}",doc.views["by_name"]["map"]);
};
-exports["test viewBy with two members"] = function(){
+exports["test viewBy two members"] = function(){
var dd = DesignDocument.create("mydd", function(){
this.viewBy("name_and_docType");
});
View
32 tests/couchdb/server.tests.js
@@ -1,28 +1,16 @@
-var assert = require("test/assert"),
- CouchDb = require("couchdb").CouchDb;
+var assert = require("test/assert");
+var couch = require("couchdb");
+var config = require("../test-config");
-exports.testConnectingToServer = function(){
- var db = new CouchDb("http://localhost:5984/");
+exports.testConnectingToInvalidServer = function() {
+ var server = couch.connect("http://localhost:29999");
- assert.isTrue(db != null);
+ assert.isEqual(null, server, "Should have returned null");
};
-exports.testCreateAndDeleteDatabase = function(){
- var server = new CouchDb("http://localhost:5984/");
- server.createDb("serverjs_test");
-
- var databases;
- server.allDbs({ async: false, success: function(data) {
- databases = data;
- }});
-
- assert.isFalse(databases.indexOf("serverjs_test") == -1);
-
- server.deleteDb("serverjs_test");
+exports.testConnectingToServer = function(){
+ var server = couch.connect(config.uri);
- server.allDbs({ async: false, success: function(data) {
- databases = data;
- }});
+ assert.isTrue(server != null);
+};
- assert.isTrue(databases.indexOf("serverjs_test") == -1);
-};
View
2  tests/test-config.js
@@ -0,0 +1,2 @@
+exports.uri = "http://localhost:5984";
+exports.database = "couch-client-test";
View
23 tests/test_helper.js
@@ -1,15 +1,26 @@
-var CouchDb = require("couchdb").CouchDb;
+var couch = require("couchdb");
var Database = require("couchdb").Database;
-var server = new CouchDb("http://localhost:5984/");
+var assert = require("test/assert");
var dbName = "serverjs_database_test";
+var config = require("./test-config");
+var server;
exports.recreateDb = function(){
- server.deleteDb(dbName);
- server.createDb(dbName);
+ server = couch.connect(config.uri);
+ if (server.hasDb(dbName)) {
+ server.deleteDb(dbName);
+ }
- return new Database(server, dbName);
+ return server.database(dbName);
};
exports.deleteDb = function() {
server.deleteDb(dbName);
-};
+};
+
+if (assert.isEqual !== undefined) {
+ assert.equal = assert.isEqual;
+}
+
+exports.assert = assert;
+
Please sign in to comment.
Something went wrong with that request. Please try again.