caminte
Version:
ORM for every database: redis, mysql, neo4j, mongodb, rethinkdb, postgres, sqlite, tingodb
690 lines (631 loc) • 24.2 kB
JavaScript
/**
* Module dependencies
*/
var utils = require('../utils');
var safeRequire = utils.safeRequire;
var r = safeRequire('rethinkdb');
var url = require('url');
var fs = require('fs');
var moment = require('moment');
var gpool = require('generic-pool');
var async = require('async');
exports.initialize = function initializeSchema(schema, callback) {
if (!r) {
return;
}
var s = schema.settings;
if (schema.settings.rs) {
s.rs = schema.settings.rs;
if (schema.settings.url) {
var uris = schema.settings.url.split(',');
s.hosts = [];
s.ports = [];
uris.forEach(function (uri) {
var purl = url.parse(uri);
s.hosts.push(purl.hostname || 'localhost');
s.ports.push(parseInt(purl.port || '28015', 10));
if (!s.database)
s.database = purl.pathname.replace(/^\//, '');
if (!s.username)
s.username = purl.auth && purl.auth.split(':')[0];
if (!s.password)
s.password = purl.auth && purl.auth.split(':')[1];
});
}
s.database = s.database || 'test';
} else {
if (schema.settings.url) {
var purl = url.parse(schema.settings.url);
s.host = purl.hostname;
s.port = purl.port;
s.database = purl.pathname.replace(/^\//, '');
s.username = purl.auth && purl.auth.split(':')[0];
s.password = purl.auth && purl.auth.split(':')[1];
}
s.host = s.host || 'localhost';
s.port = parseInt(s.port || '28015', 10);
s.database = s.database || 'test';
}
s.safe = s.safe || false;
function connect(cb) {
r.connect({host: s.host, port: s.port, db: s.database}, function (error, client) {
if (error) {
return cb(error, null);
}
r.db(s.database).tableList().run(client, function (error) {
if (error && /database(.*)does\s+not\s+exist/i.test(error.message)) {
r.dbCreate(s.database).run(client, function (error) {
client.use(s.database);
cb(null, client);
});
} else {
client.use(s.database);
cb(null, client);
}
});
});
}
schema.adapter = new RethinkDB(s, schema);
schema.adapter.pool = gpool.Pool({
name: "caminte-rethink-pool",
create: connect,
destroy: function (client) {
client.close();
},
max: s.poolMax || 10,
min: s.poolMin || 1,
idleTimeoutMillis: 30000,
log: function (what, level) {
if (level === "error") {
fs.appendFile("caminte-rethink-pool.log", what + "\r\n");
}
}
});
process.nextTick(callback);
};
function RethinkDB(s, schema) {
this.name = 'rethink';
this._models = {};
this._foreignKeys = {};
this.collections = {};
this.schema = schema;
this.settings = s;
this.database = s.database;
}
RethinkDB.prototype.connect = function (cb) {
cb(); // connection pooling handles it
};
RethinkDB.prototype.define = function (descr) {
if (!descr.settings)
descr.settings = {};
this._models[descr.model.modelName] = descr;
this._foreignKeys[descr.model.modelName] = [];
};
// creates tables if not exists
RethinkDB.prototype.autoupdate = function (callback) {
var self = this;
r.connect({host: self.settings.host, port: self.settings.port}, function (err, client) {
if (err) {
return callback && callback(err);
}
r.db(self.database).tableList().run(client, function (err, cursor) {
if (!err && cursor) {
cursor.toArray(function (err, list) {
if (err) {
return callback && callback(err);
}
var timeout = 0;
async.eachSeries(Object.keys(self._models), function (model, cb) {
var fields = self._models[model].properties;
if (list.length === 0 || list.indexOf(model) < 0) {
r.db(self.database).tableCreate(model).run(client, function (error) {
if (error) {
return cb(error);
}
timeout = 150;
process.nextTick(function() {
self.ensureIndex(model, fields, {}, cb);
});
});
} else {
process.nextTick(function() {
self.ensureIndex(model, fields, {}, cb);
});
}
}, function (err) {
setTimeout(function() {
client.close(function() {
callback(err);
});
}, timeout);
});
});
} else {
client.close(function() {
callback(err);
});
}
});
});
};
RethinkDB.prototype.ensureIndex = function (model, fields, params, callback) {
var self = this, indexes = [];
var properties = fields || self._models[model].properties;
if (Object.keys(properties).length > 0) {
r.connect({host: self.settings.host, port: self.settings.port}, function (err, client) {
if (err) {
return callback && callback(err);
}
Object.keys(properties).forEach(function (property) {
if ((properties[property].unique || properties[property].index || self._foreignKeys[model].indexOf(property) >= 0)) {
indexes.push(property);
}
});
var len = indexes.length;
if(len === 0) {
return callback && callback();
}
r.db(self.database).table(model).indexList().run(client, function (err, cursor) {
if (err || !cursor) {
return callback && callback(err);
}
cursor.toArray(function (err, list) {
if (err) {
return callback && callback(err);
}
indexes.forEach(function(index){
if(list.indexOf(index) === -1){
r.db(self.database).table(model).indexCreate(index).run(client, function (error) {
if (error) {
return callback && callback(error);
}
if (--len === 0) {
process.nextTick(function() {
client.close(function() {
return callback && callback();
});
});
}
});
} else {
if (--len === 0) {
process.nextTick(function() {
client.close(function() {
return callback && callback();
});
});
}
}
});
});
});
});
} else {
return callback && callback();
}
};
// drops tables and re-creates them
RethinkDB.prototype.automigrate = function (callback) {
this.autoupdate(callback);
};
// checks if database needs to be actualized
RethinkDB.prototype.isActual = function (callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error) {
throw error;
}
r.db(self.database).tableList().run(client, function (error, cursor) {
if (!error) {
if (cursor.next()) {
cursor.toArray(function (error, list) {
if (error) {
self.pool.release(client);
return callback(error);
}
var actual = true;
async.each(Object.keys(self._models), function (model, cb) {
if (!actual) {
return cb();
}
var properties = self._models[model].properties;
if (list.indexOf(model) < 0) {
actual = false;
cb();
} else {
r.db(self.database).table(model).indexList().run(client, function (error, cursor) {
if (error) {
return cb(error);
}
cursor.toArray(function (error, list) {
if (error) {
return cb(error);
}
Object.keys(properties).forEach(function (property) {
if ((properties[property].index || self._foreignKeys[model].indexOf(property) >= 0) && list.indexOf(property) < 0) {
actual = false;
}
});
cb();
});
});
}
}, function (err) {
self.pool.release(client);
callback(err, actual);
});
});
} else if (self._models.length > 0) {
self.pool.release(client);
callback(null, false);
}
} else {
self.pool.release(client);
callback(error);
}
});
});
};
RethinkDB.prototype.defineForeignKey = function (name, key, cb) {
this._foreignKeys[name].push(key);
cb(null, String);
};
RethinkDB.prototype.create = function (model, data, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error)
throw error;
if (data.id === null || data.id === undefined) {
delete data.id;
}
Object.keys(data).forEach(function (key) {
if (data[key] instanceof Date) {
data[key] = moment(data[key]).unix();
}
if (data[key] === undefined) {
data[key] = null;
}
});
r.db(self.database).table(model).insert(data).run(client, function (err, m) {
self.pool.release(client);
err = err || m.first_error && new Error(m.first_error);
if (m.generated_keys) {
data.id = m.generated_keys[0];
}
callback(err, err ? null : data.id);
});
});
};
RethinkDB.prototype.save = function (model, data, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error)
throw error;
Object.keys(data).forEach(function (key) {
if (data[key] instanceof Date)
data[key] = moment(data[key]).unix();
if (data[key] === undefined)
data[key] = null;
});
r.db(self.database).table(model).insert(data, {conflict: 'replace'}).run(client, function (err, notice) {
self.pool.release(client);
err = err || notice.first_error && new Error(notice.first_error);
callback(err, notice);
});
});
};
RethinkDB.prototype.exists = function (model, id, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error) {
throw error;
}
r.db(self.database).table(model).get(id).run(client, function (err, data) {
self.pool.release(client);
callback(err, !!(!err && data));
});
});
};
RethinkDB.prototype.findById = function findById(model, id, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error) {
throw error;
}
r.db(self.database).table(model).get(id).run(client, function (err, data) {
if (data)
Object.keys(data).forEach(function (key) {
if (self._models[model].properties[key]['type']['name'] === "Date")
data[key] = moment.unix(data[key]).toDate();
}.bind(self));
self.pool.release(client);
callback(err, data);
}.bind(self));
});
};
RethinkDB.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error) {
throw error;
}
if (data.id === null || data.id === undefined) {
delete data.id;
}
data.forEach(function (value, key) {
if (value instanceof Date) {
data[key] = moment(value).unix();
}
if (value === undefined) {
data[key] = null;
}
});
r.db(self.database).table(model).insert(data, {conflict: 'replace'}).run(client, function (err, m) {
self.pool.release(client);
err = err || m.first_error && new Error(m.first_error);
callback(err, err ? null : m['generated_keys'][0]);
});
});
};
RethinkDB.prototype.destroy = function destroy(model, id, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error)
throw error;
r.db(self.database).table(model).get(id).delete().run(client, function (error, result) {
self.pool.release(client);
callback(error);
});
});
};
RethinkDB.prototype.remove = function remove(model, filter, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error)
throw error;
if (!filter) {
filter = {};
}
var promise = r.db(self.database).table(model);
if (filter.where) {
promise = _processWhere(self, model, filter.where, promise);
}
if (filter.skip) {
promise = promise.skip(filter.skip);
} else if (filter.offset) {
promise = promise.skip(filter.offset);
}
if (filter.limit) {
promise = promise.limit(filter.limit);
}
_keys = self._models[model].properties;
_model = self._models[model].model;
promise.delete().run(client, function (error, cursor) {
self.pool.release(client);
callback(error);
});
}, 0); // high-priority pooling
};
RethinkDB.prototype.all = function all(model, filter, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error)
throw error;
if (!filter) {
filter = {};
}
var promise = r.db(self.database).table(model);
if (filter.where) {
promise = _processWhere(self, model, filter.where, promise);
}
if (filter.order) {
var keys = filter.order;
if (typeof keys === 'string') {
keys = keys.split(',');
}
keys.forEach(function (key) {
var m = key.match(/\s+(A|DE)SC$/);
key = key.replace(/\s+(A|DE)SC$/, '').trim();
if (m && m[1] === 'DE') {
promise = promise.orderBy(r.desc(key));
} else {
promise = promise.orderBy(r.asc(key));
}
});
} else {
// default sort by id
promise = promise.orderBy(r.asc("id"));
}
if (filter.skip) {
promise = promise.skip(filter.skip);
} else if (filter.offset) {
promise = promise.skip(filter.offset);
}
if (filter.limit) {
promise = promise.limit(filter.limit);
}
_keys = self._models[model].properties;
_model = self._models[model].model;
promise.run(client, function (error, cursor) {
if (error) {
self.pool.release(client);
callback(error, null);
}
cursor.toArray(function (err, data) {
if (err) {
self.pool.release(client);
return callback(err);
}
data.forEach(function (element, index) {
Object.keys(element).forEach(function (key) {
if (!_keys.hasOwnProperty(key))
return;
if (_keys[key]['type']['name'] === "Date")
element[key] = moment.unix(element[key]).toDate();
});
data[index] = element;
});
self.pool.release(client);
if (filter && filter.include && filter.include.length > 0) {
_model.include(data, filter.include, callback);
} else {
callback(null, data);
}
});
});
}, 0); // high-priority pooling
};
RethinkDB.prototype.destroyAll = function destroyAll(model, callback) {
var self = this;
self.pool.acquire(function (error, client) {
if (error)
throw error;
r.db(self.database).table(model).delete().run(client, function (error, result) {
self.pool.release(client);
callback(error, result);
});
});
};
RethinkDB.prototype.count = function count(model, callback, where) {
var self = this;
self.pool.acquire(function (error, client) {
if (error) {
throw error;
}
var promise = r.db(self.database).table(model);
if (where && typeof where === "object") {
promise = _processWhere(self, model, where, promise);
}
promise.count().run(client, function (err, count) {
self.pool.release(client);
callback(err, count);
});
});
};
RethinkDB.prototype.updateAttributes = function updateAttrs(model, id, data, cb) {
var self = this;
self.pool.acquire(function (error, client) {
if (error) {
throw error;
}
data.id = id;
Object.keys(data).forEach(function (key) {
if (data[key] instanceof Date) {
data[key] = moment(data[key]).unix();
}
if (data[key] === undefined) {
data[key] = null;
}
});
r.db(self.database)
.table(model)
.update(data)
.run(client, function (err, object) {
self.pool.release(client);
cb(err, data);
});
});
};
RethinkDB.prototype.update = function (model, filter, data, cb) {
var self = this;
self.pool.acquire(function (error, client) {
if (error) {
throw error;
}
r.db(self.database).table(model)
.filter(filter)
.update(data)
.run(client, function (err, object) {
self.pool.release(client);
cb(err, data);
});
});
};
RethinkDB.prototype.disconnect = function () {
var self = this;
self.pool.drain(function () {
self.pool.destroyAllNow();
});
};
function _processWhere(self, model, where, promise) {
//Transform promise (a rethinkdb query) based on the given where clause.
//Returns the modified promise
var i, m, keys;
var indexed = false;
var queryParts = [];
var queryExtra = [];
Object.keys(where).forEach(function (k) {
var spec, cond = where[k];
var allConds = [];
if (cond && cond.constructor.name === 'Object') {
keys = Object.keys(cond);
for (i = 0, m = keys.length; i < m; i++) {
allConds.push([keys[i], cond[keys[i]]]);
}
}
else {
allConds.push([false, cond]);
}
var hasIndex = self._models[model].properties[k].index || self._foreignKeys[model].indexOf(k) >= 0;
for (i = 0, m = allConds.length; i < m; i++) {
spec = allConds[i][0];
cond = allConds[i][1];
if (cond instanceof Date) {
cond = moment(cond).unix();
}
switch (spec) {
case false:
if (!indexed && hasIndex) {
promise = promise.getAll(cond, {index: k});
indexed = true;
} else {
queryParts.push(r.row(k).eq(cond));
}
break;
case 'between':
queryParts.push(r.row(k).ge(cond[0]).and(r.row(k).le(cond[1])));
break;
case 'in':
case 'inq':
var expr1 = '(function(row) { return ' + JSON.stringify(cond) + '.indexOf(row.' + k + ') >= 0 })';
queryExtra.push(r.js(expr1));
break;
case 'nin':
var expr2 = '(function(row) { return ' + JSON.stringify(cond) + '.indexOf(row.' + k + ') === -1 })';
queryExtra.push(r.js(expr2));
break;
case 'gt':
queryParts.push(r.row(k).gt(cond));
break;
case 'gte':
queryParts.push(r.row(k).ge(cond));
break;
case 'lt':
queryParts.push(r.row(k).lt(cond));
break;
case 'lte':
queryParts.push(r.row(k).le(cond));
break;
case 'ne':
case 'neq':
queryParts.push(r.row(k).ne(cond));
break;
}
}
});
var query;
queryParts.forEach(function (comp) {
if (!query) {
query = comp;
} else {
query = query.and(comp);
}
});
if (query) {
promise = promise.filter(query);
}
queryExtra.forEach(function (comp) {
promise = promise.filter(comp);
});
return promise;
}