Skip to content

Commit 5f08b5f

Browse files
committed
v2 / Core: impl. and test parsing of nodes & rels!
1 parent e52ed5c commit 5f08b5f

File tree

7 files changed

+291
-4
lines changed

7 files changed

+291
-4
lines changed

lib-new/GraphDatabase.coffee

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
$ = require 'underscore'
22
errors = require './errors'
33
lib = require '../package.json'
4+
Node = require './Node'
5+
Relationship = require './Relationship'
46
Request = require 'request'
57
URL = require 'url'
68

@@ -36,7 +38,6 @@ module.exports = class GraphDatabase
3638
method or= 'get'
3739
headers or= {}
3840

39-
# TODO: Parse response body before calling back.
4041
# TODO: Do we need to do anything special to support streaming response?
4142
req = Request[method.toLowerCase()]
4243
url: URL.resolve @url, path
@@ -66,5 +67,39 @@ module.exports = class GraphDatabase
6667
http: {body, headers, statusCode}
6768
return cb err
6869

69-
# TODO: Parse nodes and relationships.
70-
return cb null, body
70+
# Parse nodes and relationships in the body, and return:
71+
return cb null, _transform body
72+
73+
74+
## HELPERS
75+
76+
#
77+
# Deep inspects the given object -- which could be a simple primitive, a map,
78+
# an array with arbitrary other objects, etc. -- and transforms any objects that
79+
# look like nodes and relationships into Node and Relationship instances.
80+
# Returns the transformed object, and does not mutate the input object.
81+
#
82+
_transform = (obj) ->
83+
# Nothing to transform for primitives and null:
84+
if (not obj) or (typeof obj isnt 'object')
85+
return obj
86+
87+
# Process arrays:
88+
# (NOTE: Not bothering to detect arrays made in other JS contexts.)
89+
if obj instanceof Array
90+
return obj.map _transform
91+
92+
# Feature-detect (AKA "duck-type") Node & Relationship objects, by simply
93+
# trying to parse them as such.
94+
# Important: check relationships first, for precision/specificity.
95+
# TODO: If we add a Path class, we'll need to check for that here too.
96+
if rel = Relationship._fromRaw obj
97+
return rel
98+
if node = Node._fromRaw obj
99+
return node
100+
101+
# Otherwise, process as a dictionary/map:
102+
map = {}
103+
for key, val of obj
104+
map[key] = _transform val
105+
map

lib-new/Node.coffee

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
utils = require './utils'
2+
3+
module.exports = class Node
4+
5+
constructor: (opts={}) ->
6+
{@_id, @labels, @properties} = opts
7+
8+
equals: (other) ->
9+
# TODO: Is this good enough? Often don't want exact equality, e.g.
10+
# nodes' properties may change between queries.
11+
(other instanceof Node) and (@_id is other._id)
12+
13+
toString: ->
14+
labels = @labels.map (label) -> ":#{label}"
15+
"(#{@_id}#{labels.join ''})" # E.g. (123), (456:Foo), (789:Foo:Bar)
16+
17+
#
18+
# Accepts the given raw JSON from Neo4j's REST API, and if it represents a
19+
# valid node, creates and returns a Node instance from it.
20+
# If the JSON doesn't represent a valid node, returns null.
21+
#
22+
@_fromRaw: (obj) ->
23+
return null if (not obj) or (typeof obj isnt 'object')
24+
25+
{data, metadata, self} = obj
26+
27+
return null if (not self) or (typeof self isnt 'string') or
28+
(not data) or (typeof data isnt 'object')
29+
30+
# Metadata was only added in Neo4j 2.1.5, so don't *require* it,
31+
# but (a) it makes our job easier, and (b) it's the only way we can get
32+
# labels, so warn the developer if it's missing, but only once.
33+
if metadata
34+
{id, labels} = metadata
35+
else
36+
id = utils.parseId self
37+
labels = null
38+
39+
if not @_warnedMetadata
40+
@_warnedMetadata = true
41+
console.warn 'It looks like you’re running Neo4j <2.1.5.
42+
Neo4j <2.1.5 didn’t return label metadata to drivers,
43+
so node-neo4j has no way to associate nodes with labels.
44+
Thus, the `labels` property on neo4j `Node`s will always be
45+
null for you. Consider upgrading to fix. =)
46+
http://neo4j.com/release-notes/neo4j-2-1-5/'
47+
48+
return new Node
49+
_id: id
50+
labels: labels
51+
properties: data

