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) { console.log('cradle', 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 (dataSource, callback) { if (!cradle) return; // when using cradle if we dont wait for the dataSource to be connected, the models fails to load correctly. dataSource.waitForConnect = true; if (!dataSource.settings.url) { var host = dataSource.settings.host || 'localhost'; var port = dataSource.settings.port || '5984'; var options = dataSource.settings.options || { cache: true, raw: false }; if (dataSource.settings.username) { options.auth = {}; options.auth.username = dataSource.settings.username; if (dataSource.settings.password) { options.auth.password = dataSource.settings.password; } } var database = dataSource.settings.database || 'loopback-datasource-juggler'; dataSource.settings.host = host; dataSource.settings.port = port; dataSource.settings.database = database; dataSource.settings.options = options; } dataSource.client = new (cradle.Connection)(dataSource.settings.host, dataSource.settings.port, dataSource.settings.options).database(dataSource.settings.database); createdbif( dataSource.client, errorHandler(callback, function () { dataSource.connector = new CradleAdapter(dataSource.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(model, 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) { var limit = 200; var skip = 0; if (filter != null) { limit = filter.limit || limit; skip = filter.skip || skip; } var self = this; self.client.save('_design/' + model, { views: { all: { map: 'function(doc) { if (doc.nature == "' + model + '") { emit(doc._id, doc); } }' } } }, function () { self.client.view(model + '/all', {include_docs: true, limit: limit, skip: skip}, 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(self))); }); }; 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) ); };