neo4j-core 3.0.8 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/lib/neo4j-core/cypher_translator.rb +55 -42
- data/lib/neo4j-core/version.rb +1 -1
- data/lib/neo4j-server.rb +1 -0
- data/lib/neo4j-server/cypher_authentication.rb +101 -0
- data/lib/neo4j-server/cypher_node.rb +52 -46
- data/lib/neo4j-server/cypher_relationship.rb +16 -19
- data/lib/neo4j-server/cypher_session.rb +37 -77
- data/lib/neo4j/tasks/config_server.rb +21 -5
- data/lib/neo4j/tasks/neo4j_server.rake +82 -37
- metadata +74 -101
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f4890cd93db0d6c36f8208fc1e9389c587edaf2
|
4
|
+
data.tar.gz: dea68555caf4904ac68214b6cea915cb1c8462e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3e34f33523134c71bb13aa7858a3f2444bcfb252f39d2dc54c41417d8c291347e47a8730c200639e1cb0ef7094b1c84941312b1210877c46f4471c4bcaca84ce
|
7
|
+
data.tar.gz: 2a0cb02bb6ea67c92e9964643c5743a2bca5b35385cc7079b02433c283014eb929c6d403f1ef148b66b8d3eda9a2d214be9f5d6c23e3db621d120764c6dee40d
|
data/Gemfile
CHANGED
@@ -14,6 +14,8 @@ group 'development' do
|
|
14
14
|
gem 'yard'
|
15
15
|
gem 'simplecov'
|
16
16
|
gem 'pry'
|
17
|
+
gem 'pry-rescue'
|
18
|
+
gem 'pry-stack_explorer', platform: :ruby
|
17
19
|
end
|
18
20
|
|
19
21
|
group 'test' do
|
@@ -21,4 +23,3 @@ group 'test' do
|
|
21
23
|
gem "rspec", "~> 3.0"
|
22
24
|
gem 'rspec-its'
|
23
25
|
end
|
24
|
-
|
@@ -1,47 +1,52 @@
|
|
1
1
|
module Neo4j::Core
|
2
2
|
module CypherTranslator
|
3
3
|
# Cypher Helper
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
12
|
-
result
|
13
|
-
end
|
14
|
-
|
15
|
-
# Only following escape sequence characters are allowed in Cypher:
|
16
|
-
#
|
17
|
-
# \t Tab
|
18
|
-
# \b Backspace
|
19
|
-
# \n Newline
|
20
|
-
# \r Carriage return
|
21
|
-
# \f Form feed
|
22
|
-
# \' Single quote
|
23
|
-
# \" Double quote
|
24
|
-
# \\ Backslash
|
25
|
-
#
|
26
|
-
# From:
|
27
|
-
# http://docs.neo4j.org/chunked/stable/cypher-expressions.html#_note_on_string_literals
|
28
|
-
SANITIZE_ESCAPED_REGEXP = /(?<!\\)\\(\\\\)*(?![futbnr'"\\])/
|
29
|
-
def sanitize_escape_sequences(s)
|
30
|
-
s.gsub SANITIZE_ESCAPED_REGEXP, ''
|
31
|
-
end
|
32
|
-
|
33
|
-
def escape_quotes(s)
|
34
|
-
s.gsub("'", %q(\\\'))
|
35
|
-
end
|
4
|
+
def escape_value(value)
|
5
|
+
if value.is_a?(String) || value.is_a?(Symbol)
|
6
|
+
"'#{escape_quotes(sanitize_escape_sequences(value.to_s))}'"
|
7
|
+
else
|
8
|
+
value
|
9
|
+
end
|
10
|
+
end
|
36
11
|
|
37
|
-
#
|
38
|
-
def
|
39
|
-
|
12
|
+
# Like escape_value but it does not wrap the value in quotes
|
13
|
+
def create_escape_value(value)
|
14
|
+
if value.is_a?(String) || value.is_a?(Symbol)
|
15
|
+
"#{sanitize_escape_sequences(value.to_s)}"
|
16
|
+
else
|
17
|
+
value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Only following escape sequence characters are allowed in Cypher:
|
22
|
+
#
|
23
|
+
# \t Tab
|
24
|
+
# \b Backspace
|
25
|
+
# \n Newline
|
26
|
+
# \r Carriage return
|
27
|
+
# \f Form feed
|
28
|
+
# \' Single quote
|
29
|
+
# \" Double quote
|
30
|
+
# \\ Backslash
|
31
|
+
#
|
32
|
+
# From:
|
33
|
+
# http://docs.neo4j.org/chunked/stable/cypher-expressions.html#_note_on_string_literals
|
34
|
+
SANITIZE_ESCAPED_REGEXP = /(?<!\\)\\(\\\\)*(?![futbnr'"\\])/
|
35
|
+
EMPTY_PROPS = ''
|
40
36
|
|
41
|
-
|
37
|
+
def sanitize_escape_sequences(s)
|
38
|
+
s.gsub SANITIZE_ESCAPED_REGEXP, EMPTY_PROPS
|
39
|
+
end
|
42
40
|
|
43
|
-
|
44
|
-
|
41
|
+
def escape_quotes(s)
|
42
|
+
s.gsub("'", %q(\\\'))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Cypher Helper
|
46
|
+
def cypher_prop_list(props)
|
47
|
+
return nil unless props
|
48
|
+
props.reject! {|k,v| v.nil? }
|
49
|
+
{ props: props.each { |k, v| props[k] = create_escape_value(v) } }
|
45
50
|
end
|
46
51
|
|
47
52
|
# Stolen from keymaker
|
@@ -51,11 +56,19 @@ module Neo4j::Core
|
|
51
56
|
end
|
52
57
|
|
53
58
|
def self.sanitized_column_names(response_body)
|
54
|
-
response_body.columns.map
|
55
|
-
column[/[^\.]+$/]
|
56
|
-
end
|
59
|
+
response_body.columns.map { |column| column[/[^\.]+$/] }
|
57
60
|
end
|
58
61
|
|
59
|
-
|
62
|
+
def cypher_string(labels, props)
|
63
|
+
"CREATE (n#{label_string(labels)} #{prop_identifier(props)}) RETURN ID(n)"
|
64
|
+
end
|
60
65
|
|
66
|
+
def label_string(labels)
|
67
|
+
labels.empty? ? '' : ":#{labels.map{|k| "`#{k}`"}.join(':')}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def prop_identifier(props)
|
71
|
+
'{props}' unless props.nil?
|
72
|
+
end
|
73
|
+
end
|
61
74
|
end
|
data/lib/neo4j-core/version.rb
CHANGED
data/lib/neo4j-server.rb
CHANGED
@@ -4,6 +4,7 @@ require 'faraday_middleware'
|
|
4
4
|
require 'neo4j-server/resource'
|
5
5
|
require 'neo4j-server/cypher_node'
|
6
6
|
require 'neo4j-server/cypher_label'
|
7
|
+
require 'neo4j-server/cypher_authentication'
|
7
8
|
require 'neo4j-server/cypher_session'
|
8
9
|
require 'neo4j-server/cypher_node_uncommited'
|
9
10
|
require 'neo4j-server/cypher_relationship'
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Neo4j::Server
|
2
|
+
# Neo4j 2.2 has an authentication layer. This class provides methods for interacting with it.
|
3
|
+
class CypherAuthentication
|
4
|
+
class InvalidPasswordError < RuntimeError; end
|
5
|
+
class PasswordChangeRequiredError < RuntimeError; end
|
6
|
+
class MissingCredentialsError < RuntimeError; end
|
7
|
+
|
8
|
+
attr_reader :connection, :url, :params, :token
|
9
|
+
|
10
|
+
# @param [String] url_string The server address with protocol and port.
|
11
|
+
# @param [Faraday::Connection] session_connection A Faraday::Connection object. This is either an existing object, likely the
|
12
|
+
# same object used by the server for data, or a new one created specifically for auth tasks.
|
13
|
+
# @param [Hash] params_hash Faraday connection options. In particularly, we're looking for basic_auth creds.
|
14
|
+
def initialize(url_string, session_connection = new_connection, params_hash = {})
|
15
|
+
@url = url_string
|
16
|
+
@connection = session_connection
|
17
|
+
@params = params_hash
|
18
|
+
end
|
19
|
+
|
20
|
+
# Set the username and password used to communicate with the server.
|
21
|
+
def basic_auth(username, password)
|
22
|
+
params[:basic_auth] ||= {}
|
23
|
+
params[:basic_auth][:username] = username
|
24
|
+
params[:basic_auth][:password] = password
|
25
|
+
end
|
26
|
+
|
27
|
+
# POSTs to the password change endpoint of the API. Does not invalidate tokens.
|
28
|
+
# @param [String] old_password The current password.
|
29
|
+
# @param [String] new_password The password you want to use.
|
30
|
+
# @return [Hash] The response from the server.
|
31
|
+
def change_password(old_password, new_password)
|
32
|
+
connection.post("#{url}/user/neo4j/password", { 'password' => old_password, 'new_password' => new_password }).body
|
33
|
+
end
|
34
|
+
|
35
|
+
# Uses the given username and password to obtain a token, then adds the token to the connection's parameters.
|
36
|
+
# @return [String] An access token provided by the server.
|
37
|
+
def authenticate
|
38
|
+
auth_response = connection.get("#{url}/authentication")
|
39
|
+
return nil if auth_response.body.empty?
|
40
|
+
auth_body = JSON.parse(auth_response.body)
|
41
|
+
token = auth_body['errors'][0]['code'] == 'Neo.ClientError.Security.AuthorizationFailed' ? obtain_token : nil
|
42
|
+
add_auth_headers(token) unless token.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Invalidates the existing token, which will invalidate all conncetions using this token, applies for a new token, adds this into
|
46
|
+
# the connection headers.
|
47
|
+
# @param [String] password The current server password.
|
48
|
+
def reauthenticate(password)
|
49
|
+
invalidate_token(password)
|
50
|
+
add_auth_headers(obtain_token)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Requests a token from the authentication endpoint using the given username and password.
|
54
|
+
# @return [String] A plain-text token.
|
55
|
+
def obtain_token
|
56
|
+
begin
|
57
|
+
user = params[:basic_auth][:username]
|
58
|
+
pass = params[:basic_auth][:password]
|
59
|
+
rescue NoMethodError
|
60
|
+
raise MissingCredentialsError, 'Neo4j authentication is enabled, username/password are required but missing'
|
61
|
+
end
|
62
|
+
auth_response = connection.post("#{url}/authentication", { 'username' => user, 'password' => pass })
|
63
|
+
raise PasswordChangeRequiredError, "Server requires a password change, please visit #{url}" if auth_response.body['password_change_required']
|
64
|
+
raise InvalidPasswordError, "Neo4j server responded with: #{auth_response.body['errors'][0]['message']}" if auth_response.status.to_i == 422
|
65
|
+
auth_response.body['authorization_token']
|
66
|
+
end
|
67
|
+
|
68
|
+
# Invalidates tokens as described at http://neo4j.com/docs/snapshot/rest-api-security.html#rest-api-invalidating-the-authorization-token
|
69
|
+
# @param [String] current_password The current password used to connect to the database
|
70
|
+
def invalidate_token(current_password)
|
71
|
+
connection.post("#{url}/user/neo4j/authorization_token", { 'password' => current_password }).body
|
72
|
+
end
|
73
|
+
|
74
|
+
# Stores an authentication token in the properly-formatted header.
|
75
|
+
# @param [String] token The authentication token provided by the database.
|
76
|
+
def add_auth_headers(token)
|
77
|
+
@token = token
|
78
|
+
connection.headers['Authorization'] = "Basic realm=\"Neo4j\" #{token_hash(token)}"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def self.new_connection
|
84
|
+
conn = Faraday.new do |b|
|
85
|
+
b.request :json
|
86
|
+
b.response :json, :content_type => "application/json"
|
87
|
+
b.use Faraday::Adapter::NetHttpPersistent
|
88
|
+
end
|
89
|
+
conn.headers = { 'Content-Type' => 'application/json' }
|
90
|
+
conn
|
91
|
+
end
|
92
|
+
|
93
|
+
def new_connection
|
94
|
+
self.class.new_connection
|
95
|
+
end
|
96
|
+
|
97
|
+
def token_hash(token)
|
98
|
+
Base64.strict_encode64(":#{token}")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -32,18 +32,21 @@ module Neo4j::Server
|
|
32
32
|
|
33
33
|
# (see Neo4j::Node#create_rel)
|
34
34
|
def create_rel(type, other_node, props = nil)
|
35
|
-
|
36
|
-
id = @session._query_or_fail(q, true)
|
35
|
+
id = @session._query_or_fail(rel_string(type, other_node, props), true, cypher_prop_list(props))
|
37
36
|
data_hash = { 'type' => type, 'data' => props, 'start' => self.neo_id.to_s, 'end' => other_node.neo_id.to_s, 'id' => id }
|
38
37
|
CypherRelationship.new(@session, data_hash)
|
39
38
|
end
|
40
39
|
|
40
|
+
def rel_string(type, other_node, props)
|
41
|
+
"MATCH (a), (b) WHERE ID(a) = #{neo_id} AND ID(b) = #{other_node.neo_id} CREATE (a)-[r:`#{type}` #{prop_identifier(props)}]->(b) RETURN ID(r)"
|
42
|
+
end
|
43
|
+
|
41
44
|
# (see Neo4j::Node#props)
|
42
45
|
def props
|
43
46
|
if @props
|
44
47
|
@props
|
45
48
|
else
|
46
|
-
hash = @session._query_entity_data("
|
49
|
+
hash = @session._query_entity_data("#{match_start} RETURN n")
|
47
50
|
@props = Hash[hash['data'].map{ |k, v| [k.to_sym, v] }]
|
48
51
|
end
|
49
52
|
end
|
@@ -55,26 +58,26 @@ module Neo4j::Server
|
|
55
58
|
# (see Neo4j::Node#remove_property)
|
56
59
|
def remove_property(key)
|
57
60
|
refresh
|
58
|
-
@session._query_or_fail("
|
61
|
+
@session._query_or_fail("#{match_start} REMOVE n.`#{key}`")
|
59
62
|
end
|
60
63
|
|
61
64
|
# (see Neo4j::Node#set_property)
|
62
65
|
def set_property(key,value)
|
63
66
|
refresh
|
64
|
-
@session._query_or_fail("
|
67
|
+
@session._query_or_fail("#{match_start} SET n.`#{key}` = { value }", false, value: value)
|
65
68
|
value
|
66
69
|
end
|
67
70
|
|
68
71
|
# (see Neo4j::Node#props=)
|
69
72
|
def props=(properties)
|
70
73
|
refresh
|
71
|
-
@session._query_or_fail("
|
74
|
+
@session._query_or_fail("#{match_start} SET n = { props }", false, {props: properties})
|
72
75
|
properties
|
73
76
|
end
|
74
77
|
|
75
78
|
def remove_properties(properties)
|
76
79
|
refresh
|
77
|
-
q = "
|
80
|
+
q = "#{match_start} REMOVE " + properties.map do |k|
|
78
81
|
"n.`#{k}`"
|
79
82
|
end.join(', ')
|
80
83
|
@session._query_or_fail(q)
|
@@ -89,7 +92,7 @@ module Neo4j::Server
|
|
89
92
|
remove_properties(removed_keys) unless removed_keys.empty?
|
90
93
|
properties_to_set = properties.keys - removed_keys
|
91
94
|
return if properties_to_set.empty?
|
92
|
-
q = "
|
95
|
+
q = "#{match_start} SET " + properties_to_set.map do |k|
|
93
96
|
"n.`#{k}`= #{escape_value(properties[k])}"
|
94
97
|
end.join(',')
|
95
98
|
@session._query_or_fail(q)
|
@@ -101,14 +104,13 @@ module Neo4j::Server
|
|
101
104
|
if @props
|
102
105
|
@props[key.to_sym]
|
103
106
|
else
|
104
|
-
@session._query_or_fail("
|
107
|
+
@session._query_or_fail("#{match_start} RETURN n.`#{key}`", true)
|
105
108
|
end
|
106
109
|
end
|
107
110
|
|
108
111
|
# (see Neo4j::Node#labels)
|
109
112
|
def labels
|
110
|
-
@labels ||= @session._query_or_fail("
|
111
|
-
|
113
|
+
@labels ||= @session._query_or_fail("#{match_start} RETURN labels(n) as labels", true)
|
112
114
|
@labels.map(&:to_sym)
|
113
115
|
end
|
114
116
|
|
@@ -117,63 +119,40 @@ module Neo4j::Server
|
|
117
119
|
end
|
118
120
|
|
119
121
|
def add_label(*labels)
|
120
|
-
@session._query_or_fail("
|
122
|
+
@session._query_or_fail("#{match_start} SET n #{_cypher_label_list(labels)}")
|
121
123
|
end
|
122
124
|
|
123
125
|
def remove_label(*labels)
|
124
|
-
@session._query_or_fail("
|
126
|
+
@session._query_or_fail("#{match_start} REMOVE n #{_cypher_label_list(labels)}")
|
125
127
|
end
|
126
128
|
|
127
129
|
def set_label(*label_names)
|
128
|
-
|
129
|
-
to_keep = labels & label_as_symbols
|
130
|
-
to_remove = labels - to_keep
|
131
|
-
to_set = label_as_symbols - to_keep
|
132
|
-
|
133
|
-
# no change ?
|
134
|
-
return if to_set.empty? && to_remove.empty?
|
135
|
-
|
136
|
-
q = "START n=node(#{neo_id})"
|
137
|
-
q += " SET n #{_cypher_label_list(to_set)}" unless to_set.empty?
|
138
|
-
q += " REMOVE n #{_cypher_label_list(to_remove)}" unless to_remove.empty?
|
139
|
-
|
130
|
+
q = "#{match_start} #{remove_labels_if_needed} #{set_labels_if_needed(label_names)}"
|
140
131
|
@session._query_or_fail(q)
|
141
132
|
end
|
142
133
|
|
143
134
|
# (see Neo4j::Node#del)
|
144
135
|
def del
|
145
|
-
@session._query_or_fail("
|
146
|
-
@session._query_or_fail("
|
136
|
+
@session._query_or_fail("#{match_start} MATCH n-[r]-() DELETE r")
|
137
|
+
@session._query_or_fail("#{match_start} DELETE n")
|
147
138
|
end
|
139
|
+
|
148
140
|
alias_method :delete, :del
|
149
141
|
alias_method :destroy, :del
|
150
142
|
|
151
|
-
|
152
143
|
# (see Neo4j::Node#exist?)
|
153
144
|
def exist?
|
154
|
-
|
155
|
-
if (!response.error?)
|
156
|
-
return true
|
157
|
-
elsif (response.error_status =~ /EntityNotFound/)
|
158
|
-
return false
|
159
|
-
else
|
160
|
-
response.raise_error
|
161
|
-
end
|
145
|
+
@session._query("#{match_start} RETURN ID(n)").data.empty? ? false : true
|
162
146
|
end
|
163
147
|
|
164
|
-
|
165
148
|
# (see Neo4j::Node#node)
|
166
149
|
def node(match={})
|
167
|
-
|
168
|
-
raise "Expected to only find one relationship from node #{neo_id} matching #{match.inspect} but found #{result.count}" if result.count > 1
|
169
|
-
result.first
|
150
|
+
ensure_single_relationship { match(CypherNode, "p as result LIMIT 2", match) }
|
170
151
|
end
|
171
152
|
|
172
153
|
# (see Neo4j::Node#rel)
|
173
154
|
def rel(match={})
|
174
|
-
|
175
|
-
raise "Expected to only find one relationship from node #{neo_id} matching #{match.inspect} but found #{result.count}" if result.count > 1
|
176
|
-
result.first
|
155
|
+
ensure_single_relationship { match(CypherRelationship, "r as result LIMIT 2", match) }
|
177
156
|
end
|
178
157
|
|
179
158
|
# (see Neo4j::Node#rel?)
|
@@ -187,7 +166,6 @@ module Neo4j::Server
|
|
187
166
|
match(CypherNode, "p as result", match)
|
188
167
|
end
|
189
168
|
|
190
|
-
|
191
169
|
# (see Neo4j::Node#rels)
|
192
170
|
def rels(match = {dir: :both})
|
193
171
|
match(CypherRelationship, "r as result", match)
|
@@ -200,9 +178,9 @@ module Neo4j::Server
|
|
200
178
|
both: ->(rel) {"-#{rel}-"} }
|
201
179
|
|
202
180
|
cypher_rel = match[:type] ? "[r:`#{match[:type]}`]" : '[r]'
|
203
|
-
between_id = match[:between] ? "
|
181
|
+
between_id = match[:between] ? "MATCH (p) WHERE ID(p) = #{match[:between].neo_id}" : ""
|
204
182
|
dir_func = to_dir[match[:dir] || :both]
|
205
|
-
cypher = "
|
183
|
+
cypher = "#{match_start} #{between_id} MATCH (n)#{dir_func.call(cypher_rel)}(p) RETURN #{returns}"
|
206
184
|
r = @session._query(cypher)
|
207
185
|
r.raise_error if r.error?
|
208
186
|
_map_result(r)
|
@@ -211,5 +189,33 @@ module Neo4j::Server
|
|
211
189
|
def _map_result(r)
|
212
190
|
r.to_node_enumeration.map { |rel| rel.result }
|
213
191
|
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def remove_labels_if_needed
|
196
|
+
if labels.empty?
|
197
|
+
""
|
198
|
+
else
|
199
|
+
" REMOVE n #{_cypher_label_list(labels)}"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def set_labels_if_needed(label_names)
|
204
|
+
if label_names.empty?
|
205
|
+
""
|
206
|
+
else
|
207
|
+
" SET n #{_cypher_label_list(label_names.map(&:to_sym).uniq)}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def ensure_single_relationship(&block)
|
212
|
+
result = yield
|
213
|
+
raise "Expected to only find one relationship from node #{neo_id} matching #{match.inspect} but found #{result.count}" if result.count > 1
|
214
|
+
result.first
|
215
|
+
end
|
216
|
+
|
217
|
+
def match_start(identifier = 'n')
|
218
|
+
"MATCH (#{identifier}) WHERE ID(#{identifier}) = #{neo_id}"
|
219
|
+
end
|
214
220
|
end
|
215
221
|
end
|