From d333fe1d306a6ea2904df8854c3c173280505ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Thieriot?= Date: Sun, 10 Jun 2012 13:41:07 +0200 Subject: [PATCH] First version of a cradle adapter --- lib/adapters/cradle.js | 319 ++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- test/common_test.js | 5 +- test/performance.coffee | 1 + 4 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 lib/adapters/cradle.js diff --git a/lib/adapters/cradle.js b/lib/adapters/cradle.js new file mode 100644 index 00000000..d0e6e2a5 --- /dev/null +++ b/lib/adapters/cradle.js @@ -0,0 +1,319 @@ +var safeRequire = require('../utils').safeRequire; + +/** + * Module dependencies + */ +var cradle = safeRequire('cradle'); + +/** + * Private functions for internal use + */ +function CradleAdapter(client) { + this._models = {}; + this.client = client; +} + +function createdbif(client, callback) { + client.exists(function (err, exists) { + if(err) callback(err); + if (!exists) { client.create(function() { callback(); }); } + else { callback(); } + }); +} + +function naturalize(data, model) { + data.nature = model; + //TODO: maybe this is not a really good idea + if(data.date) data.date = data.date.toString(); + return data; +} +function idealize(data) { + data.id = data._id; + return data; +} +function stringify(data) { + return data ? data.toString() : data +} + +function errorHandler(callback, func) { + return function(err, res) { + if(err) { + callback(err); + } else { + if(func) { + func(res, function(res) { + callback(null, res); + }); + } else { + callback(null, res); + } + } + } +}; + +function synchronize(functions, args, callback) { + if(functions.length === 0) callback(); + if(functions.length > 0 && args.length === functions.length) { + functions[0](args[0][0], args[0][1], function(err, res) { + if(err) callback(err); + functions.splice(0, 1); + args.splice(0, 1); + synchronize(functions, args, callback); + }); + } +}; + +function applyFilter(filter) { + if (typeof filter.where === 'function') { + return filter.where; + } + var keys = Object.keys(filter.where); + return function (obj) { + var pass = true; + keys.forEach(function (key) { + if (!test(filter.where[key], obj[key])) { + pass = false; + } + }); + return pass; + } + + function test(example, value) { + if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { + return value.match(example); + } + // not strict equality + return example == value; + } +} + +function numerically(a, b) { + return a[this[0]] - b[this[0]]; +} + +function literally(a, b) { + return a[this[0]] > b[this[0]]; +} + +function filtering(res, model, filter, instance) { + + if(model) { + if(filter == null) filter = {}; + if(filter.where == null) filter.where = {}; + filter.where.nature = model; + } + // do we need some filtration? + if (filter.where) { + res = res ? res.filter(applyFilter(filter)) : res; + } + + // do we need some sorting? + if (filter.order) { + var props = instance[model].properties; + var allNumeric = true; + var orders = filter.order; + var reverse = false; + if (typeof filter.order === "string") { + orders = [filter.order]; + } + + orders.forEach(function (key, i) { + var m = key.match(/\s+(A|DE)SC$/i); + if (m) { + key = key.replace(/\s+(A|DE)SC/i, ''); + if (m[1] === 'DE') reverse = true; + } + orders[i] = key; + if (props[key].type.name !== 'Number') { + allNumeric = false; + } + }); + if (allNumeric) { + res = res.sort(numerically.bind(orders)); + } else { + res = res.sort(literally.bind(orders)); + } + if (reverse) res = res.reverse(); + } + return res; +} + +/** + * Connection/Disconnection + */ +exports.initialize = function(schema, callback) { + if (!cradle) return; + + if (!schema.settings.url) { + var host = schema.settings.host || 'localhost'; + var port = schema.settings.port || '5984'; + var options = schema.settings.options || { + cache: true, + raw: false + }; + if (schema.settings.username) { + options.auth = {}; + options.auth.username = schema.settings.username; + if (schema.settings.password) { + options.auth.password = schema.settings.password; + } + } + var database = schema.settings.database || 'jugglingdb'; + + schema.settings.host = host; + schema.settings.port = port; + schema.settings.database = database; + schema.settings.options = options; + } + schema.client = new(cradle.Connection)(schema.settings.host, schema.settings.port,schema.settings.options).database(schema.settings.database); + + createdbif( + schema.client, + errorHandler(callback, function() { + schema.adapter = new CradleAdapter(schema.client); + process.nextTick(callback); + })); +}; + +CradleAdapter.prototype.disconnect = function() { +}; + +/** + * Write methods + */ +CradleAdapter.prototype.define = function(descr) { + this._models[descr.model.modelName] = descr; +}; + +CradleAdapter.prototype.create = function(model, data, callback) { + this.client.save( + stringify(data.id), + naturalize(data, model), + errorHandler(callback, function(res, cb) { + cb(res.id); + }) + ); +}; + +CradleAdapter.prototype.save = function(model, data, callback) { + this.client.save( + stringify(data.id), + naturalize(data, model), + errorHandler(callback) + ) +}; + +CradleAdapter.prototype.updateAttributes = function(model, id, data, callback) { + this.client.merge( + stringify(id), + data, + errorHandler(callback, function(doc, cb) { + cb(idealize(doc)); + }) + ); +}; + +CradleAdapter.prototype.updateOrCreate = function(model, data, callback) { + this.client.get( + stringify(data.id), + function (err, doc) { + if(err) { + this.create(model, data, callback); + } else { + this.updateAttributes(mode, data.id, data, callback); + } + }.bind(this) + ) +}; + +/** + * Read methods + */ +CradleAdapter.prototype.exists = function(model, id, callback) { + this.client.get( + stringify(id), + errorHandler(callback, function(doc, cb) { + cb(!!doc); + }) + ); +}; + +CradleAdapter.prototype.find = function(model, id, callback) { + this.client.get( + stringify(id), + errorHandler(callback, function(doc, cb) { + cb(idealize(doc)); + }) + ); +}; + +CradleAdapter.prototype.count = function(model, callback, where) { + this.models( + model, + {where: where}, + callback, + function(docs, cb) { + cb(docs.length); + } + ); +}; + +CradleAdapter.prototype.models = function(model, filter, callback, func) { + this.client.all( + {include_docs: true}, + errorHandler(callback, function(res, cb) { + var docs = res.map(function(doc) { + return idealize(doc); + }); + var filtered = filtering(docs, model, filter, this._models) + + func ? func(filtered, cb) : cb(filtered); + }.bind(this)) + ); +}; + +CradleAdapter.prototype.all = function(model, filter, callback) { + this.models( + model, + filter, + callback + ); +}; + +/** + * Detroy methods + */ +CradleAdapter.prototype.destroy = function(model, id, callback) { + this.client.remove( + stringify(id), + function (err, doc) { + callback(err); + } + ); +}; + +CradleAdapter.prototype.destroyAll = function(model, callback) { + this.models( + model, + null, + callback, + function(docs, cb) { + var docIds = docs.map(function(doc) { + return doc.id; + }); + this.client.get(docIds, function(err, res) { + if(err) cb(err); + + var funcs = res.map(function(doc) { + return this.client.remove.bind(this.client); + }.bind(this)); + + var args = res.map(function(doc) { + return [doc._id, doc._rev]; + }); + + synchronize(funcs, args, cb); + }.bind(this)); + }.bind(this) + ); +}; diff --git a/package.json b/package.json index 19f3b6eb..54a9b116 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "riak-js": ">= 0.4.1", "neo4j": ">= 0.2.5", "mongodb": ">= 0.9.9", - "felix-couchdb": ">= 1.0.3" + "felix-couchdb": ">= 1.0.3", + "cradle": ">= 0.6.3" } } diff --git a/test/common_test.js b/test/common_test.js index 149066eb..8b303af9 100644 --- a/test/common_test.js +++ b/test/common_test.js @@ -20,7 +20,8 @@ var schemas = { mongoose: { url: 'mongodb://travis:test@localhost:27017/myapp' }, mongodb: { url: 'mongodb://travis:test@localhost:27017/myapp' }, redis: {}, - memory: {} + memory: {}, + cradle: {} }; var specificTest = getSpecificTests(); @@ -579,7 +580,7 @@ function testOrm(schema) { }); - if (schema.name !== 'redis' && schema.name !== 'memory' && schema.name !== 'neo4j') + if (schema.name !== 'redis' && schema.name !== 'memory' && schema.name !== 'neo4j' && schema.name !== 'cradle') it('should allow advanced queying: lt, gt, lte, gte, between', function (test) { Post.destroyAll(function () { Post.create({date: new Date('Wed, 01 Feb 2012 13:56:12 GMT')}, done); diff --git a/test/performance.coffee b/test/performance.coffee index 1770edd6..0119b8ec 100644 --- a/test/performance.coffee +++ b/test/performance.coffee @@ -10,6 +10,7 @@ schemas = url: 'mongodb://localhost/test' redis: {} memory: {} + cradle: {} testOrm = (schema) ->