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 +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
|