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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 282dbe05a1e9fe442230e3fc5a4e0f7f7555b53e
4
- data.tar.gz: ece1c98b2675ccc0a4c6b20d993f8a7e11802a99
3
+ metadata.gz: 6f4890cd93db0d6c36f8208fc1e9389c587edaf2
4
+ data.tar.gz: dea68555caf4904ac68214b6cea915cb1c8462e9
5
5
  SHA512:
6
- metadata.gz: e0eb0f57bb90a510bbb08ee600e57560df6810196e693b9abba495e616a62f0e4ff682be5cc48d45160b2222653dc3e5bd0bbbbb0a2df5ca08a01273a422cd42
7
- data.tar.gz: 16f6b02085a574f7d303574db967f6b12ea42d447349428d5d82e18c579b34e4c4d27e3bd800a0dc0573cf39e42802fa1bc1b9dd43d4569b1908e494f3691525
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
- def escape_value(value)
5
- result = case value
6
- when String
7
- sanitized = sanitize_escape_sequences(value)
8
- "'#{escape_quotes(sanitized)}'"
9
- else
10
- value
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
- # Cypher Helper
38
- def cypher_prop_list(props)
39
- return "" unless props
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
- properties_to_set = props.reject {|k,v| v.nil? }
37
+ def sanitize_escape_sequences(s)
38
+ s.gsub SANITIZE_ESCAPED_REGEXP, EMPTY_PROPS
39
+ end
42
40
 
43
- list = properties_to_set.map{|k,v| "#{k} : #{escape_value(v)}"}.join(',')
44
- "{#{list}}"
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 do |column|
55
- column[/[^\.]+$/]
56
- end
59
+ response_body.columns.map { |column| column[/[^\.]+$/] }
57
60
  end
58
61
 
59
- end
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
@@ -1,5 +1,5 @@
1
1
  module Neo4j
2
2
  module Core
3
- VERSION = "3.0.8"
3
+ VERSION = "3.1.0"
4
4
  end
5
5
  end
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
- q = "START a=node(#{neo_id}), b=node(#{other_node.neo_id}) CREATE (a)-[r:`#{type}` #{cypher_prop_list(props)}]->(b) RETURN ID(r)"
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("START n=node(#{neo_id}) RETURN n")
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("START n=node(#{neo_id}) REMOVE n.`#{key}`")
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("START n=node(#{neo_id}) SET n.`#{key}` = { value }", false, value: value)
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("START n=node(#{neo_id}) SET n = { props }", false, {props: properties})
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 = "START n=node(#{neo_id}) REMOVE " + properties.map do |k|
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 = "START n=node(#{neo_id}) SET " + properties_to_set.map do |k|
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("START n=node(#{neo_id}) RETURN n.`#{key}`", true)
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("START n=node(#{neo_id}) RETURN labels(n) as labels", true)
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("START n=node(#{neo_id}) SET n #{_cypher_label_list(labels)}")
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("START n=node(#{neo_id}) REMOVE n #{_cypher_label_list(labels)}")
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
- label_as_symbols = label_names.map(&:to_sym)
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("START n = node(#{neo_id}) MATCH n-[r]-() DELETE r")
146
- @session._query_or_fail("START n = node(#{neo_id}) DELETE n")
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
- response = @session._query("START n=node(#{neo_id}) RETURN ID(n)")
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
- result = match(CypherNode, "p as result LIMIT 2", match)
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
- result = match(CypherRelationship, "r as result LIMIT 2", match)
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] ? ",p=node(#{match[:between].neo_id}) " : ""
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 = "START n=node(#{neo_id}) #{between_id} MATCH (n)#{dir_func.call(cypher_rel)}(p) RETURN #{returns}"
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