Gremlin Base Functionality #47

Merged
merged 7 commits into from Sep 21, 2012
View
@@ -241,6 +241,45 @@ module.exports = class GraphDatabase
catch error
throw adjustError error
+ # wrapper around the Gremlin plugin to execute scripts bundled with
+ # Neo4j. Pass in the Gremlin script as a string, and optionally script
+ # parameters as a map -- recommended for both perf and security!
+ # http://docs.neo4j.org/chunked/snapshot/gremlin-plugin.html
+ # returns...
+ execute: (script, params, _) ->
+ try
+ services = @getServices _
+ endpoint = services.gremlin or
+ services.extensions?.GremlinPlugin?['execute_script']

This comment has been minimized.

@aseemk

aseemk Sep 21, 2012

Member

There is no official gremlin endpoint baked in, so simplifying this to only check the plugin endpoint.

@aseemk

aseemk Sep 21, 2012

Member

There is no official gremlin endpoint baked in, so simplifying this to only check the plugin endpoint.

+
+ if not endpoint
+ throw new Error 'Gremlin plugin not installed'
+
+ response = @_request.post
+ uri: endpoint
+ json: if params then {script, params} else {script}
+ , _
+
+ # XXX workaround for neo4j silent failures for invalid queries:
+ if response.statusCode is status.NO_CONTENT
+ throw new Error """
+ Unknown Neo4j error for Gremlin script:
+
+ #{script}
+
+ """
+
+ if response.statusCode isnt status.OK
+ # Database error
+ throw response
+
+ # Success: transform nodes/relationships
+ results = util.transform response.body, this
+ return results
+
+ catch error
+ throw adjustError error
+
# XXX temporary backwards compatibility shim for query() argument order:
do (actual = @::query) =>
@::query = (query, params, callback) ->
@@ -259,6 +298,16 @@ module.exports = class GraphDatabase
actual.call @, query, params, callback

This comment has been minimized.

@aseemk

aseemk Sep 21, 2012

Member

Minor note: the overload helper for query() should remain just after the query() method itself, so I'll move this up.

@aseemk

aseemk Sep 21, 2012

Member

Minor note: the overload helper for query() should remain just after the query() method itself, so I'll move this up.

+
+ #
+ do (actual = @::execute) =>
+ @::execute = (script, params, callback) ->
+ if typeof params is 'function'
+ callback = params
+ params = null
+
+ actual.call @, script, params, callback
+
# executes a query against the given node index. lucene syntax reference:
# http://lucene.apache.org/java/3_1_0/queryparsersyntax.html
queryNodeIndex: (index, query, _) ->
View
@@ -24,7 +24,8 @@
"clean": "rm lib/*.js",
"postinstall": "npm run build",
"profile": "_coffee test/profile",
- "test": "_coffee test"
+ "test": "_coffee test",
+ "gremlin": "_coffee test/gremlin"

This comment has been minimized.

@aseemk

aseemk Sep 21, 2012

Member

I assume this was for testing during development. =) I'll go ahead remove this, but I'd really like to port our tests to Mocha, which among other things will let you grep which test cases to run!

@aseemk

aseemk Sep 21, 2012

Member

