diff --git a/.travis.yml b/.travis.yml index d28a25d8..5269a4cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ services: - mongodb - redis-server - neo4j + - couchdb before_install: - git submodule init && git submodule --quiet update - ./support/ci/neo4j.sh @@ -13,3 +14,4 @@ before_script: - "mysql -e 'create database myapp_test;'" - "psql -c 'create database myapp_test;' -U postgres" - mongo mydb_test --eval 'db.addUser("travis", "test");' + - curl -X PUT localhost:5984/nano-test \ No newline at end of file diff --git a/Cakefile b/Cakefile new file mode 100644 index 00000000..c325fe4c --- /dev/null +++ b/Cakefile @@ -0,0 +1,4 @@ +{spawn} = require 'child_process' + +task 'build', 'Build lib/ from src/', -> + spawn 'coffee', ['-c', '-o', 'lib', 'src'], stdio: 'inherit' \ No newline at end of file diff --git a/lib/abstract-class.js b/lib/abstract-class.js index 12e6bf4f..61c9a6ee 100644 --- a/lib/abstract-class.js +++ b/lib/abstract-class.js @@ -193,12 +193,15 @@ AbstractClass.create = function (data, callback) { var data = this.toObject(true); // Added this to fix the beforeCreate trigger not fire. // The fix is per issue #72 and the fix was found by by5739. - this._adapter().create(modelName, this.constructor._forDB(data), function (err, id) { + this._adapter().create(modelName, this.constructor._forDB(data), function (err, id, rev) { if (id) { obj.__data.id = id; obj.__dataWas.id = id; defineReadonlyProp(obj, 'id', id); } + if (rev) { + obj._rev = rev + } done.call(this, function () { if (callback) { callback(err, obj); diff --git a/lib/adapters/nano.js b/lib/adapters/nano.js new file mode 100644 index 00000000..cec6456d --- /dev/null +++ b/lib/adapters/nano.js @@ -0,0 +1,318 @@ +// Generated by CoffeeScript 1.4.0 +(function() { + var NanoAdapter, helpers, _, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __slice = [].slice; + + _ = require('lodash')._; + + exports.initialize = function(schema, callback) { + var db, design, opts; + if (!(opts = schema.settings)) { + throw new Error('url is missing'); + } + db = require('nano')(opts); + schema.adapter = new NanoAdapter(db); + design = { + views: { + by_model: { + map: 'function (doc) { if (doc.model) return emit(doc.model, null); }' + } + } + }; + return db.insert(design, '_design/nano', function(err, doc) { + return callback(); + }); + }; + + NanoAdapter = (function() { + + function NanoAdapter(db) { + this.db = db; + this.all = __bind(this.all, this); + + this.fromDB = __bind(this.fromDB, this); + + this.forDB = __bind(this.forDB, this); + + this.destroyAll = __bind(this.destroyAll, this); + + this.count = __bind(this.count, this); + + this.updateAttributes = __bind(this.updateAttributes, this); + + this.destroy = __bind(this.destroy, this); + + this.find = __bind(this.find, this); + + this.exists = __bind(this.exists, this); + + this.updateOrCreate = __bind(this.updateOrCreate, this); + + this.save = __bind(this.save, this); + + this.create = __bind(this.create, this); + + this.define = __bind(this.define, this); + + this._models = {}; + } + + NanoAdapter.prototype.define = function(descr) { + var m; + m = descr.model.modelName; + descr.properties._rev = { + type: String + }; + return this._models[m] = descr; + }; + + NanoAdapter.prototype.create = function() { + var args; + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return this.save.apply(this, args); + }; + + NanoAdapter.prototype.save = function(model, data, callback) { + var _this = this; + data.model = model; + helpers.savePrep(data); + return this.db.insert(this.forDB(model, data), function(err, doc) { + return callback(err, doc.id, doc.rev); + }); + }; + + NanoAdapter.prototype.updateOrCreate = function(model, data, callback) { + var _this = this; + return this.exists(model, data.id, function(err, exists) { + if (exists) { + return _this.save(model, data, callback); + } else { + return _this.create(model, data, function(err, id) { + data.id = id; + return callback(err, data); + }); + } + }); + }; + + NanoAdapter.prototype.exists = function(model, id, callback) { + return this.db.head(id, function(err, _, headers) { + if (err) { + return callback(null, false); + } + return callback(null, headers != null); + }); + }; + + NanoAdapter.prototype.find = function(model, id, callback) { + var _this = this; + return this.db.get(id, function(err, doc) { + return callback(err, _this.fromDB(model, doc)); + }); + }; + + NanoAdapter.prototype.destroy = function(model, id, callback) { + var _this = this; + return this.db.get(id, function(err, doc) { + if (err) { + return callback(err); + } + return _this.db.destroy(id, doc._rev, function(err, doc) { + if (err) { + return callback(err); + } + callback.removed = true; + return callback(); + }); + }); + }; + + NanoAdapter.prototype.updateAttributes = function(model, id, data, callback) { + var _this = this; + return this.db.get(id, function(err, base) { + if (err) { + return callback(err); + } + return _this.save(model, helpers.merge(base, data), callback); + }); + }; + + NanoAdapter.prototype.count = function(model, callback, where) { + var _this = this; + return this.all(model, { + where: where + }, function(err, docs) { + return callback(err, docs.length); + }); + }; + + NanoAdapter.prototype.destroyAll = function(model, callback) { + var _this = this; + return this.all(model, {}, function(err, docs) { + var doc; + docs = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = docs.length; _i < _len; _i++) { + doc = docs[_i]; + _results.push({ + _id: doc.id, + _rev: doc._rev, + _deleted: true + }); + } + return _results; + })(); + return _this.db.bulk({ + docs: docs + }, function(err, body) { + return callback(err, body); + }); + }); + }; + + NanoAdapter.prototype.forDB = function(model, data) { + var k, props, v; + if (data == null) { + data = {}; + } + props = this._models[model].properties; + for (k in props) { + v = props[k]; + if (data[k] && props[k].type.name === 'Date' && (data[k].getTime != null)) { + data[k] = data[k].getTime(); + } + } + return data; + }; + + NanoAdapter.prototype.fromDB = function(model, data) { + var date, k, props, v; + if (!data) { + return data; + } + props = this._models[model].properties; + for (k in props) { + v = props[k]; + if ((data[k] != null) && props[k].type.name === 'Date') { + date = new Date(data[k]); + date.setTime(data[k]); + data[k] = date; + } + } + return data; + }; + + NanoAdapter.prototype.all = function(model, filter, callback) { + var params, + _this = this; + params = { + keys: [model], + include_docs: true + }; + return this.db.view('nano', 'by_model', params, function(err, body) { + var doc, docs, i, k, key, orders, row, sorting, v, where, _i, _len; + docs = (function() { + var _i, _len, _ref, _results; + _ref = body.rows; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + row = _ref[_i]; + row.doc.id = row.doc._id; + delete row.doc._id; + _results.push(row.doc); + } + return _results; + })(); + if (where = filter != null ? filter.where : void 0) { + for (k in where) { + v = where[k]; + if (_.isDate(v)) { + where[k] = v.getTime(); + } + } + docs = _.where(docs, where); + } + if (orders = filter != null ? filter.order : void 0) { + if (_.isString(orders)) { + orders = [orders]; + } + sorting = function(a, b) { + var ak, bk, i, item, rev, _i, _len; + for (i = _i = 0, _len = this.length; _i < _len; i = ++_i) { + item = this[i]; + ak = a[this[i].key]; + bk = b[this[i].key]; + rev = this[i].reverse; + if (ak > bk) { + return 1 * rev; + } + if (ak < bk) { + return -1 * rev; + } + } + return 0; + }; + for (i = _i = 0, _len = orders.length; _i < _len; i = ++_i) { + key = orders[i]; + orders[i] = { + reverse: helpers.reverse(key), + key: helpers.stripOrder(key) + }; + } + docs.sort(sorting.bind(orders)); + } + return callback(err, (function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = docs.length; _j < _len1; _j++) { + doc = docs[_j]; + _results.push(this.fromDB(model, doc)); + } + return _results; + }).call(_this)); + }); + }; + + return NanoAdapter; + + })(); + + helpers = { + merge: function(base, update) { + var k, v; + if (!base) { + return update; + } + for (k in update) { + v = update[k]; + base[k] = update[k]; + } + return base; + }, + reverse: function(key) { + var hasOrder; + if (hasOrder = key.match(/\s+(A|DE)SC$/i)) { + if (hasOrder[1] === "DE") { + return -1; + } + } + return 1; + }, + stripOrder: function(key) { + return key.replace(/\s+(A|DE)SC/i, ""); + }, + savePrep: function(data) { + var id; + if (id = data.id) { + delete data.id; + data._id = id.toString(); + } + if (data._rev === null) { + return delete data._rev; + } + } + }; + +}).call(this); diff --git a/package.json b/package.json index ffa7967b..b675fba5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jugglingdb", - "description": "ORM for every database: redis, mysql, neo4j, mongodb, postgres, sqlite", + "description": "ORM for every database: redis, mysql, neo4j, mongodb, couchdb, postgres, sqlite", "version": "0.1.27-3", "author": "Anatoliy Chakkaev ", "contributors": [ @@ -39,6 +39,10 @@ { "name": "Rick O'Toole", "email": "patrick.n.otoole@gmail.com" + }, + { + "name": "Nicholas Westlake", + "email": "nicholasredlin@gmail.com" } ], "repository": { @@ -53,7 +57,8 @@ "node >= 0.4.12" ], "dependencies": { - "node-uuid": ">= 1.3.3" + "node-uuid": ">= 1.3.3", + "lodash": "1.x.x" }, "devDependencies": { "semicov": "*", @@ -69,6 +74,7 @@ "neo4j": ">= 0.2.5", "mongodb": ">= 0.9.9", "felix-couchdb": ">= 1.0.3", - "cradle": ">= 0.6.3" + "cradle": ">= 0.6.3", + "nano": "3.3.x" } } diff --git a/src/adapters/nano.coffee b/src/adapters/nano.coffee new file mode 100644 index 00000000..5fbc31d6 --- /dev/null +++ b/src/adapters/nano.coffee @@ -0,0 +1,141 @@ +{_} = require 'lodash' + +# api +exports.initialize = (schema, callback) -> + throw new Error 'url is missing' unless opts = schema.settings + db = require('nano')(opts) + + schema.adapter = new NanoAdapter db + design = views: by_model: map: + 'function (doc) { if (doc.model) return emit(doc.model, null); }' + db.insert design, '_design/nano', (err, doc) -> callback() + +class NanoAdapter + constructor: (@db) -> + @_models = {} + + define: (descr) => + m = descr.model.modelName + descr.properties._rev = type: String + @_models[m] = descr + + create: (args...) => @save args... + + save: (model, data, callback) => + data.model = model + helpers.savePrep data + + @db.insert @forDB(model, data), (err, doc) => + callback err, doc.id, doc.rev + + updateOrCreate: (model, data, callback) => + @exists model, data.id, (err, exists) => + if exists + @save model, data, callback + else + @create model, data, (err, id) -> + data.id = id + callback err, data + + exists: (model, id, callback) => + @db.head id, (err, _, headers) -> + return callback null, no if err + callback null, headers? + + find: (model, id, callback) => + @db.get id, (err, doc) => + callback err, @fromDB(model, doc) + + destroy: (model, id, callback) => + @db.get id, (err, doc) => + return callback err if err + @db.destroy id, doc._rev, (err, doc) => + return callback err if err + callback.removed = yes + callback() + + updateAttributes: (model, id, data, callback) => + @db.get id, (err, base) => + return callback err if err + @save model, helpers.merge(base, data), callback + + count: (model, callback, where) => + @all model, {where}, (err, docs) => + callback err, docs.length + + destroyAll: (model, callback) => + @all model, {}, (err, docs) => + docs = for doc in docs + {_id: doc.id, _rev: doc._rev, _deleted: yes} + @db.bulk {docs}, (err, body) => + callback err, body + + forDB: (model, data = {}) => + props = @_models[model].properties + for k, v of props + if data[k] and props[k].type.name is 'Date' and data[k].getTime? + data[k] = data[k].getTime() + data + + fromDB: (model, data) => + return data unless data + props = @_models[model].properties + for k, v of props + if data[k]? and props[k].type.name is 'Date' + date = new Date data[k] + date.setTime data[k] + data[k] = date + data + + all: (model, filter, callback) => + params = + keys: [model] + include_docs: yes + + @db.view 'nano', 'by_model', params, (err, body) => + docs = for row in body.rows + row.doc.id = row.doc._id + delete row.doc._id + row.doc + + if where = filter?.where + for k, v of where + where[k] = v.getTime() if _.isDate v + docs = _.where docs, where + + if orders = filter?.order + orders = [orders] if _.isString orders + + sorting = (a, b) -> + for item, i in @ + ak = a[@[i].key]; bk = b[@[i].key]; rev = @[i].reverse + if ak > bk then return 1 * rev + if ak < bk then return -1 * rev + 0 + + for key, i in orders + orders[i] = + reverse: helpers.reverse key + key: helpers.stripOrder key + + docs.sort sorting.bind orders + callback err, (@fromDB model, doc for doc in docs) + +# helpers +helpers = + merge: (base, update) -> + return update unless base + base[k] = update[k] for k, v of update + base + reverse: (key) -> + if hasOrder = key.match(/\s+(A|DE)SC$/i) + return -1 if hasOrder[1] is "DE" + 1 + stripOrder: (key) -> + key.replace(/\s+(A|DE)SC/i, "") + savePrep: (data) -> + if id = data.id + delete data.id + data._id = id.toString() + if data._rev is null + delete data._rev diff --git a/test/common_test.js b/test/common_test.js index 1dd955f9..c26e7a6d 100644 --- a/test/common_test.js +++ b/test/common_test.js @@ -21,7 +21,8 @@ var schemas = { mongodb: { url: 'mongodb://travis:test@localhost:27017/myapp' }, redis2: {}, memory: {}, - cradle: {} + cradle: {}, + nano: { url: 'http://localhost:5984/nano-test' } }; var specificTest = getSpecificTests(); @@ -165,9 +166,12 @@ function testOrm(schema) { test.done(); }); - it('should be expoted to JSON', function (test) { - test.equal(JSON.stringify(new Post({id: 1, title: 'hello, json', date: 1})), - '{"title":"hello, json","subject":null,"content":null,"date":1,"published":false,"likes":[],"related":[],"id":1,"userId":null}'); + it('should be exported to JSON', function (test) { + var outString = '{"title":"hello, json","subject":null,"content":null,"date":1,"published":false,"likes":[],"related":[],"id":1,"userId":null}' + if (schema.name === 'nano') + outString = '{"title":"hello, json","subject":null,"content":null,"date":1,"published":false,"likes":[],"related":[],"_rev":null,"id":1,"userId":null}' + + test.equal(JSON.stringify(new Post({id: 1, title: 'hello, json', date: 1})),outString); test.done(); }); @@ -455,6 +459,7 @@ function testOrm(schema) { schema.name !== 'memory' && schema.name !== 'neo4j' && schema.name !== 'cradle' && + schema.name !== 'nano' && schema.name !== 'mongodb' ) it('hasMany should support additional conditions', function (test) { @@ -483,7 +488,8 @@ function testOrm(schema) { !schema.name.match(/redis/) && schema.name !== 'memory' && schema.name !== 'neo4j' && - schema.name !== 'cradle' + schema.name !== 'cradle' && + schema.name !== 'nano' ) it('hasMany should be cached', function (test) { // Finding one post with an existing author associated @@ -758,7 +764,8 @@ function testOrm(schema) { !schema.name.match(/redis/) && schema.name !== 'memory' && schema.name !== 'neo4j' && - schema.name !== 'cradle' + schema.name !== 'cradle' && + schema.name !== 'nano' ) it('should allow advanced queying: lt, gt, lte, gte, between', function (test) { Post.destroyAll(function () { @@ -991,7 +998,8 @@ function testOrm(schema) { !schema.name.match(/redis/) && schema.name !== 'memory' && schema.name !== 'neo4j' && - schema.name !== 'cradle' + schema.name !== 'cradle' && + schema.name !== 'nano' ) it('belongsTo should be cached', function (test) { User.findOne(function(err, user) { diff --git a/test/performance.coffee b/test/performance.coffee index 0119b8ec..89de6477 100644 --- a/test/performance.coffee +++ b/test/performance.coffee @@ -11,6 +11,8 @@ schemas = redis: {} memory: {} cradle: {} + nano: + url: 'http://localhost:5984/nano-test' testOrm = (schema) ->