caminte
Version:
ORM for every database: redis, mysql, neo4j, mongodb, rethinkdb, postgres, sqlite, tingodb
588 lines (537 loc) • 18.2 kB
JavaScript
/**
* Module dependencies
*/
var utils = require('../utils');
var safeRequire = utils.safeRequire;
var neo4j = safeRequire('node-neo4j');
exports.initialize = function initializeSchema(schema, callback) {
// 'http://username:password@localhost:7474'
if (!schema.settings.url) {
var auth = '';
var url = schema.settings.host || 'localhost';
var port = schema.settings.port || 7474;
url += ':' + port;
if (schema.settings.username) {
auth = schema.settings.username;
if (schema.settings.password) {
auth += ':' + schema.settings.password;
}
}
if (auth) {
url = auth + '@' + url;
}
/*
if (schema.settings.database) {
url += '/' + schema.settings.database;
} else {
url += '/';
}
*/
url = 'http://' + url;
schema.settings.url = url;
}
var client = new neo4j(schema.settings.url);
schema.adapter = new Neo4j(schema.settings, client);
schema.adapter.client = client;
process.nextTick(callback);
};
function Neo4j(s, client) {
this.name = 'neo4j';
this._models = {};
this.client = client;
this.cache = {};
this.settings = s;
}
Neo4j.prototype.define = function defineModel(descr) {
// this.mixClassMethods(descr.model, descr.properties);
// this.mixInstanceMethods(descr.model.prototype, descr.properties);
this._models[descr.model.modelName] = descr;
};
/**
* Update existing database collections.
* @param {Function} callback
*/
Neo4j.prototype.autoupdate = function (callback) {
return callback && callback();
};
Neo4j.prototype.createIndexHelper = function (cls, indexName) {
var db = this.client;
var method = 'findBy' + indexName[0].toUpperCase() + indexName.substr(1);
cls[method] = function (value, cb) {
db.getIndexedNode(cls.modelName, indexName, value, function (err, node) {
if (err)
return cb(err);
if (node) {
node.data.id = node.id;
cb(null, new cls(node.data));
} else {
cb(null, null);
}
});
};
};
Neo4j.prototype.mixClassMethods = function mixClassMethods(cls, properties) {
var neo = this;
Object.keys(properties).forEach(function (name) {
if (properties[name].index) {
neo.createIndexHelper(cls, name);
}
});
cls.setupCypherQuery = function (name, queryStr, rowHandler) {
cls[name] = function cypherQuery(params, cb) {
if (typeof params === 'function') {
cb = params;
params = [];
} else if (params.constructor.name !== 'Array') {
params = [params];
}
var i = 0;
var q = queryStr.replace(/\?/g, function () {
return params[i++];
});
neo.client.query(function (err, result) {
if (err)
return cb(err, []);
cb(null, result.map(rowHandler));
}, q);
};
};
/**
* @param from - id of object to check relation from
* @param to - id of object to check relation to
* @param type - type of relation
* @param direction - all | incoming | outgoing
* @param cb - callback (err, rel || false)
*/
cls.relationshipExists = function relationshipExists(from, to, type, direction, cb) {
neo.node(from, function (err, node) {
if (err)
return cb(err);
node._getRelationships(direction, type, function (err, rels) {
if (err && cb) {
return cb(err);
}
if (err && !cb) {
throw err;
}
var found = false;
if (rels && rels.forEach) {
rels.forEach(function (r) {
if (r.start.id === from && r.end.id === to) {
found = true;
}
});
}
cb && cb(err, found);
});
});
};
cls.createRelationshipTo = function createRelationshipTo(id1, id2, type, data, cb) {
var fromNode, toNode;
neo.node(id1, function (err, node) {
if (err && cb)
return cb(err);
if (err && !cb)
throw err;
fromNode = node;
ok();
});
neo.node(id2, function (err, node) {
if (err && cb)
return cb(err);
if (err && !cb)
throw err;
toNode = node;
ok();
});
function ok() {
if (fromNode && toNode) {
fromNode.createRelationshipTo(toNode, type, cleanup(data), cb);
}
}
};
cls.createRelationshipFrom = function createRelationshipFrom(id1, id2, type, data, cb) {
cls.createRelationshipTo(id2, id1, type, data, cb);
};
// only create relationship if it is not exists
cls.ensureRelationshipTo = function (id1, id2, type, data, cb) {
cls.relationshipExists(id1, id2, type, 'outgoing', function (err, exists) {
if (err && cb)
return cb(err);
if (err && !cb)
throw err;
if (exists)
return cb && cb(null);
cls.createRelationshipTo(id1, id2, type, data, cb);
});
};
};
Neo4j.prototype.mixInstanceMethods = function mixInstanceMethods(proto) {
var neo = this;
/**
* @param obj - Object or id of object to check relation with
* @param type - type of relation
* @param cb - callback (err, rel || false)
* @param {mixed} direction
*/
proto.isInRelationWith = function isInRelationWith(obj, type, direction, cb) {
this.constructor.relationshipExists(this.id, obj.id || obj, type, 'all', cb);
};
};
Neo4j.prototype.findById = function findById(model, id, callback) {
var self = this;
self.client.readNode(id, function (err, node) {
if (!node) {
return callback && callback(err, null);
}
var id = node._id;
delete node._id;
node = self.fromDatabase(model, node);
node.id = id;
return callback && callback(err, node);
}.bind(self));
};
Neo4j.prototype.create = function create(model, data, callback) {
var cdata = {};
cdata.nodeType = model;
var self = this, props = self._models[model].properties;
Object.keys(data).forEach(function (key) {
if (!props[key]) return;
cdata[key] = self.toDatabase(props[key], data[key]);
});
self.client.insertNode(cdata, model, function (err, node) {
if (err) {
return callback(err);
}
callback(err, node._id);
});
};
Neo4j.prototype.updateIndexes = function updateIndexes(model, node, callback) {
var props = this._models[model].properties;
var wait = 1;
Object.keys(props).forEach(function (key) {
if (props[key].index && node.data[key]) {
wait += 1;
node.index(model, key, node.data[key], done);
}
});
done();
var error = false;
function done(err) {
error = error || err;
if (--wait === 0) {
callback(error);
}
}
};
Neo4j.prototype.save = function save(model, data, callback) {
var self = this, id = data.id;
self.updateAttributes(model, id, data, function (err, updatedNode) {
return callback && callback(err, updatedNode);
}.bind(self));
};
Neo4j.prototype.exists = function exists(model, id, callback) {
this.findById(model, id, function (err, data) {
return callback(err, !err && data)
}.bind(this));
};
Neo4j.prototype.destroy = function destroy(model, id, callback) {
this.client.deleteNode(id, function (err, node) {
callback(err, node);
});
};
Neo4j.prototype.all = function all(model, filter, callback) {
if ('function' === typeof filter) {
callback = filter;
filter = {};
}
if (!filter) {
filter = {};
}
var self = this, query = ['MATCH (data:' + model + ')'];
query.push('WHERE data.nodeType = \'' + model + '\'');
if (filter) {
if (filter.where) {
var sql = self.buildWhere(filter.where, self, model);
query.push(sql);
}
}
query.push('RETURN data');
if (filter.order) {
var order = 'data.' + filter.order;
if (typeof order === 'string') {
order = [order];
}
query.push('ORDER BY ', order.join(', '));
}
if (filter.limit) {
if (filter.skip) {
query.push('SKIP ' + (filter.skip || 0));
}
query.push('LIMIT ' + filter.limit);
}
self.client.cypherQuery(query.join(' '), function (err, res) {
var data = (res || {}).data || [];
data = data.map(function (obj) {
var cleared = self.fromDatabase(model, obj);
cleared.id = obj._id;
return cleared;
});
return callback && callback(err, data);
}.bind(self));
};
Neo4j.prototype.destroyAll = function destroyAll(model, callback) {
var query = 'MATCH (data:' + model + ') ' +
'WHERE data.nodeType = \'' + model + '\' ' +
'DELETE data RETURN count(data)';
this.client.cypherQuery(query, function (err, res) {
callback(err, res);
}.bind(this));
};
Neo4j.prototype.count = function count(model, callback, filter) {
var self = this, query = ['MATCH (data:' + model + ')'];
query.push('WHERE data.nodeType = \'' + model + '\'');
if (filter) {
if (filter.where) {
var sql = self.buildWhere(filter.where, self, model);
query.push(sql);
}
}
query.push('RETURN count(data) AS count');
self.client.cypherQuery(query.join(' '), function (err, res) {
var count = 0;
if (res && res.data) {
count = res.data[0] || 0;
}
return callback && callback(err, count);
}.bind(self));
};
Neo4j.prototype.updateAttributes = function updateAttributes(model, id, data, callback) {
var self = this, props = self._models[model].properties;
self.findById(model, id, function (err, node) {
Object.keys(data).forEach(function (key) {
data[key] = self.toDatabase(props[key], data[key]);
});
var merged = merge(node, data);
merged.id = id;
merged.nodeType = model;
self.client.updateNode(id, merged, function (err, updated) {
return callback && callback(err, updated);
});
}.bind(self));
};
/**
* Update rows
* @param {String} model
* @param {Object} filter
* @param {Object} data
* @param {Function} callback
*/
Neo4j.prototype.update = function (model, filter, data, callback) {
if ('function' === typeof filter) {
return filter(new Error("Get parametrs undefined"), null);
}
if ('function' === typeof data) {
return data(new Error("Set parametrs undefined"), null);
}
var self = this, cdata = {}, props = self._models[model].properties;
filter = filter.where ? filter.where : filter;
Object.keys(data).forEach(function (key) {
cdata[key] = self.toDatabase(props[key], data[key]);
});
self.client.updateNodesWithLabelsAndProperties(model, filter, cdata, [], false, function (err, updatedNodes) {
return callback && callback(err, updatedNodes);
});
};
Neo4j.prototype.toDatabase = function (prop, val, esc) {
"use strict";
if (val === null) {
return '';
}
if (!prop) {
return val;
}
var type = (prop.type.name || '').toString().toLowerCase();
if (type === 'number') {
return val;
}
if (type === 'date') {
if (!val) {
return 0;
}
if (typeof val === 'string') {
val = Date.parse(val);
}
if (val instanceof Date) {
val = val.getTime();
}
return val;
}
if (type === "boolean") {
return val ? 1 : 0;
}
if (type === "json") {
if (typeof val === 'object') {
val = JSON.stringify(val);
}
}
return esc ? '\'' + val.toString() + '\'' : val.toString();
};
Neo4j.prototype.fromDatabase = function (model, data) {
if (!data) {
return null;
}
var clean = {};
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
var val = data[key];
if (!props[key]) {
return;
}
var type = (props[key].type.name || '').toString().toLowerCase();
if (type === 'date' && val !== null) {
if (val !== '') {
clean[key] = new Date(val);
} else {
clean[key] = '';
}
} else if (type === 'json') {
if (typeof val === 'string') {
try {
clean[key] = JSON.parse(val);
} catch (err) {
clean[key] = val;
}
} else {
clean[key] = val;
}
} else {
clean[key] = val;
}
});
return clean;
};
Neo4j.prototype.buildWhere = function buildWhere(conds, adapter, model) {
'use strict';
var cs = [], or = [],
self = adapter,
props = self._models[model].properties;
Object.keys(conds).forEach(function (key) {
if (key !== 'or') {
cs = parseCond(cs, key, props, conds, self);
} else {
conds[key].forEach(function (oconds) {
Object.keys(oconds).forEach(function (okey) {
or = parseCond(or, okey, props, oconds, self);
});
});
}
});
if (cs.length === 0 && or.length === 0) {
return '';
}
var orop = "";
if (or.length) {
orop = ' (' + or.join(' OR ') + ') ';
}
orop += (orop !== "" && cs.length > 0) ? ' AND ' : '';
return 'AND ' + orop + cs.join(' AND ');
};
var parseCond = function (cs, key, props, conds, self) {
'use strict';
var keyEscaped = 'data.' + key;
var val = conds[key];
if (val === null) {
cs.push(keyEscaped + '\'\'');
} else if (val.constructor.name === 'Object') {
Object.keys(val).forEach(function (condType) {
val = self.toDatabase(props[key], val[condType], true);
var sqlCond = keyEscaped;
if ((condType === 'inq' || condType === 'nin') && val.length === 0) {
cs.push(condType === 'inq' ? 0 : 1);
return true;
}
switch (condType.toString().toLowerCase()) {
case 'gt':
sqlCond += ' > ';
break;
case 'gte':
sqlCond += ' >= ';
break;
case 'lt':
sqlCond += ' < ';
break;
case 'lte':
sqlCond += ' <= ';
break;
case 'between':
sqlCond += '';
break;
case 'inq':
case 'in':
sqlCond += ' IN ';
break;
case 'nin':
sqlCond = ' NOT ( ' + keyEscaped + ' IN [' + val + '])';
break;
case 'neq':
case 'ne':
sqlCond = ' NOT ( ' + keyEscaped + ' = ' + val + ' )';
break;
case 'regex':
sqlCond += ' REGEXP ';
break;
case 'like':
sqlCond += ' LIKE ';
break;
case 'nlike':
sqlCond += ' NOT LIKE ';
break;
default:
sqlCond += ' ' + condType + ' ';
break;
}
if (condType === 'between') {
sqlCond = val[0] + ' <= ' + keyEscaped + ' <= ' + val[1];
} else if (condType === 'in' || condType === 'inq') {
sqlCond += '[' + val + ']';
} else if (condType === 'neq' || condType === 'ne' || condType === 'nin') {
} else {
sqlCond += val;
}
cs.push(sqlCond);
});
} else if (/^\//gi.test(conds[key])) {
var reg = val.toString().split('/');
cs.push(keyEscaped + ' REGEXP "' + reg[1] + '"');
} else {
val = self.toDatabase(props[key], val, true);
cs.push(keyEscaped + ' = ' + val);
}
return cs;
};
var cleanup = function (data) {
if (!data) {
return null;
}
var res = {};
Object.keys(data).forEach(function (key) {
var v = data[key];
if (v === null) {
// skip
// console.log('skip null', key);
} else if (v && v.constructor.name === 'Array' && v.length === 0) {
// skip
// console.log('skip blank array', key);
} else if (typeof v !== 'undefined') {
res[key] = v;
}
});
return res;
};
var merge = function (base, update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
});
return base;
};