neo4j-core 3.0.8 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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