-
Notifications
You must be signed in to change notification settings - Fork 5
/
neo4j.coffee
161 lines (142 loc) · 4.62 KB
/
neo4j.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
stringify = (value) ->
# turn an object into a string that plays well with Cipher queries.
if _.isArray(value)
"[#{value.map(stringify).join(',')}]"
else if U.isPlainObject(value)
pairs = []
for k,v of value
pairs.push "#{k}:#{stringify(v)}"
"{" + pairs.join(', ') + "}"
else if _.isString(value)
"'#{value.replace(/'/g, "\\'")}'"
else if value is undefined
null
else
"#{value}"
regexify = (string) ->
"'(?i).*#{string.replace(/'/g, "\\'").replace(/\//g, '\/')}.*'"
# transpose a 2D array
transpose = (xy) ->
# get the index, pull the nth item, pass that function to map
R.mapIndexed(R.pipe(R.nthArg(1), R.nth, R.map(R.__, xy)), R.head(xy))
# turns a matrix of rows by columns into rows with key values.
# zip(keys, rowsByColumns) -> [{key:val}, ...]
zip = R.curry (keys, data) ->
z = R.useWith(R.map, R.zipObj)
if keys.length is 1
z(keys, data.map((elm) -> [elm]))
else
z(keys, data)
ensureTrailingSlash = (url) ->
if url[url.length-1] isnt '/'
return url + '/'
else
return url
ensureEnding = (url) ->
ending = 'db/data/'
if url[url.length-ending.length...url.length] is ending
return url
else
return url + ending
parseUrl = (url) ->
url = ensureTrailingSlash(url)
url = ensureEnding(url)
match = url.match(/^(.*\/\/)(.*)@(.*$)/)
if match
# [ 'http://username:password@localhost:7474/',
# 'http://',
# 'username:password',
# 'localhost:7474/']
url = match[1] + match[3]
auth = match[2]
return {url, auth}
else
return {url}
# create a Neo4j connection. you could potentially connect to multiple.
Neo4jDb = (url) ->
db = {}
db.options = {}
# configure url and auth
url = url or 'http://localhost:7474/'
{url, auth} = parseUrl(url)
db.url = url
if auth
db.options.auth = auth
log = console.log.bind(console, "[#{db.url}] neo4j")
warn = console.warn.bind(console, "[#{db.url}] neo4j")
# run http queries catching errors with nice logs
db.http = (f) ->
try
return f()
catch error
if error.response
code = error.response.statusCode
message = error.response.message
if code is 401
warn "[#{code}] auth error:\n", db.options.auth, "\n" + message
else
warn "[#{code}] error response:", message
else
warn "error:", error.toString()
return
# test the connection
db.connect = ->
db.http ->
log "connecting..."
response = HTTP.call('GET', db.url, db.options)
if response.statusCode is 200
log "connected"
else
warn "could not connect\n", response.toString()
# test the database latency
db.latency = ->
db.http ->
R.mean [0...10].map ->
start = Date.now()
HTTP.call('GET', db.url, db.options)
Date.now() - start
# get a query. if theres only one column, the results are flattened. else
# it returns a 2D array of rows by columns.
db.query = (statement, parameters={}) ->
result = db.http ->
params = R.merge(db.options, {data: {statements: [{statement, parameters}]}})
response = HTTP.post(db.url+"transaction/commit", params)
# neo4j can take multiple queries at once, but we're just doing one
if response.data.results.length is 1
# get the first result
result = response.data.results[0]
# the result is a 2D array of rows by columns
# if there was no return statement, then lets return nothing
if result.columns.length is 0
return []
else if result.columns.length is 1
# if theres only one column returned then lets flatten the results
# so we just get that column across all rows
return R.pipe(R.map(R.prop('row')), R.flatten)(result.data)
else
# if there are multiple columns, return an array of rows
return R.map(R.prop('row'))(result.data)
# if we get an error, lets still just return an empty array of data
# so we can map over it or whatever we expected to do originally.
return result or []
db.reset = ->
log "resetting..."
db.query "MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r"
log "reset"
db.isEmpty = ->
[n] = Neo4j.query("MATCH (n) MATCH (n)-[r]-() RETURN count(n)+count(r)")
return (n is 0)
# some utils for generating cypher queries
db.stringify = stringify
db.regexify = regexify
db.transpose = transpose
db.zip = zip
db.connect()
return db
# autoconnect to neo4j if given the appropriate settings or environment variable
if url = Meteor.settings.neo4j_url
Neo4j = Neo4jDb(url)
else if url = process.env.NEO4J_URL
Neo4j = Neo4jDb(url)
@Neo4j = Neo4j
@Neo4jDb = Neo4jDb