From 0e0ecfca7adfc13ba0ef4073e99841b521ccede0 Mon Sep 17 00:00:00 2001 From: Craig Edmunds Date: Thu, 26 Mar 2015 21:26:05 +0000 Subject: [PATCH 1/6] Skeleton neo4j adapter --- examples/projects.js | 2 +- lib/adapter.js | 1 + lib/adapters/neo4j.js | 263 ++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 lib/adapters/neo4j.js diff --git a/examples/projects.js b/examples/projects.js index ad99ecf34..84b8163b6 100644 --- a/examples/projects.js +++ b/examples/projects.js @@ -7,7 +7,7 @@ var fortune = require('../lib/fortune'); */ fortune({ - adapter: "mongodb", + adapter: "neo4j", db: 'projects' }) diff --git a/lib/adapter.js b/lib/adapter.js index 4d7b9a873..efb6548a8 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -6,6 +6,7 @@ var adapters = { mongodb: './adapters/mongodb', //'fortune-mongodb', mysql: 'fortune-relational', psql: 'fortune-relational', + neo4j: './adapters/neo4j', postgres: 'fortune-relational', sqlite: 'fortune-relational' }; diff --git a/lib/adapters/neo4j.js b/lib/adapters/neo4j.js new file mode 100644 index 000000000..b9c07a3b1 --- /dev/null +++ b/lib/adapters/neo4j.js @@ -0,0 +1,263 @@ +var RSVP = require('rsvp'); +var _ = require('lodash'); +var moment = require("moment"); +var Promise = RSVP.Promise; +var adapter = {}; +var neo4j = require('neo4j'); + +adapter._init = function (options) { + // var connectionString = options.connectionString; + + // if (!connectionString || !connectionString.length) { + // connectionString = 'mongodb://' + + // (options.username ? options.username + ':' + options.password + '@' : '') + + // options.host + (options.port ? ':' + options.port : '') + '/' + options.db; + // } + + // mongoose.set('debug', options.debug); + + //Setup mongoose instance + this.db = new neo4j.GraphDatabase('http://localhost:7474'); + +}; + +/** + * Store models in an object here. + * + * @api private + */ +adapter._models = {}; + +adapter.schema = function (name, schema, options, schemaCallback) { + options = options || {}; + + console.log("neo4j schema", name, schema, options); + + if (schemaCallback) + schemaCallback(schema); + + return schema; + +}; + +adapter.model = function(name, schema, options) { + + console.log("neo4j model", name, schema, options); + +}; + +adapter.create = function (model, id, resource) { + var _this = this; + + console.log("neo4j create", model, id, resource); + +}; + +adapter.update = function (model, id, update) { + + console.log("neo4j update", model, id, update); + +}; + +adapter.markDeleted = function(model, id){ + console.log("neo4j markDeleted", model, id); +}; + +adapter.delete = function (model, id) { + console.log("neo4j create", model, id); +}; + +/** + * + * @param model {Model} + * @param query {Object} + * @param projection {Object} + * @returns {Promise} + */ +adapter.find = function(model, query, projection){ + console.log("neo4j find", model, query, projection); +}; + +/** + * @param model {Model || String} + * @param query {Object} + * //@param limit {Number} - deprecated as unused + * @param projection {Object} + * @returns {Promise} + */ + +adapter.findMany = function(model, query, projection) { + console.log("neo4j findMany", model, query, projection); +} + +adapter.count = function(model, query, projection) { + console.log("neo4j count", model, query, projection); +} + +adapter.parseQuery = function(model, query){ + console.log("neo4j parseQuery", model, query, projection); +}; + +adapter.awaitConnection = function () { + var _this = this; + console.log("neo4j awaitConnection"); +}; + +/** + * Parse incoming resource. + * + * @api private + * @param {Object} model + * @param {Object} resource + * @return {Object} + */ +adapter._serialize = function (model, resource) { + + console.log("neo4j _serialize", model, resource); + + return { id : "1" }; +}; + +/** + * Return a resource ready to be sent back to client. + * + * @api private + * @param {Object} model + * @param {Object} resource mongoose document + * @return {Object} + */ +adapter._deserialize = function (model, resource) { + var json = {}; + console.log("neo4j _deserialize", model, resource); + + return { id : "1" }; +}; + +/** + * What happens after the DB has been written to, successful or not. + * + * @api private + * @param {Object} model + * @param {Object} resource + * @param {Object} error + * @param {Function} resolve + * @param {Function} reject + * @param {Array} modifiedRefs + */ +adapter._handleWrite = function (model, resource, error, resolve, reject, modifiedRefs) { + console.log("neo4j _handleWrite"); +}; + +/** + * This method is designed to parse update command and return a list of paths that + * will be modified by given update command. + * It was introduced to handle relationship updates it a more neat way when only + * modified paths trigger update of related documents. + * It's NOT guaranteed to return ALL modified paths. Only that are of interest to _updateRelationships method + * @param {Object} update + * @private + */ +adapter._getModifiedRefs = function(update){ + console.log("neo4j _getModifiedRefs"); +}; + +/** + * Update relationships manually. By nature of NoSQL, + * relations don't come for free. Don't try this at home, kids. + * You've been warned! + * + * @api private + * @param {Object} model + * @param {Object} resource + * @param {Array} modifiedRefs + * @return {Promise} + */ +adapter._updateRelationships = function (model, resource, modifiedRefs) { + console.log("neo4j _updateRelationships"); +}; + +/** + * Update one-to-one mapping. + * + * @api private + * @parameter {Object} relatedModel + * @parameter {Object} resource + * @parameter {Object} reference + * @parameter {Object} field + * @return {Promise} + */ + +adapter._updateOneToOne = function(model, relatedModel, resource, reference, field) { + console.log("neo4j _updateOneToOne"); +}; + +/** + * Update one-to-many mapping. + * + * @api private + * @parameter {Object} relatedModel + * @parameter {Object} resource + * @parameter {Object} reference + * @parameter {Object} field + * @return {Promise} + */ +adapter._updateOneToMany = function(model, relatedModel, resource, reference, field) { + console.log("neo4j _updateOneToMany"); +}; + +/** + * Update many-to-one mapping. + * + * @api private + * @parameter {Object} model - model that has many-to-one ref + * @parameter {Object} relatedModel - model with corresponding one-to-many ref + * @parameter {Object} resource - resource currently being updated + * @parameter {Object} reference - this model reference schema + * @parameter {Object} field - related model reference schema + * @return {Promise} + */ +adapter._updateManyToOne = function(model, relatedModel, resource, reference, field) { + console.log("neo4j _updateManyToOne"); +}; + +/** + * Update many-to-many mapping. + * + * @api private + * @parameter {Object} relatedModel + * @parameter {Object} resource + * @parameter {Object} reference + * @parameter {Object} field + * @return {Promise} + */ +adapter._updateManyToMany = function(model, relatedModel, resource, reference, field) { + console.log("neo4j _updateManyToMany"); +}; + +/** + * Remove all associations from a resource. + * + * @api private + * @parameter {Object} model + * @parameter {Object} resource + * @return {Object} + */ +adapter._dissociate = function (model, resource) { + console.log("neo4j _dissociate"); +}; + +/** + * Determine whether we should perform an upsert (ie. pass {upsert : true} to + * Mongoose) if certain keys exist in the schema's resource. + * + * @api private + * @parameter {Object} model + * @parameter {Object} resource + * @parameter {Object} ops + * @return {Object} + */ +adapter._shouldUpsert = function(model, resource, opts) { + console.log("neo4j _shouldUpsert"); +}; + +module.exports = adapter; \ No newline at end of file diff --git a/package.json b/package.json index 3be6a88c7..8cba195d9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "moment": "~2.8.4", "mongoose": "~3.8.21", "nedb": "~0.10.4", + "neo4j": "^1.1.1", "qs": "^2.3.3", "rsvp": "~3.0.6", "sift": "^0.2.3", From 0c79e103327c9b284b76b4bdccf9ab6c083b812b Mon Sep 17 00:00:00 2001 From: Craig Edmunds Date: Thu, 26 Mar 2015 21:54:45 +0000 Subject: [PATCH 2/6] find many working, needs to be connected to DB --- lib/adapters/neo4j.js | 119 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 6 deletions(-) diff --git a/lib/adapters/neo4j.js b/lib/adapters/neo4j.js index b9c07a3b1..ad7b5c45e 100644 --- a/lib/adapters/neo4j.js +++ b/lib/adapters/neo4j.js @@ -4,6 +4,7 @@ var moment = require("moment"); var Promise = RSVP.Promise; var adapter = {}; var neo4j = require('neo4j'); +var mongoose = require('mongoose'); adapter._init = function (options) { // var connectionString = options.connectionString; @@ -27,44 +28,132 @@ adapter._init = function (options) { * @api private */ adapter._models = {}; +_schema = {}; adapter.schema = function (name, schema, options, schemaCallback) { + + console.log("neo4j schema", name, schema, options); + + //TODO : Copied wholemeal from mongoDB - need to think about refactor & re-use options = options || {}; - console.log("neo4j schema", name, schema, options); + var refkeys = []; + var Mixed = mongoose.Schema.Types.Mixed; + var pk = (options.model || {}).pk; + + _.each(schema, function (val, key) { + var obj = {}; + var isArray = _.isArray(val); + var value = isArray ? val[0] : val; + var isObject = _.isPlainObject(value); + var ref = isObject ? value.ref : value; + var inverse = isObject ? value.inverse : undefined; + var pkType = value.type || value.pkType || mongoose.Schema.Types.ObjectId; + var fieldsToIndex = {}; + + // Convert strings to associations + if (typeof ref === 'string') { + var field = _.extend(isObject ? value : {}, { + ref: ref, + inverse: inverse, + type: pkType, + external: !!value.external, + alias: val.alias || null + }); + + schema[key] = isArray ? [field] : field; + + refkeys.push(key); + } + + // Convert native object to schema type Mixed + if (typeof value == 'function' && typeCheck(value) == 'object') { + + if (isObject) { + schema[key].type = Mixed; + } else { + schema[key] = Mixed; + } + } + }); + + if(pk){ + if(_.isFunction(schema[pk])){ + schema[pk] = { type: schema[pk]}; + }else if(!(_.isObject(schema[pk]) && schema[pk].type)){ + throw new Error("Schema PK must either be a type function or an object with a " + + "`type` property"); + } + + _.extend(schema[pk], {index: {unique: true}}); + } + + schema = mongoose.Schema(schema, options); + schema.refkeys = refkeys; + + _.each(refkeys, function(key){ + var index = {}; + index[key] = 1; + + schema.index(index); + }); + + //Set index on deletedAt + schema.index({ + deletedAt: 1 + },{ + sparse: true + }); if (schemaCallback) schemaCallback(schema); + _schema[name] = schema; + return schema; + function typeCheck(fn) { + return Object.prototype.toString.call(new fn('')) + .slice(1, -1).split(' ')[1].toLowerCase(); + } }; adapter.model = function(name, schema, options) { console.log("neo4j model", name, schema, options); - + console.log(_.keys(_schema)); + + // if(schema) { + var model = _.clone(_schema[name]); + // this._models[name] = model; + return _.extend(model, options, { modelName : name}); + // } else { + // return this._models[name]; + // } + // throw new Error("NotImplemented") }; adapter.create = function (model, id, resource) { var _this = this; console.log("neo4j create", model, id, resource); - + throw new Error("NotImplemented") }; adapter.update = function (model, id, update) { console.log("neo4j update", model, id, update); - + throw new Error("NotImplemented") }; adapter.markDeleted = function(model, id){ console.log("neo4j markDeleted", model, id); + throw new Error("NotImplemented") }; adapter.delete = function (model, id) { console.log("neo4j create", model, id); + throw new Error("NotImplemented") }; /** @@ -76,6 +165,7 @@ adapter.delete = function (model, id) { */ adapter.find = function(model, query, projection){ console.log("neo4j find", model, query, projection); + throw new Error("NotImplemented") }; /** @@ -88,19 +178,27 @@ adapter.find = function(model, query, projection){ adapter.findMany = function(model, query, projection) { console.log("neo4j findMany", model, query, projection); + return []; + // throw new Error("NotImplemented") } adapter.count = function(model, query, projection) { console.log("neo4j count", model, query, projection); + throw new Error("NotImplemented") } adapter.parseQuery = function(model, query){ console.log("neo4j parseQuery", model, query, projection); + throw new Error("NotImplemented") }; adapter.awaitConnection = function () { var _this = this; console.log("neo4j awaitConnection"); + + return new Promise(function (resolve, reject) { + resolve(); + }); }; /** @@ -115,7 +213,7 @@ adapter._serialize = function (model, resource) { console.log("neo4j _serialize", model, resource); - return { id : "1" }; + throw new Error("NotImplemented") }; /** @@ -130,7 +228,7 @@ adapter._deserialize = function (model, resource) { var json = {}; console.log("neo4j _deserialize", model, resource); - return { id : "1" }; + throw new Error("NotImplemented") }; /** @@ -146,6 +244,7 @@ adapter._deserialize = function (model, resource) { */ adapter._handleWrite = function (model, resource, error, resolve, reject, modifiedRefs) { console.log("neo4j _handleWrite"); + throw new Error("NotImplemented") }; /** @@ -159,6 +258,7 @@ adapter._handleWrite = function (model, resource, error, resolve, reject, modifi */ adapter._getModifiedRefs = function(update){ console.log("neo4j _getModifiedRefs"); + throw new Error("NotImplemented") }; /** @@ -174,6 +274,7 @@ adapter._getModifiedRefs = function(update){ */ adapter._updateRelationships = function (model, resource, modifiedRefs) { console.log("neo4j _updateRelationships"); + throw new Error("NotImplemented") }; /** @@ -189,6 +290,7 @@ adapter._updateRelationships = function (model, resource, modifiedRefs) { adapter._updateOneToOne = function(model, relatedModel, resource, reference, field) { console.log("neo4j _updateOneToOne"); + throw new Error("NotImplemented") }; /** @@ -203,6 +305,7 @@ adapter._updateOneToOne = function(model, relatedModel, resource, reference, fie */ adapter._updateOneToMany = function(model, relatedModel, resource, reference, field) { console.log("neo4j _updateOneToMany"); + throw new Error("NotImplemented") }; /** @@ -218,6 +321,7 @@ adapter._updateOneToMany = function(model, relatedModel, resource, reference, fi */ adapter._updateManyToOne = function(model, relatedModel, resource, reference, field) { console.log("neo4j _updateManyToOne"); + throw new Error("NotImplemented") }; /** @@ -232,6 +336,7 @@ adapter._updateManyToOne = function(model, relatedModel, resource, reference, fi */ adapter._updateManyToMany = function(model, relatedModel, resource, reference, field) { console.log("neo4j _updateManyToMany"); + throw new Error("NotImplemented") }; /** @@ -244,6 +349,7 @@ adapter._updateManyToMany = function(model, relatedModel, resource, reference, f */ adapter._dissociate = function (model, resource) { console.log("neo4j _dissociate"); + throw new Error("NotImplemented") }; /** @@ -258,6 +364,7 @@ adapter._dissociate = function (model, resource) { */ adapter._shouldUpsert = function(model, resource, opts) { console.log("neo4j _shouldUpsert"); + throw new Error("NotImplemented") }; module.exports = adapter; \ No newline at end of file From 0233082a421ff9e9f4bfde72944bc5bb2ac3445a Mon Sep 17 00:00:00 2001 From: Craig Edmunds Date: Thu, 26 Mar 2015 22:17:34 +0000 Subject: [PATCH 3/6] Adapter is calling the neo driver for the find by id api --- examples/projects.js | 5 ++++- lib/adapters/neo4j.js | 27 ++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/examples/projects.js b/examples/projects.js index 84b8163b6..2ccaf2181 100644 --- a/examples/projects.js +++ b/examples/projects.js @@ -8,7 +8,10 @@ var fortune = require('../lib/fortune'); fortune({ adapter: "neo4j", - db: 'projects' + connectionString : "http://localhost:7474", + // adapter: "mongodb", + db: 'projects', + }) diff --git a/lib/adapters/neo4j.js b/lib/adapters/neo4j.js index ad7b5c45e..f9044396a 100644 --- a/lib/adapters/neo4j.js +++ b/lib/adapters/neo4j.js @@ -20,6 +20,7 @@ adapter._init = function (options) { //Setup mongoose instance this.db = new neo4j.GraphDatabase('http://localhost:7474'); + console.log("Init DB", this.db); }; /** @@ -32,7 +33,7 @@ _schema = {}; adapter.schema = function (name, schema, options, schemaCallback) { - console.log("neo4j schema", name, schema, options); + // console.log("neo4j schema", name, schema, options); //TODO : Copied wholemeal from mongoDB - need to think about refactor & re-use options = options || {}; @@ -120,8 +121,8 @@ adapter.schema = function (name, schema, options, schemaCallback) { adapter.model = function(name, schema, options) { - console.log("neo4j model", name, schema, options); - console.log(_.keys(_schema)); + // console.log("neo4j model", name, schema, options); + // console.log(_.keys(_schema)); // if(schema) { var model = _.clone(_schema[name]); @@ -177,8 +178,24 @@ adapter.find = function(model, query, projection){ */ adapter.findMany = function(model, query, projection) { - console.log("neo4j findMany", model, query, projection); - return []; + console.log("neo4j findMany", query, projection); //model, ); + var that = this; + + if (query.id) { + //Don't know why this is coming into findMany with id - would expect it to go into .find... + + return new Promise(function (resolve, reject) { + + that.db.getNodeById(query.id, function(err, node) { + // resolve([node]); + console.log("getNodeById callback", err, node); + resolve([{ id : query.id }]) + }); + + }); + } + + // return [{ id : query.id }]; // throw new Error("NotImplemented") } From 1ee9c666533064bf4a17d818f4c55046d1d0b204 Mon Sep 17 00:00:00 2001 From: Craig Edmunds Date: Thu, 26 Mar 2015 22:18:00 +0000 Subject: [PATCH 4/6] Added initial test framework - needs refactoring --- test/fortune-mongodb/neo4j.spec.js | 509 +++++++++++++++++++++++++++++ test/neo4j-runner.js | 135 ++++++++ 2 files changed, 644 insertions(+) create mode 100644 test/fortune-mongodb/neo4j.spec.js create mode 100644 test/neo4j-runner.js diff --git a/test/fortune-mongodb/neo4j.spec.js b/test/fortune-mongodb/neo4j.spec.js new file mode 100644 index 000000000..9201e5988 --- /dev/null +++ b/test/fortune-mongodb/neo4j.spec.js @@ -0,0 +1,509 @@ +var should = require('should'); + +var adapter = require('../../lib/adapters/neo4j'); + +var _ = require('lodash'); + +//TODO : I think these tests would be better written as pure unit tests, asserting that the correct call was made to a stub of the +//neo4j adapter... No need for the "runner" then +module.exports = function(options){ + describe('Neo4j adapter', function(){ + var ids; + + beforeEach(function(){ + ids = options.ids; + }); + + xdescribe('Creation', function(){ + it('should be able to create document with provided id', function(done){ + var doc = { + id: '123456789012345678901234' + }; + adapter.create('pet', doc).then(function(){ + var model = adapter.model('pet'); + model.findOne({_id: '123456789012345678901234'}).exec(function(err, doc){ + should.not.exist(err); + should.exist(doc); + done(); + }); + }); + }); + it('should be able to cast provided id to proper type', function(done){ + var doc = { + id: '123456789012345678901234' + }; + adapter.create('person', doc).then(function(){ + var model = adapter.model('person'); + model.findOne({email: '123456789012345678901234'}).exec(function(err, doc){ + should.not.exist(err); + should.exist(doc); + done(); + }); + }); + }); + it("should upsert where the appropriate upsert keys are specified", function(done) { + var doc = { + id: '123456789012345678901234', + upsertTest : "foo" + }; + + var model = adapter.model("person"); + model.schema.upsertKeys = ["upsertTest"]; + + var response = null, + upsertVal = false, + origUpsert = adapter._shouldUpsert; + + adapter._shouldUpsert = function() { + return response = origUpsert.apply(this, arguments); + }; + + adapter.create("person", doc).then(function() { + should.exist(response); + (response.status).should.equal(true); + (response.opts.upsert).should.equal(true); + + model.findOne({email: '123456789012345678901234'}).exec(function(err, doc){ + should.not.exist(err); + should.exist(doc); + + adapter._shouldUpsert = origUpsert; + done(); + }); + }); + }); + it("should not upsert where the appropriate upsert keys are not specified", function(done) { + var doc = { + id: '123456789012345678901234', + upsertTestYYY : "foo" + }; + + var model = adapter.model("person"); + model.schema.upsertKeys = ["upsertTest"]; + + var response = null, + upsertVal = false, + origUpsert = adapter._shouldUpsert; + + adapter._shouldUpsert = function() { + return response = origUpsert.apply(this, arguments); + }; + + adapter.create("person", doc).then(function() { + should.exist(response); + (response.status).should.equal(false); + (response.opts.upsert).should.equal(false); + + model.findOne({email: '123456789012345678901234'}).exec(function(err, doc){ + should.not.exist(err); + should.exist(doc); + + adapter._shouldUpsert = origUpsert; + done(); + }); + }); + }); + }); + + xdescribe('Relationships', function(){ + describe('synchronizing many-to-many', function(){ + it('should keep in sync many-to-many relationship', function(){ + return adapter.update('person', ids.people[0], {$pushAll: {houses: [ids.houses[0]]}}) + .then(function(created){ + (created.links.houses[0].toString()).should.equal(ids.houses[0].toString()); + }).then(function(){ + return adapter.find('house', {id: ids.houses[0]}); + }).then(function(found){ + (found.links.owners[0]).should.equal(ids.people[0]); + }); + }); + it('should sync correctly when many docs have reference', function(){ + var upd = { + $pushAll: { + houses: ids.houses + } + }; + return adapter.update('person', ids.people[0], upd) + + //Prove successful initial association + .then(function(updated){ + (updated.links.houses.length).should.eql(4); + var refHouses = []; + updated.links.houses.forEach(function(id){ + refHouses.push(id.toString()); + }); + return adapter.findMany('house', {owners: ids.people[0]}); + }) + + .then(function(found){ + (found.length).should.equal(4); + //Do some other updates to mix docs in Mongo + return adapter.update('person', ids.people[1], {$push: {houses: ids.houses[0]}}); + }) + + //Kick him out the house + .then(function(){ + return adapter.update('person', ids.people[0], {$pull: {houses: ids.houses[0]}}); + }) + + //Then assert related docs sync + .then(function(pulled){ + //Now there should be only three houses that person[0] owns + (pulled.links.houses.length).should.eql(3); + return adapter.findMany('house', {owners: ids.people[0]}) + }) + .then(function(found){ + (found.length).should.eql(3); + //Assert there's no house[0] in found docs + found.forEach(function(item){ + (item.id.toString()).should.not.equal(ids.houses[0].toString()); + }); + }); + }); + }); + describe('sync path selection', function(){ + it('should have a method to identify changed paths', function(){ + (adapter._getModifiedRefs).should.be.a.Function; + var update = { + refPath: 'some new value', + $push: { + manyRefOne: 'one' + }, + $pull: { + manyRefTwo: 'two' + }, + $addToSet: { + manyRefThree: 'three' + }, + $unset: { + 'nested.ref': 'nested' + } + }; + var modifiedPaths = adapter._getModifiedRefs(update); + (modifiedPaths.indexOf('refPath')).should.not.equal(-1); + (modifiedPaths.indexOf('manyRefOne')).should.not.equal(-1); + (modifiedPaths.indexOf('manyRefTwo')).should.not.equal(-1); + (modifiedPaths.indexOf('manyRefThree')).should.not.equal(-1); + (modifiedPaths.indexOf('nested.ref')).should.not.equal(-1); + }); + it('should not run updates on related documents which binding path were not modified during the update', function(){ + var oto = adapter._updateOneToOne; + var otm = adapter._updateOneToMany; + var mtm = adapter._updateManyToMany; + var mto = adapter._updateManyToOne; + var mockCalled = false; + adapter._updateOneToOne = function(){ + mockCalled = true; + }; + adapter._updateOneToMany = function(){ + mockCalled = true; + }; + adapter._updateManyToMany = function(){ + mockCalled = true; + }; + adapter._updateManyToOne = function(){ + mockCalled = true; + }; + return adapter.update('person', ids.people[0], {$set: {name: 'Filbert'}}).then(function(){ + mockCalled.should.equal(false); + adapter._updateOneToOne = oto; + adapter._updateOneToMany = otm; + adapter._updateManyToMany = mtm; + adapter._updateManyToOne = mto; + }); + }); + it('should update references if ref path was changed', function(){ + var oto = adapter._updateOneToOne; + var mockCalled = false; + adapter._updateOneToOne = function(){ + mockCalled = true; + return oto.apply(null, arguments); + }; + return adapter.update('person', ids.people[0], {$set: {soulmate: ids.people[1]}}).then(function(){ + mockCalled.should.equal(true); + adapter._updateOneToOne = oto; + }); + }); + }); + }); + + describe('Select', function(){ + xdescribe('count', function(){ + it('should provide interface for counting resources', function(){ + return adapter.count('person').then(function(docs){ + should.exist(docs); + docs.should.eql(4); + }); + }); + it("should count results falling under query", function() { + return adapter.count('person', { birthday: { $gte: new Date(1995, 0, 1)}}).then(function(docs){ + should.exist(docs); + docs.should.eql(3); + }); + }); + it("should ignore not valid queries", function() { + return adapter.count('person', 'some').then(function(docs){ + should.exist(docs); + docs.should.eql(4); + }); + }); + }); + + describe('findMany', function(){ + it('should provide interface for selecting fields to return', function(){ + var projection = { + select: ['name'] + }; + return adapter.findMany('person', {}, projection).then(function(docs){ + should.exist(docs); + }); + }); + xit('should select specified fields for a collection', function(){ + var projection = { + select: ['name', 'appearances', 'pets'] + }; + return adapter.findMany('person', {}, projection).then(function(docs){ + (Object.keys(docs[0]).length).should.equal(3); + should.exist(docs[0].name); + should.exist(docs[0].appearances); + should.exist(docs[0].id); + }); + }); + xit('should return all existing fields when no select is specified', function(){ + return adapter.findMany('person').then(function(docs){ + //hooks add their black magic here. + //See what you have in fixtures + what beforeWrite hooks assign in addiction + var keys = Object.keys(docs[0]).length; + (keys).should.equal(9); + }); + }); + xit('should not affect business id selection', function(){ + return adapter.findMany('person', [ids.people[0]], {select: ['name']}).then(function(docs){ + docs[0].id.should.equal(ids.people[0]); + should.not.exist(docs[0].email); + }); + }); + xit('should apply be able to apply defaults for query and projection', function(){ + return adapter.findMany('person'); + }); + xit('should be able to work with numerical limits', function(){ + return adapter.findMany('person', 1).then(function(docs){ + docs.length.should.equal(1); + }); + }); + }); + xdescribe('find', function(){ + beforeEach(function(done){ + adapter.update('person', ids.people[0], {$push: {pets: ids.pets[0]}}) + .then(function(){ + return adapter.update('person', ids.people[0], {$set: {soulmate: ids.people[1]}}) + }) + .then(function(){ + return adapter.update('person', ids.people[0], {$push: {houses: ids.houses[0]}}) + }) + .then(function(){ + done(); + }); + }); + it('should provide interface for selecting fields to return', function(done){ + var projection = { + select: ['name', 'pets', 'soulmate'] + }; + (function(){ + adapter.find('person', {email: ids.people[0]}, projection) + .then(function(docs){ + should.exist(docs); + done(); + }); + }).should.not.throw(); + }); + it('should select specified fields for a single document', function(done){ + var projection = { + select: ['name', 'soulmate', 'pets', 'houses'] + }; + adapter.find('person', ids.people[0], projection) + .then(function(doc){ + (Object.keys(doc).length).should.equal(3); + (Object.keys(doc.links).length).should.equal(3); + should.exist(doc.name); + should.exist(doc.links.pets); + should.exist(doc.links.soulmate); + should.exist(doc.links.houses); + done(); + }); + }); + it('should return all existing fields when no select is specified', function(done){ + adapter.find('person', ids.people[0]) + .then(function(doc){ + //hooks add their black magic here. + //See what you have in fixtures + what beforeWrite hooks assign in addiction + //+ soulmate from before each + (Object.keys(doc).length).should.equal(10); + done(); + }); + }); + it('should not affect business id selection', function(done){ + adapter.find('person', ids.people[0], {select: ['name', 'soulmate', 'pets', 'houses']}) + .then(function(doc){ + (doc.id).should.equal(ids.people[0]); + (doc.links.soulmate).should.equal(ids.people[1]); + (doc.links.houses[0].toString()).should.equal(ids.houses[0]); + (doc.links.pets[0].toString()).should.equal(ids.pets[0]); + should.not.exist(doc.email); + done(); + }); + }); + it('should apply be able to apply defaults for query and projection', function(done){ + (function(){ + adapter.find('person', ids.people[0]); + }).should.not.throw(); + done(); + }); + }); + }); + + xdescribe('Filtering', function(){ + it('should be able to filter date by exact value', function(done){ + adapter.findMany('person', {birthday: '2000-01-01'}) + .then(function(docs){ + (docs.length).should.equal(1); + (docs[0].name).should.equal('Robert'); + done(); + }); + }); + it('should be able to filter date range: exclusive', function(done){ + var query = { + birthday: { + lt: '2000-02-02', + gt: '1990-01-01' + } + }; + adapter.findMany('person', query) + .then(function(docs){ + (docs.length).should.equal(3); + done(); + }); + }); + it('should be able to filter date range: inclusive', function(done){ + var query = { + birthday: { + gte: '1995-01-01', + lte: '2000-01-01' + } + }; + adapter.findMany('person', query) + .then(function(docs){ + (docs.length).should.equal(3); + done(); + }); + }); + it('should be able to filter number range: exclusive', function(done){ + var query = { + appearances: { + gt: 1934, + lt: 4000 + } + }; + adapter.findMany('person', query) + .then(function(docs){ + (docs.length).should.equal(1); + done(); + }); + }); + it('should be able to filter number range: inclusive', function(done){ + var query = { + appearances: { + gte: 1934, + lte: 3457 + } + }; + adapter.findMany('person', query) + .then(function(docs){ + (docs.length).should.equal(2); + done(); + }); + }); + + it("should be tolerant to $in:undefined queries", function(done){ + var query = { '$in': undefined }; + + adapter.findMany("person", query).then(function(){ done(); }); + }); + + it("should be tolerant to $in:null queries", function(done){ + var query = { '$in': null }; + + adapter.findMany("person", query).then(function(){ done(); }); + }); + + it('should be able to run regex query with default options', function(){ + var queryLowercase = { + email: { + regex: 'bert@' + } + }; + var queryUppercase = { + email: { + regex: 'Bert@' + } + }; + return adapter.findMany('person', queryLowercase).then(function(docs){ + docs.length.should.equal(2); + }).then(function(){ + return adapter.findMany('person',queryUppercase).then(function(docs){ + (docs.length).should.equal(0); + }); + }); + }); + it('should be possible to specify custom options', function(done){ + var query = { + name: { + regex: 'WALLY', + options: 'i' + } + }; + adapter.findMany('person', query) + .then(function(docs){ + (docs.length).should.equal(1); + (docs[0].name).should.equal('Wally'); + done(); + }); + }); + it('should treat empty regex as find all', function(done){ + var query = { + email: { + regex: '' + } + }; + adapter.findMany('person', query) + .then(function(docs){ + (docs.length).should.equal(4); + done(); + }); + }); + it('should deeply parse nested $and, $or, or, and queries', function(done){ + var query = { + $or: [{ + or: [{ + $and: [{ + and: [{ + name: { + regex: 'WALLY', + options: 'i' + } + }] + }] + }] + }] + }; + adapter.findMany('person', query) + .then(function(docs){ + docs.length.should.equal(1); + docs[0].name.should.equal('Wally'); + done(); + }); + }); + }); + }); + +}; diff --git a/test/neo4j-runner.js b/test/neo4j-runner.js new file mode 100644 index 000000000..3a2fdb377 --- /dev/null +++ b/test/neo4j-runner.js @@ -0,0 +1,135 @@ +var inflect= require('i')(); +var should = require('should'); +var _ = require('lodash'); +var RSVP = require('rsvp'); +var request = require('supertest'); +var Promise = RSVP.Promise; +var fixtures = require('./fixtures.json'); + +var port = 8891; +var ioPort = 8892; +var baseUrl = 'http://localhost:' + port; + +xdescribe('Fortune neo4j test runner', function(){ + var options = { + app: null, + port: port, + ioPort: ioPort, + baseUrl: baseUrl, + ids: {} + }; + + //TODO : This should be refactored to support testing multiple DBs + + before(function(done){ + var remoteDB = process.env.WERCKER_MONGODB_URL ? process.env.WERCKER_MONGODB_URL + '/fortune' : null; + + if(remoteDB){ + console.log("Using remote mongodb:",remoteDB); + } + + options.app = require("./app")({ + adapter: "neo4j", + connectionString: remoteDB || "mongodb://localhost/fortune_test", + inflect: true, + enableWebsockets: true + }, port, ioPort); + + var app = options.app; + options.app.adapter.awaitConnection().then(function(){ + return new RSVP.Promise(function(resolve){ + app.adapter.mongoose.connections[1].db.collectionNames(function(err, collections){ + resolve(_.compact(_.map(collections, function(collection){ + + var name = collection.name.split(".")[1]; + if(name && name !== "system"){ + return new RSVP.Promise(function(resolve){ + app.adapter.mongoose.connections[1].db.collection(name, function(err, collection){ + collection.remove({},null, function(){ + console.log("Wiped collection", name); + resolve(); + }); + }); + }); + } + return null; + }))); + }); + }); + }).then(function(wipeFns){ + console.log("Wiping collections:"); + return RSVP.all(wipeFns); + }).then(function(){ + app.router.post("/remove-pets-link/:personid", function(req, res) { + var Person = app.adapter.model("person"); + Person.findOne({email: req.params.personid}, function(err,person) { + if (err) { + console.error(err); + res.send(500,err); + return; + } + person.pets = null; + person.save(function() { + res.send(200); + }); + }); + + }); + }).then(done); + }); + + beforeEach(function(done) { + var createResources = []; + + _.each(fixtures, function (resources, collection) { + createResources.push(new Promise(function (resolve) { + var body = {}; + body[collection] = resources; + + request(baseUrl) + .post('/' + collection) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end(function (error, response) { + should.not.exist(error); + var resources = JSON.parse(response.text)[collection]; + options.ids[collection] = options.ids[collection] || []; + resources.forEach(function (resource) { + options.ids[collection].push(resource.id); + }); + resolve(); + }); + })); + }); + + RSVP.all(createResources).then(function () { + done(); + }, function () { + throw new Error('Failed to create resources.'); + }); + + }); + + require('./fortune-mongodb/neo4j.spec.js')(options); + + afterEach(function(done) { + var promises = []; + _.each(fixtures, function(resources, collection) { + promises.push(new RSVP.Promise(function(resolve) { + request(baseUrl) + .del('/' + collection + '?destroy=true') + .end(function(error) { + resolve(); + }); + })); + }); + RSVP.all(promises).then(function() { + options.ids = {}; + done(); + }, function() { + throw new Error('Failed to delete resources.'); + }); + }); + +}); From 3f4a67e34df258cb27b32d69f5ee64620db3c10a Mon Sep 17 00:00:00 2001 From: Craig Edmunds Date: Thu, 26 Mar 2015 23:39:04 +0000 Subject: [PATCH 5/6] Making find work --- lib/adapters/neo4j.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/adapters/neo4j.js b/lib/adapters/neo4j.js index f9044396a..6f20c3f1c 100644 --- a/lib/adapters/neo4j.js +++ b/lib/adapters/neo4j.js @@ -18,7 +18,7 @@ adapter._init = function (options) { // mongoose.set('debug', options.debug); //Setup mongoose instance - this.db = new neo4j.GraphDatabase('http://localhost:7474'); + this.db = new neo4j.GraphDatabase('http://172.17.0.2:7474'); console.log("Init DB", this.db); }; @@ -180,7 +180,7 @@ adapter.find = function(model, query, projection){ adapter.findMany = function(model, query, projection) { console.log("neo4j findMany", query, projection); //model, ); var that = this; - + if (query.id) { //Don't know why this is coming into findMany with id - would expect it to go into .find... @@ -188,11 +188,23 @@ adapter.findMany = function(model, query, projection) { that.db.getNodeById(query.id, function(err, node) { // resolve([node]); - console.log("getNodeById callback", err, node); + console.log("getNodeById callback", err.length, node); + resolve([{ id : query.id }]) + }); + + }); + } + else { + return new Promise(function (resolve, reject) { + + that.db.getIndexedNodes(null, null, null, function(err, node) { + // resolve([node]); + console.log("getIndexedNodes callback", err.message, node); resolve([{ id : query.id }]) }); }); + } // return [{ id : query.id }]; From e74c7d171b0fe83894f1ea33e8cd0527e462b875 Mon Sep 17 00:00:00 2001 From: Craig Edmunds Date: Fri, 27 Mar 2015 11:36:52 +0000 Subject: [PATCH 6/6] Neo4j POST prototype working --- lib/adapters/neo4j.js | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/lib/adapters/neo4j.js b/lib/adapters/neo4j.js index 6f20c3f1c..e09a21ca7 100644 --- a/lib/adapters/neo4j.js +++ b/lib/adapters/neo4j.js @@ -18,7 +18,7 @@ adapter._init = function (options) { // mongoose.set('debug', options.debug); //Setup mongoose instance - this.db = new neo4j.GraphDatabase('http://172.17.0.2:7474'); + this.db = new neo4j.GraphDatabase(options.connectionString); console.log("Init DB", this.db); }; @@ -134,11 +134,38 @@ adapter.model = function(name, schema, options) { // throw new Error("NotImplemented") }; -adapter.create = function (model, id, resource) { +adapter.create = function (model, resource) { var _this = this; - console.log("neo4j create", model, id, resource); - throw new Error("NotImplemented") + console.log("neo4j create", resource); + + console.log("neo4j _this.db", _this.db); + + var node = _this.db.createNode(resource); + + return new Promise(function (resolve, reject) { + + node.save(function (err, node) { // ...this is what actually persists. + if (err) { + console.error('Error saving new node to database:', err); + reject(err); + } else { + console.log('Node saved to database with id:', node.id, node); + resolve([_.extend(node.data, { id : node.id })]) + } + }); + + // that.db.getNodeById(query.id, function(err, node) { + // // resolve([node]); + // console.log("getNodeById callback", err.length, node); + // resolve([{ id : query.id }]) + // }); + + }); + + + return resource; + // throw new Error("NotImplemented") }; adapter.update = function (model, id, update) {