caminte
Version:
ORM for every database: redis, mysql, neo4j, mongodb, rethinkdb, postgres, sqlite, tingodb
807 lines (733 loc) • 24.5 kB
JavaScript
/**
* Module dependencies
*/
var utils = require('../utils');
var safeRequire = utils.safeRequire;
var redis = safeRequire('redis');
var utils = require('../utils');
var helpers = utils.helpers;
exports.initialize = function initializeSchema(schema, callback) {
if (!redis) {
return;
}
if (schema.settings.socket) {
schema.client = redis.createClient(
schema.settings.socket,
schema.settings.options
);
} else if (schema.settings.url) {
var url = require('url');
var redisUrl = url.parse(schema.settings.url);
var redisAuth = (redisUrl.auth || '').split(':');
schema.settings.host = redisUrl.hostname;
schema.settings.port = redisUrl.port;
if (redisAuth.length === 2) {
schema.settings.db = redisAuth[0];
schema.settings.password = redisAuth[1];
}
}
if (!schema.client) {
schema.client = redis.createClient(
schema.settings.port,
schema.settings.host,
schema.settings.options
);
if (schema.settings.password) {
schema.client.auth(schema.settings.password);
}
}
var callbackCalled = false;
var database = schema.settings.hasOwnProperty('database') && schema.settings.database;
schema.client.on('connect', function () {
if (!callbackCalled && database === false) {
callbackCalled = true;
callback();
} else if (database !== false) {
if (callbackCalled) {
return schema.client.select(schema.settings.database);
} else {
callbackCalled = true;
return schema.client.select(schema.settings.database, callback);
}
}
});
var clientWrapper = new Client(schema.client);
schema.adapter = new BridgeToRedis(schema.settings, clientWrapper);
clientWrapper._adapter = schema.adapter;
};
function Client(client) {
this._client = client;
}
var commands = Object.keys(redis.Multi.prototype).filter(function (n) {
return n.match(/^[a-z]/);
});
commands.forEach(function (cmd) {
Client.prototype[cmd] = function (args, callback) {
var c = this._client, log;
if (typeof args === 'string') {
args = [args];
}
if (!args) {
args = [];
}
var lstr = cmd.toUpperCase() + ' ' + args.map(function (a) {
if (typeof a === 'object') {
return JSON.stringify(a);
}
return a;
}).join(' ');
args.push(function (err, replies) {
if (err) {
console.log(err);
}
callback && callback(err, replies);
});
c[cmd].apply(c, args);
};
});
Client.prototype.multi = function (commands, callback) {
if (commands.length === 0)
return callback && callback();
if (commands.length === 1) {
return this[commands[0].shift().toLowerCase()].call(
this,
commands[0],
callback && function (e, r) {
callback(e, [r]);
});
}
var lstr = 'MULTI\n ' + commands.map(function (x) {
return x.join(' ');
}).join('\n ') + '\nEXEC';
this._client.multi(commands).exec(function (err, replies) {
if (err) {
console.log(err);
}
callback && callback(err, replies);
});
};
Client.prototype.transaction = function () {
return new Transaction(this);
};
function Transaction(client) {
this._client = client;
this._handlers = [];
this._schedule = [];
}
Transaction.prototype.run = function (cb) {
var t = this;
var atLeastOneHandler = false;
switch (this._schedule.length) {
case 0:
return cb();
case 1:
return this._client[this._schedule[0].shift()].call(
this._client,
this._schedule[0],
this._handlers[0] || cb);
default:
this._client.multi(this._schedule, function (err, replies) {
if (err)
return cb(err);
replies.forEach(function (r, i) {
if (t._handlers[i]) {
atLeastOneHandler = true;
t._handlers[i](err, r);
}
});
if (!atLeastOneHandler)
cb(err);
});
}
};
commands.forEach(function (k) {
Transaction.prototype[k] = function (args, cb) {
if (typeof args === 'string') {
args = [args];
}
args.unshift(k);
this._schedule.push(args);
this._handlers.push(cb || false);
};
});
function BridgeToRedis(s, client) {
this.name = 'redis';
this._models = {};
this.client = client;
this.indexes = {};
this.settings = s;
}
BridgeToRedis.prototype.define = function (descr) {
var self = this;
var m = descr.model.modelName;
self._models[m] = descr;
self.indexes[m] = {
id: Number
};
Object.keys(descr.properties).forEach(function (prop) {
if (descr.properties[prop].index) {
self.indexes[m][prop] = descr.properties[prop].type;
} else if (prop === 'id') {
self.indexes[m][prop] = descr.properties[prop].type;
}
}.bind(this));
};
BridgeToRedis.prototype.defineForeignKey = function (model, key, cb) {
this.indexes[model][key] = Number;
cb(null, Number);
};
BridgeToRedis.prototype.forDatabase = function (model, data) {
var p = this._models[model].properties;
for (var i in data) {
if (!p[i]) {
continue;
}
if (typeof data[i] === 'undefined' || data[i] === null) {
if (p[i].default || p[i].default === 0) {
if (typeof p[i].default === 'function') {
data[i] = p[i].default();
} else {
data[i] = p[i].default;
}
} else {
data[i] = "";
continue;
}
}
switch ((p[i].type.name || '').toString().toLowerCase()) {
case "date":
if (data[i].getTime) {
data[i] = data[i].getTime().toString();
} else if (parseInt(data[i]) > 0) {
data[i] = data[i].toString();
} else {
data[i] = '0';
}
break;
case "number":
data[i] = data[i].toString();
break;
case "boolean":
data[i] = !!data[i] ? "1" : "0";
break;
case "json":
if (typeof data[i] === 'object') {
data[i] = JSON.stringify(data[i]);
}
break;
default:
data[i] = data[i].toString();
}
}
return data;
};
BridgeToRedis.prototype.fromDatabase = function (model, data) {
var p = this._models[model].properties, d;
for (var i in data) {
if (!p[i]) {
continue;
}
var type = (p[i].type.name || '').toString().toLowerCase();
if (typeof data[i] === 'undefined' || data[i] === null) {
if (p[i].default || p[i].default === 0) {
if (typeof p[i].default === 'function') {
data[i] = p[i].default();
} else {
data[i] = p[i].default;
}
} else {
data[i] = "";
continue;
}
}
switch (type) {
case "json":
try {
if (typeof data[i] === 'string') {
data[i] = JSON.parse(data[i]);
}
} catch (err) {
}
break;
case "date":
d = new Date(data[i]);
d.setTime(data[i]);
data[i] = d;
break;
case "number":
data[i] = Number(data[i]);
break;
case "boolean":
data[i] = data[i] === "1";
break;
}
}
return data;
};
BridgeToRedis.prototype.save = function (model, data, callback) {
var self = this;
data = self.forDatabase(model, data);
deleteNulls(data);
self.client.hgetall(model + ':' + data.id, function (err, prevData) {
if (err) {
return callback(err);
}
self.client.hmset([model + ':' + data.id, self.forDatabase(model, data)], function (err) {
if (err) {
return callback(err);
}
if (prevData) {
Object.keys(prevData).forEach(function (k) {
if (data.hasOwnProperty(k)) {
return;
}
data[k] = prevData[k];
});
}
self.updateIndexes(model, data.id, data, callback, self.forDatabase(model, prevData));
}.bind(this));
}.bind(this));
};
BridgeToRedis.prototype.updateIndexes = function (model, id, data, callback, prevData) {
var p = this._models[model].properties;
var i = this.indexes[model];
var schedule = [];
if (!callback.removed) {
schedule.push(['SADD', 's:' + model, id]);
}
Object.keys(i).forEach(function (key) {
if (data.hasOwnProperty(key)) {
var val = data[key];
schedule.push([
'SADD',
'i:' + model + ':' + key + ':' + val,
id
]);
}
if (prevData && prevData[key] !== data[key]) {
schedule.push([
'SREM',
'i:' + model + ':' + key + ':' + prevData[key],
id
]);
}
});
if (schedule.length) {
this.client.multi(schedule, function (err) {
callback(err, data);
});
} else {
callback(null);
}
};
BridgeToRedis.prototype.create = function (model, data, callback) {
if (data.id) {
return create.call(this, data.id, true);
}
this.client.incr('id:' + model, function (err, id) {
create.call(this, id);
}.bind(this));
function create(id, upsert) {
data.id = id.toString();
this.save(model, data, function (err) {
if (callback) {
callback(err, parseInt(id, 10));
}
});
// push the id to the list of user ids for sorting
this.client.sadd(['s:' + model, data.id]);
}
};
BridgeToRedis.prototype.update = function (model, filter, data, callback) {
if ('function' === typeof filter) {
callback = filter;
filter = {};
}
if (!filter) {
filter = {};
}
if (!filter.where) {
filter = {where: filter};
}
var self = this;
self.all(model, filter, function (err, found) {
if (!found || !found.length) {
return callback && callback(err);
}
var dlen = found.length;
found.forEach(function (doc) {
doc = helpers.merge(doc, data);
self.save(model, doc, function (error) {
if (--dlen === 0) {
return callback && callback(error, 0);
}
});
});
});
};
BridgeToRedis.prototype.exists = function (model, id, callback) {
this.client.exists(model + ':' + id, function (err, exists) {
if (callback) {
callback(err, exists ? true : false);
}
});
};
BridgeToRedis.prototype.findById = function findById(model, id, callback) {
var self = this;
self.client.hgetall(model + ':' + id, function (err, data) {
if (data && Object.keys(data).length > 0) {
data.id = id;
} else {
data = null;
}
data = self.fromDatabase(model, data);
callback(err, data);
}.bind(this));
};
BridgeToRedis.prototype.destroy = function destroy(model, id, callback) {
var br = this;
var trans = br.client.transaction();
br.client.hgetall(model + ':' + id, function (err, data) {
if (err) {
return callback(err);
}
trans.srem(['s:' + model, id]);
trans.del(model + ':' + id);
trans.run(function (err) {
if (err) {
return callback(err);
}
callback.removed = true;
br.updateIndexes(model, id, {}, callback, data);
});
});
};
BridgeToRedis.prototype.possibleIndexes = function (model, filter, callback) {
if (!filter || Object.keys(filter.where || {}).length === 0) {
// no index needed
callback([[], [], [], true]);
return false;
/*
filter.where = {
id: {
gt: 0
}
};
*/
}
var self = this;
var dest = 'where:' + (Date.now() * Math.random());
var props = self._models[model].properties;
var compIndex = {};
var foundIndex = [];
var noIndex = [];
Object.keys(filter.where).forEach(function (key) {
var i = self.indexes[model][key];
if (i && typeof i !== 'undefined') {
var val = filter.where[key];
if (val && typeof val === 'object' && !val.getTime) {
var cin = 'i:' + model + ':' + key + ':';
if (!compIndex[key]) {
compIndex[key] = {
conds: []
};
}
if (i.name === 'Date') {
Object.keys(val).forEach(function (cndkey) {
val[cndkey] = val[cndkey] && val[cndkey].getTime ? val[cndkey].getTime() : 0;
});
}
compIndex[key].rkey = cin + '*';
compIndex[key].fkey = cin;
compIndex[key].type = props[key].type.name;
compIndex[key].conds.push(val);
} else {
if (i.name === 'Date') {
val = val && val.getTime ? val.getTime() : 0;
}
foundIndex.push('i:' + model + ':' + key + ':' + val);
}
} else {
noIndex.push(key);
}
}.bind(this));
if (Object.keys(compIndex || {}).length > 0) {
var multi = self.client._client.multi();
for (var ik in compIndex) {
multi.keys(compIndex[ik].rkey);
}
multi.exec(function (err, mkeys) {
if (err) {
console.log(err);
}
var condIndex = [];
for (var ic in compIndex) {
var kregex = new RegExp('^' + compIndex[ic].fkey + '(.*)');
if (mkeys) {
for (var i in mkeys) {
var keys = mkeys[i];
if (keys.length) {
keys.forEach(function (key) {
if (kregex.test(key)) {
var fkval = RegExp.$1;
if (compIndex[ic].type === 'Number' || compIndex[ic].type === 'Date') {
fkval = parseInt(fkval);
}
if (helpers.parseCond(fkval, compIndex[ic].conds[0])) {
condIndex.push(key);
}
}
}.bind(this));
}
}
}
}
condIndex.unshift(dest);
self.client._client.sunionstore(condIndex, function (err, replies) {
if (replies > 0) {
foundIndex.push(dest);
}
callback([foundIndex, noIndex, [dest]]);
});
}.bind(this));
} else {
callback([foundIndex, noIndex, [dest]]);
}
};
BridgeToRedis.prototype.all = BridgeToRedis.prototype.find = function all(model, filter, callback) {
if ('function' === typeof filter) {
callback = filter;
filter = {};
}
if (!filter) {
filter = {};
}
var self = this;
var dest = 'temp:' + (Date.now() * Math.random());
if (!filter) {
filter = {order: 'id'};
}
// WHERE
if (!filter.where || Object.keys(filter.where).length === 0) {
dest = 's:' + model;
// no filtering, just sort/limit (if any)
// but we need where to be an object for possibleIndexes
filter.where = {};
}
self.possibleIndexes(model, filter, function (pi) {
var client = self.client;
var cmd;
var sortCmd = [];
var props = self._models[model].properties;
var allNumeric = true;
var trans = self.client.transaction();
var indexes = pi[0];
var noIndexes = pi[1];
if (noIndexes.length) {
throw new Error(model + ': no indexes found for ' +
noIndexes.join(', ') +
' impossible to sort and filter using redis adapter');
}
// indexes needed
if (pi.length < 4) {
if (indexes && indexes.length > 0) {
indexes.unshift(dest);
trans.sinterstore(indexes);
} else {
return callback && callback(null, []);
}
}
// only counting?
if (filter.getCount) {
trans.scard(dest, callback);
return trans.run();
}
// ORDER
var reverse = false;
if (!filter.order) {
filter.order = 'id';
}
var orders = filter.order;
if (typeof filter.order === "string") {
orders = [filter.order];
}
orders.forEach(function (key) {
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;
}
if (props[key].type.name !== 'Number' && props[key].type.name !== 'Date') {
allNumeric = false;
}
sortCmd.push("BY", model + ":*->" + key);
});
// LIMIT
if (filter.limit) {
var offset = (filter.offset || filter.skip || 0), limit = filter.limit;
sortCmd.push("LIMIT", offset, limit);
}
// we need ALPHA modifier when sorting string values
// the only case it's not required - we sort numbers
if (!allNumeric) {
sortCmd.push('ALPHA');
}
if (reverse) {
sortCmd.push('DESC');
}
sortCmd.unshift(dest);
sortCmd.push("GET", "#");
cmd = "SORT " + sortCmd.join(" ");
trans.sort(sortCmd, function (err, ids) {
if (err) {
return callback(err, []);
}
var sortedKeys = ids.map(function (i) {
return model + ":" + i;
});
handleKeys(err, sortedKeys);
}.bind(this));
if (dest.match(/^temp/)) {
trans.del(dest);
}
if (indexes && indexes.length > 0) {
indexes.forEach(function (idx) {
if (idx.match(/^where/)) {
trans.del(idx);
}
}.bind(this));
}
function handleKeys(err, keys) {
if (err) {
console.log(err);
}
var query = keys.map(function (key) {
return ['hgetall', key];
});
client.multi(query, function (err, replies) {
callback(err, (replies || []).map(function (r) {
return self.fromDatabase(model, r);
}));
}.bind(this));
}
function numerically(a, b) {
return a[this[0]] - b[this[0]];
}
function literally(a, b) {
return a[this[0]] > b[this[0]];
}
return trans.run(function(err, data){
return callback && callback(err, data);
});
});
};
BridgeToRedis.prototype.remove = function remove(model, filter, callback) {
var self = this;
var dest = 'temp:' + (Date.now() * Math.random());
self.possibleIndexes(model, filter, function (pi) {
var indexes = pi[0];
var noIndexes = pi[1];
var trans = self.client._client.multi();
if (noIndexes.length) {
throw new Error(model + ': no indexes found for ' +
noIndexes.join(', ') +
' impossible to sort and filter using redis adapter');
}
if (indexes && indexes.length > 0) {
if (indexes.length === 1) {
indexes.unshift(dest);
trans.sunionstore(indexes);
trans.smembers(dest);
} else {
indexes.unshift(dest);
trans.sinterstore(indexes);
}
} else {
callback(null, null);
}
if (dest.match(/^temp/)) {
trans.del(dest);
}
if (indexes && indexes.length > 0) {
indexes.forEach(function (idx) {
if (idx.match(/^where/)) {
trans.del(idx);
}
}.bind(this));
}
trans.exec(function (err, result) {
if (err) {
console.log(err);
}
var found = result[1] || [];
var query = found.map(function (key) {
return ['hgetall', (model + ':' + key)];
});
if (found && found.length > 0) {
self.client.multi(query, function (err, replies) {
var schedule = [];
if (replies && replies.length > 0) {
replies.forEach(function (reply) {
if (reply) {
schedule.push([
'DEL',
model + ':' + reply.id
]);
schedule.push([
'SREM',
's:' + model,
reply.id
]);
Object.keys(reply).forEach(function (replyKey) {
schedule.push([
'SREM',
'i:' + model + ':' + replyKey + ':' + reply[replyKey],
reply.id
]);
}.bind(this));
}
}.bind(this));
self.client.multi(schedule, callback);
} else {
callback(null);
}
}.bind(this));
} else {
callback(null);
}
});
});
};
BridgeToRedis.prototype.destroyAll = function destroyAll(model, callback) {
var br = this;
br.client.multi([
['KEYS', model + ':*'],
['KEYS', '*:' + model + ':*']
], function (err, k) {
br.client.del(k[0].concat(k[1]).concat('s:' + model), callback);
});
};
BridgeToRedis.prototype.count = function count(model, callback, where) {
if (where && Object.keys(where).length) {
this.all(model, {where: where, getCount: true}, callback);
} else {
this.client.scard('s:' + model, callback);
}
};
BridgeToRedis.prototype.updateAttributes = function updateAttrs(model, id, data, callback) {
var self = this;
data.id = id;
self.findById(model, id, function (err, prevData) {
self.save(model, data, callback, prevData);
});
};
function deleteNulls(data) {
Object.keys(data).forEach(function (key) {
if (data[key] === null)
delete data[key];
});
}
BridgeToRedis.prototype.disconnect = function disconnect() {
this.client.quit();
};