forked from thingdom/node-neo4j
-
Notifications
You must be signed in to change notification settings - Fork 0
/
GraphDatabase._coffee
337 lines (255 loc) · 10 KB
/
GraphDatabase._coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# TODO many of these functions take a callback but, in some cases, call the
# callback immediately (e.g. if a value is cached). we should probably make
# sure to always call callbacks asynchronously, to prevent race conditions.
# this can be done in Streamline syntax by adding one line before cases where
# we're returning immediately: process.nextTick _
status = require 'http-status'
util = require './util'
adjustError = util.adjustError
Relationship = require './Relationship'
Node = require './Node'
module.exports = class GraphDatabase
constructor: (opts) ->
# normalize arg:
opts =
if typeof opts is 'string' then {url: opts}
else opts
{@url} = opts
@_request = util.wrapRequest opts
# Cache
@_root = null
@_services = null
# Database
_purgeCache: ->
@_root = null
@_services = null
_getRoot: (_) ->
if @_root?
return @_root
try
response = @_request.get @url, _
if response.statusCode isnt status.OK
throw response
return @_root = response.body
catch error
throw adjustError error
getServices: (_) ->
if @_services?
return @_services
try
root = @_getRoot _
response = @_request.get root.data, _
if response.statusCode isnt status.OK
throw response
return @_services = response.body
catch error
throw adjustError error
getVersion: (_) ->
try
services = @getServices _
# Neo4j 1.5 onwards report their version number here;
# if it's not there, assume Neo4j 1.4.
parseFloat services['neo4j_version'] or '1.4'
catch error
throw adjustError
# Nodes
createNode: (data) ->
data = data || {}
node = new Node this,
data: data
return node
getNode: (url, _) ->
try
response = @_request.get url, _
if response.statusCode isnt status.OK
# Node not found
if response.statusCode is status.NOT_FOUND
throw new Error "No node at #{url}"
# Other unknown errors
throw response
return new Node this, response.body
catch error
throw adjustError error
getIndexedNode: (index, property, value, _) ->
try
nodes = @getIndexedNodes index, property, value, _
node = null
if nodes and nodes.length > 0
node = nodes[0]
return node
catch error
throw adjustError error
getIndexedNodes: (index, property, value, _) ->
try
services = @getServices _
key = encodeURIComponent property
val = encodeURIComponent value
url = "#{services.node_index}/#{index}/#{key}/#{val}"
response = @_request.get url, _
if response.statusCode isnt status.OK
# Database error
throw response
# Success
return response.body.map (node) =>
new Node this, node
catch error
throw adjustError error
getNodeById: (id, _) ->
try
services = @getServices _
url = "#{services.node}/#{id}"
node = @getNode url, _
return node
catch error
throw adjustError error
# Relationships
createRelationship: (startNode, endNode, type, _) ->
# TODO: Implement
getRelationship: (url, _) ->
try
response = @_request.get url, _
if response.statusCode isnt status.OK
# TODO: Handle 404
throw response
return new Relationship this, response.body
catch error
throw adjustError error
getIndexedRelationship: (index, property, value, _) ->
try
relationships = @getIndexedRelationships index, property, value, _
return relationships?[0] or null
catch error
throw adjustError error
getIndexedRelationships: (index, property, value, _) ->
try
services = @getServices _
key = encodeURIComponent property
val = encodeURIComponent value
url = "#{services.relationship_index}/#{index}/#{key}/#{val}"
response = @_request.get url, _
if response.statusCode isnt status.OK
# Database error
throw response
# Success
return response.body.map (relationship) =>
new Relationship this, relationship
catch error
throw adjustError error
getRelationshipById: (id, _) ->
services = @getServices _
# FIXME: Neo4j doesn't expose the path to relationships
relationshipURL = services.node.replace('node', 'relationship')
url = "#{relationshipURL}/#{id}"
@getRelationship url, _
# wrapper around the Cypher plugin, which comes bundled w/ Neo4j.
# pass in the Cypher query as a string (can be multi-line), and optionally
# query parameters as a map -- recommended for both perf and security!
# http://docs.neo4j.org/chunked/stable/cypher-query-lang.html
# returns an array of "rows" (matches), where each row is a map from
# variable name (as given in the passed in query) to value. any values
# that represent nodes or relationships are transformed to instances.
query: (query, params, _) ->
try
services = @getServices _
endpoint = services.cypher or
services.extensions?.CypherPlugin?['execute_query']
if not endpoint
throw new Error 'Cypher plugin not installed'
response = @_request.post
uri: endpoint
json: if params then {query, params} else {query}
, _
# XXX workaround for neo4j silent failures for invalid queries:
if response.statusCode is status.NO_CONTENT
throw new Error """
Unknown Neo4j error for query:
#{query}
"""
if response.statusCode isnt status.OK
# Database error
throw response
# Success: build result maps, and transform nodes/relationships
body = response.body
columns = body.columns
results = for row in body.data
map = {}
for value, i in row
map[columns[i]] = util.transform value, this
map
return results
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']
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: build result maps, and transform nodes/relationships
body = response.body # JSON already parsed by request
if body instanceof Array
results = for row in body
map = util.transform row, this
map
else
results = util.transform 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) ->
if typeof query is 'function' and typeof params is 'string'
# instantiate a new error to derive the current stack, and
# show the relevant source line in a warning:
console.warn 'neo4j.GraphDatabase::query()’s signature is ' +
'now (query, params, callback). Please update your code!\n' +
new Error().stack.split('\n')[2] # includes indentation
callback = query
query = params
params = null
else if typeof params is 'function'
callback = params
params = null
actual.call @, query, params, callback
#
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, _) ->
try
services = @getServices _
url = "#{services.node_index}/#{index}?query=#{encodeURIComponent query}"
response = @_request.get url, _
if response.statusCode isnt status.OK
# Database error
throw response
# Success
return response.body.map (node) =>
new Node this, node
catch error
throw adjustError error