I assume this was for testing during development. =) I'll go ahead remove this, but I'd really like to port our tests to Mocha, which among other things will let you grep which test cases to run!

},
"keywords": [
"neo4j", "graph", "database", "driver", "rest", "api", "client"
View
@@ -0,0 +1,93 @@
+# will be used for testing gremlin script executions
+# as well as validating the return results are as expected
+
+assert = require('assert')
+neo4j = require('..')
+
+db = new neo4j.GraphDatabase 'http://localhost:7474'
+
+# convenience wrapper
+createNode = (name) ->
+ node = db.createNode {name}
+ node.name = name
+ node.toString = -> name
+ node
+
+#create some nodes
+users = for i in [0..6]
+ createNode "gremlinTest#{i}"
+
+# save in parallel
+futures = (user.save() for user in users)
+future _ for future in futures
+
+# convenience aliases
+user0 = users[0]
+user1 = users[1]
+user2 = users[2]
+user3 = users[3]
+user4 = users[4]
+user5 = users[5]
+user6 = users[6]
+
+# test: can query a single user
+results = db.execute """
+ g.v(#{user0.id})
+""", {}, _

This comment has been minimized.

@aseemk

aseemk Sep 21, 2012

Member

Given that you already test no params at the end, I'll change this to test params support since it doesn't seem like any other case tests that.

@aseemk

aseemk Sep 21, 2012

Member

Given that you already test no params at the end, I'll change this to test params support since it doesn't seem like any other case tests that.

+
+assert.ok typeof results, 'object'
+assert.ok typeof results.data.name, 'string' # dislike this because it will throw for the wrong reasons possibly

This comment has been minimized.

@aseemk

aseemk Sep 21, 2012

Member

You can use results.data?.name to guard against those. =)

@aseemk

aseemk Sep 21, 2012

Member

You can use results.data?.name to guard against those. =)

This comment has been minimized.

@aseemk

aseemk Sep 21, 2012

Member

But in this case, you don't need to check the name's type since you're already checking its value (via strict equality from assert.equal).

@aseemk

aseemk Sep 21, 2012

Member

But in this case, you don't need to check the name's type since you're already checking its value (via strict equality from assert.equal).

This comment has been minimized.

@aseemk

aseemk Sep 21, 2012

Member

Oh also, watch out -- if you're checking typeof, you want to use assert.equal, not assert.ok. (Tricky because instanceof is the opposite.) I'd love to move to expect.js or similar when moving to Mocha.

@aseemk

aseemk Sep 21, 2012

Member

Oh also, watch out -- if you're checking typeof, you want to use assert.equal, not assert.ok. (Tricky because instanceof is the opposite.) I'd love to move to expect.js or similar when moving to Mocha.

+assert.equal results.data.name, user0.name
+
+# test: create relationships between users (same as cypher tests), then query by relationships
+createFollowRelationships = (i, _) ->
+ user = users[i]
+ i1 = (i + 1) % users.length
+ i2 = (i + 2) % users.length
+ i3 = (i + 3) % users.length
+ f1 = user.createRelationshipTo users[i1], 'gremlin_follows', {}
+ f2 = user.createRelationshipTo users[i2], 'gremlin_follows', {}
+ f3 = user.createRelationshipTo users[i3], 'gremlin_follows', {}
+ f1 _
+ f2 _
+ f3 _
+
+# create follow relationships for each user in parallel
+futures = (createFollowRelationships(i) for user, i in users)
+future _ for future in futures
+
+relationships = db.execute """
+ g.v(#{user0.id}).in('gremlin_follows')
+""", {} , _
+
+# above is working, but lib should support returning instances of Node and Relationship if possible

This comment has been minimized.

@aseemk

aseemk Sep 21, 2012

Member

It does now! ;)

@aseemk

aseemk Sep 21, 2012

Member

It does now! ;)

+
+
+# handle multiple types of data return
+traversals = db.execute """
+ g.v(#{user0.id}).transform{ [it, it.out.toList(), it.in.count()] }
+""", {}, _
+
+assert.ok traversals instanceof Array
+assert.equal traversals.length, 1
+
+assert.ok traversals[0] instanceof Array
+assert.equal traversals[0].length, 3
+
+assert.ok typeof traversals[0][0], 'object'
+assert.ok traversals[0][1] instanceof Array
+assert.ok typeof traversals[0][2], 'number'
+
+console.log 'Multiple data types appear to have worked with .execute() and util.transform()'
+
+# ensure you can call without params
+
+params_test = db.execute """
+ g.v(#{user0.id})
+""", _
+
+assert.ok typeof params_test, 'object'
+assert.equal params_test.data.name, user0.name
+
+# Should be relatively clear at this point the .execute() function is working with gremlin on some level
+console.log 'Passed initial Gremlin tests'
View
@@ -1,2 +1,3 @@
require './crud'
require './cypher'
+require './gremlin'