lib-new/Relationship.coffee

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
utils = require './utils'
2+
3+
module.exports = class Relationship
4+
5+
constructor: (opts={}) ->
6+
{@_id, @type, @properties, @_fromId, @_toId} = opts
7+
8+
equals: (other) ->
9+
# TODO: Is this good enough? Often don't want exact equality, e.g.
10+
# nodes' properties may change between queries.
11+
(other instanceof Relationship) and (@_id is other._id)
12+
13+
toString: ->
14+
"-[#{@_id}:#{@type}]-" # E.g. -[123:FOLLOWS]-
15+
16+
#
17+
# Accepts the given raw JSON from Neo4j's REST API, and if it represents a
18+
# valid relationship, creates and returns a Relationship instance from it.
19+
# If the JSON doesn't represent a valid relationship, returns null.
20+
#
21+
@_fromRaw: (obj) ->
22+
return null if (not obj) or (typeof obj isnt 'object')
23+
24+
{data, self, type, start, end} = obj
25+
26+
return null if (not self) or (typeof self isnt 'string') or
27+
(not type) or (typeof type isnt 'string') or
28+
(not start) or (typeof start isnt 'string') or
29+
(not end) or (typeof end isnt 'string') or
30+
(not data) or (typeof data isnt 'object')
31+
32+
# Relationships also have `metadata`, added in Neo4j 2.1.5, but it
33+
# doesn't provide anything new. (And it doesn't give us from/to ID.)
34+
# We don't want to rely on it, so we don't bother using it at all.
35+
id = utils.parseId self
36+
fromId = utils.parseId start
37+
toId = utils.parseId end
38+
39+
return new Relationship
40+
_id: id
41+
type: type
42+
properties: data
43+
_fromId: fromId
44+
_toId: toId

lib-new/index.coffee

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@ $ = require 'underscore'
22

33
$(exports).extend
44
GraphDatabase: require './GraphDatabase'
5+
Node: require './Node'
6+
Relationship: require './Relationship'
57

68
$(exports).extend require './errors'

lib-new/utils.coffee

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
#
3+
# Parses and returns the native Neo4j ID out of the given Neo4j URL,
4+
# or null if no ID could be matched.
5+
#
6+
@parseId = (url) ->
7+
match = url.match /// /db/data/(node|relationship)/(\d+)$ ///
8+
return null if not match
9+
return parseInt match[2], 10

test-new/fixtures/index._coffee

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#
2+
# NOTE: This file is within a directory named `fixtures`, rather than a file
3+
# named `fixtures._coffee`, in order to not have Mocha treat it like a test.
4+
#
5+
6+
neo4j = require '../../'
7+
8+
exports.DB =
9+
new neo4j.GraphDatabase process.env.NEO4J_URL or 'http://localhost:7474'
10+
11+
exports.TEST_LABEL = 'Test'
12+
13+
exports.TEST_REL_TYPE = 'TEST'

test-new/http._coffee

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
{expect} = require 'chai'
2+
fixtures = require './fixtures'
23
http = require 'http'
34
neo4j = require '../'
45

56

67
## SHARED STATE
78

8-
DB = new neo4j.GraphDatabase process.env.NEO4J_URL or 'http://localhost:7474'
9+
{DB, TEST_LABEL, TEST_REL_TYPE} = fixtures
10+
11+
TEST_NODE_A = new neo4j.Node
12+
# _id will get filled in once we persist
13+
labels: [TEST_LABEL]
14+
properties: {suite: module.filename, name: 'a'}
15+
16+
TEST_NODE_B = new neo4j.Node
17+
# _id will get filled in once we persist
18+
labels: [TEST_LABEL]
19+
properties: {suite: module.filename, name: 'b'}
20+
21+
TEST_REL = new neo4j.Relationship
22+
# _id, _fromId (node A), _toId (node B) will get filled in once we persist
23+
type: TEST_REL_TYPE
24+
properties: {suite: module.filename, name: 'r'}
925

1026

