From ece00ceaaa00cbb955810fb2271de6f14e6bdb07 Mon Sep 17 00:00:00 2001 From: Anatoliy Chakkaev Date: Sun, 16 Oct 2011 21:21:08 +0400 Subject: [PATCH] Neo4j features --- lib/adapters/neo4j.js | 136 ++++++++++++++++++++++++++++++++++++++++-- test/common_test.js | 30 +++++++++- 2 files changed, 159 insertions(+), 7 deletions(-) diff --git a/lib/adapters/neo4j.js b/lib/adapters/neo4j.js index 7dd26e04..6893c44f 100644 --- a/lib/adapters/neo4j.js +++ b/lib/adapters/neo4j.js @@ -1,7 +1,7 @@ /** * Module dependencies */ -var neo4j = require('neo4j'); +var neo4j = require('./neo4j-lib'); exports.initialize = function initializeSchema(schema, callback) { schema.client = new neo4j.GraphDatabase(schema.settings.url); @@ -15,9 +15,107 @@ function Neo4j(client) { } 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; }; +Neo4j.prototype.createIndexHelper = function (class, indexName) { + var db = this.client; + var method = 'findBy' + indexName[0].toUpperCase() + indexName.substr(1); + class[method] = function (value, cb) { + db.getIndexedNode(class.modelName, indexName, value, function (err, node) { + if (err) return cb(err); + if (node) { + node.data.id = node.id; + cb(null, new class(node.data)); + } else { + cb(null, null); + } + }); + }; +}; + +Neo4j.prototype.mixClassMethods = function mixClassMethods(class, properties) { + var neo = this; + + Object.keys(properties).forEach(function (name) { + if (properties[name].index) { + neo.createIndexHelper(class, name); + } + }); + + /** + * @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) + */ + class.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) return cb(err); + var found = false; + if (rels && rels.forEach) { + rels.forEach(function (r) { + if (r.start.id === from && r.end.id === to) { + found = true; + } + }); + } + cb(err, found); + }); + }); + }; + + class.createRelationshipTo = function createRelationshipTo(id1, id2, type, data, cb) { + var fromNode, toNode; + neo.node(id1, function (err, node) { + if (err) return cb(err); + fromNode = node; + ok(); + }); + neo.node(id2, function (err, node) { + if (err) return cb(err); + toNode = node; + ok(); + }); + function ok() { + if (fromNode && toNode) { + fromNode.createRelationshipTo(toNode, type, cleanup(data), cb); + } + } + }; + + class.createRelationshipFrom = function createRelationshipFrom(id1, id2, type, data, cb) { + class.createRelationshipTo(id2, id1, type, data, cb); + } + + // only create relationship if it is not exists + class.ensureRelationshipTo = function (id1, id2, type, data, cb) { + class.relationshipExists(id1, id2, type, 'outgoing', function (err, exists) { + if (err) return cb(err); + if (exists) return cb(null); + class.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) + */ + proto.isInRelationWith = function isInRelationWith(obj, type, direction, cb) { + this.constructor.relationshipExists(this.id, obj.id || obj, type, 'all', cb); + }; +}; + Neo4j.prototype.node = function find(id, callback) { if (this.cache[id]) { callback(null, this.cache[id]); @@ -35,15 +133,40 @@ Neo4j.prototype.create = function create(model, data, callback) { node.data = cleanup(data); node.save(function (err) { if (err) { - return callback && callback(err); + return callback(err); } this.cache[node.id] = node; node.index(model, 'id', node.id, function (err) { - callback && callback(err, node.id); - }); + if (err) return callback(err); + this.updateIndexes(model, node, function (err) { + if (err) return callback(err); + callback(null, node.id); + }); + }.bind(this)); }.bind(this)); }; +Neo4j.prototype.updateIndexes = function updateIndexes(model, node, cb) { + var props = this._models[model].properties; + var wait = 1; + Object.keys(props).forEach(function (key) { + if (props[key].index) { + wait += 1; + node.index(model, key, node.data[key], done); + } + }); + + done(); + + var error = false; + function done(err) { + error = error || err; + if (--wait === 0) { + cb(error); + } + } +}; + Neo4j.prototype.save = function save(model, data, callback) { this.node(data.id, function (err, node) { if (err) return callback(err); @@ -82,12 +205,13 @@ Neo4j.prototype.readFromDb = function readFromDb(model, data) { }; Neo4j.prototype.destroy = function destroy(model, id, callback) { + var force = true; this.node(id, function (err, node) { if (err) return callback(err); node.delete(function (err) { if (err) return callback(err); delete this.cache[id]; - }.bind(this), true); + }.bind(this), force); }); }; @@ -153,7 +277,7 @@ Neo4j.prototype.updateAttributes = function updateAttributes(model, id, data, cb }; function cleanup(data) { - if (!data) return console.log('no data!') && {}; + if (!data) return null; var res = {}; Object.keys(data).forEach(function (key) { var v = data[key]; diff --git a/test/common_test.js b/test/common_test.js index 6c046843..36212c44 100644 --- a/test/common_test.js +++ b/test/common_test.js @@ -17,11 +17,14 @@ var schemas = { memory: {} }; +var specificTest = getSpecificTests(); + Object.keys(schemas).forEach(function (schemaName) { if (process.env.ONLY && process.env.ONLY !== schemaName) return; context(schemaName, function () { var schema = new Schema(schemaName, schemas[schemaName]); testOrm(schema); + if (specificTest[schemaName]) specificTest[schemaName](schema); }); }); @@ -41,7 +44,7 @@ function testOrm(schema) { }); Post = schema.define('Post', { - title: { type: String, length: 255 }, + title: { type: String, length: 255, index: true }, content: { type: Text }, date: { type: Date, default: Date.now }, published: { type: Boolean, default: false } @@ -342,3 +345,28 @@ function testOrm(schema) { } } + +function getSpecificTests() { + var sp = {}; + + sp['neo4j'] = function (schema) { + + it('should create methods for searching by index', function (test) { + var Post = schema.models['Post']; + test.ok(typeof Post.findByTitle === 'function'); + Post.create({title: 'Catcher in the rye'}, function (err, post) { + if (err) return console.log(err); + test.ok(!post.isNewRecord()); + Post.findByTitle('Catcher in the rye', function (err, foundPost) { + if (err) return console.log(err); + if (foundPost) { + test.equal(post.id, foundPost.id); + test.done(); + } + }); + }); + }); + }; + + return sp; +}