1127
## HELPERS
@@ -106,3 +122,120 @@ describe 'GraphDatabase::http', ->
106122
# Test that it immediately returns a duplex HTTP stream.
107123
# Test writing request data to this stream.
108124
# Test reading response data from this stream.
125+
126+
127+
## Object parsing:
128+
129+
it '(create test objects)', (_) ->
130+
# NOTE: Using the old Cypher endpoint for simplicity here.
131+
# Nicer than using the raw REST API to create these test objects,
132+
# but also nice to neither use this driver's Cypher functionality
133+
# (which is tested in a higher-level test suite), nor re-implement it.
134+
# http://neo4j.com/docs/stable/rest-api-cypher.html#rest-api-use-parameters
135+
{data} = DB.http
136+
method: 'POST'
137+
path: '/db/data/cypher'
138+
body:
139+
query: """
140+
CREATE (a:#{TEST_LABEL} {propsA})
141+
CREATE (b:#{TEST_LABEL} {propsB})
142+
CREATE (a) -[r:#{TEST_REL_TYPE} {propsR}]-> (b)
143+
RETURN ID(a), ID(b), ID(r)
144+
"""
145+
params:
146+
propsA: TEST_NODE_A.properties
147+
propsB: TEST_NODE_B.properties
148+
propsR: TEST_REL.properties
149+
, _
150+
151+
[row] = data
152+
[idA, idB, idR] = row
153+
154+
TEST_NODE_A._id = idA
155+
TEST_NODE_B._id = idB
156+
TEST_REL._id = idR
157+
TEST_REL._fromId = idA
158+
TEST_REL._toId = idB
159+
160+
it 'should parse nodes by default', (_) ->
161+
node = DB.http
162+
method: 'GET'
163+
path: "/db/data/node/#{TEST_NODE_A._id}"
164+
, _
165+
166+
expect(node).to.be.an.instanceOf neo4j.Node
167+
expect(node).to.eql TEST_NODE_A
168+
169+
it 'should parse relationships by default', (_) ->
170+
rel = DB.http
171+
method: 'GET'
172+
path: "/db/data/relationship/#{TEST_REL._id}"
173+
, _
174+
175+
expect(rel).to.be.an.instanceOf neo4j.Relationship
176+
expect(rel).to.eql TEST_REL
177+
178+
it 'should parse nested nodes & relationships by default', (_) ->
179+
{data} = DB.http
180+
method: 'POST'
181+
path: '/db/data/cypher'
182+
body:
183+
query: """
184+
START a = node({idA})
185+
MATCH (a) -[r:#{TEST_REL_TYPE}]-> (b)
186+
RETURN a, r, b
187+
"""
188+
params:
189+
idA: TEST_NODE_A._id
190+
, _
191+
192+
[row] = data
193+
[nodeA, rel, nodeB] = row
194+
195+
expect(nodeA).to.be.an.instanceOf neo4j.Node
196+
expect(nodeA).to.eql TEST_NODE_A
197+
expect(nodeB).to.be.an.instanceOf neo4j.Node
198+
expect(nodeB).to.eql TEST_NODE_B
199+
expect(rel).to.be.an.instanceOf neo4j.Relationship
200+
expect(rel).to.eql TEST_REL
201+
202+
it 'should not parse nodes for raw responses', (_) ->
203+
{body} = DB.http
204+
method: 'GET'
205+
path: "/db/data/node/#{TEST_NODE_A._id}"
206+
raw: true
207+
, _
208+
209+
expect(body).to.not.be.an.instanceOf neo4j.Node
210+
expect(body.metadata).to.be.an 'object'
211+
expect(body.metadata.id).to.equal TEST_NODE_A._id
212+
expect(body.metadata.labels).to.eql TEST_NODE_A.labels
213+
expect(body.data).to.eql TEST_NODE_A.properties
214+
215+
it 'should not parse relationships for raw responses', (_) ->
216+
{body} = DB.http
217+
method: 'GET'
218+
path: "/db/data/relationship/#{TEST_REL._id}"
219+
raw: true
220+
, _
221+
222+
expect(body.metadata).to.be.an 'object'
223+
expect(body.metadata.id).to.equal TEST_REL._id
224+
expect(body).to.not.be.an.instanceOf neo4j.Relationship
225+
expect(body.type).to.equal TEST_REL.type
226+
expect(body.data).to.eql TEST_REL.properties
227+
228+
it '(delete test objects)', (_) ->
229+
DB.http
230+
method: 'POST'
231+
path: '/db/data/cypher'
232+
body:
233+
query: """
234+
START a = node({idA}), b = node({idB}), r = rel({idR})
235+
DELETE a, b, r
236+
"""
237+
params:
238+
idA: TEST_NODE_A._id
239+
idB: TEST_NODE_B._id
240+
idR: TEST_REL._id
241+
, _

0 commit comments

Comments
 (0)