Skip to content

Commit 2df0560

Browse files
authored
Merge pull request #8 from doximity/MOFONETPROFILES-135-add-routing-header-for-query-vs-write
Mofonetprofiles 135 add routing header for query vs write
2 parents c77eda5 + 8f450eb commit 2df0560

23 files changed

+98
-26
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
Changelog
22
=========
33

4+
## v1.0.2 07/19/2022
5+
* Adds an access_mode setting to cypher queries
6+
47
## v1.0.1 05/24/2022
58
* Expand documentation configuration settings
69

Gemfile.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
PATH
22
remote: .
33
specs:
4-
neo4j-http (1.0.1)
4+
neo4j-http (1.0.2)
55
activesupport (>= 5.2)
66
faraday (< 2)
77
faraday-retry
88
faraday_middleware
9+
pry
910

1011
GEM
1112
remote: https://rubygems.org/
1213
specs:
13-
activesupport (7.0.3)
14+
activesupport (7.0.3.1)
1415
concurrent-ruby (~> 1.0, >= 1.0.2)
1516
i18n (>= 1.6, < 2)
1617
minitest (>= 5.1)
@@ -35,20 +36,20 @@ GEM
3536
faraday-em_synchrony (1.0.0)
3637
faraday-excon (1.1.0)
3738
faraday-httpclient (1.0.1)
38-
faraday-multipart (1.0.3)
39-
multipart-post (>= 1.2, < 3)
39+
faraday-multipart (1.0.4)
40+
multipart-post (~> 2)
4041
faraday-net_http (1.0.1)
4142
faraday-net_http_persistent (1.2.0)
4243
faraday-patron (1.0.0)
4344
faraday-rack (1.0.0)
4445
faraday-retry (1.0.3)
4546
faraday_middleware (1.2.0)
4647
faraday (~> 1.0)
47-
i18n (1.10.0)
48+
i18n (1.12.0)
4849
concurrent-ruby (~> 1.0)
4950
method_source (1.0.0)
50-
minitest (5.15.0)
51-
multipart-post (2.1.1)
51+
minitest (5.16.2)
52+
multipart-post (2.2.3)
5253
parallel (1.22.1)
5354
parser (3.1.1.0)
5455
ast (~> 2.4.1)
@@ -100,7 +101,6 @@ PLATFORMS
100101

101102
DEPENDENCIES
102103
neo4j-http!
103-
pry
104104
rake (~> 12.0)
105105
rspec (~> 3.0)
106106
standard

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The client is configured by default via a set of environment variables from [Neo
3333
* `NEO4J_DATABASE` - The database name to be used when connecting. By default this will be `nil`.
3434
* `NEO4J_HTTP_USER_AGENT` - The user agent name provided in the request - defaults to `"Ruby Neo4j Http Client"`
3535
* `NEO4J_REQUEST_TIMEOUT_IN_SECONDS` - The number of seconds for the http request to time out if provided - defaults to `nil`
36+
* `ACCESS_MODE` - "WRITE", or "READ" for read only instances of Neo4j clients - defaults to `"WRITE"`
3637

3738
These configuration values can also be set during initalization, and take precedence over the environment variables:
3839

@@ -44,6 +45,7 @@ Neo4j::Http.configure do |config|
4445
config.database_name = nil
4546
config.user_agent = "Ruby Neo4j Http Client"
4647
config.request_timeout_in_seconds = nil
48+
config.access_mode = "WRITE"
4749
end
4850
```
4951

lib/neo4j/http/configuration.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class Configuration
99
attr_accessor :uri
1010
attr_accessor :user
1111
attr_accessor :user_agent
12+
attr_accessor :access_mode
1213

1314
def initialize(options = ENV)
1415
@uri = options.fetch("NEO4J_URL", "http://localhost:7474")
@@ -17,6 +18,7 @@ def initialize(options = ENV)
1718
@database_name = options.fetch("NEO4J_DATABASE", nil)
1819
@user_agent = options.fetch("NEO4J_HTTP_USER_AGENT", "Ruby Neo4j Http Client")
1920
@request_timeout_in_seconds = options.fetch("NEO4J_REQUEST_TIMEOUT_IN_SECONDS", nil)
21+
@access_mode = options.fetch("NEO4J_ACCESS_MODE", "WRITE")
2022
end
2123

2224
# https://neo4j.com/developer/manage-multiple-databases/

lib/neo4j/http/cypher_client.rb

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,47 @@ def self.default
1212
@default ||= new(Neo4j::Http.config)
1313
end
1414

15-
def initialize(configuration)
15+
def initialize(configuration, injected_connection = nil)
1616
@configuration = configuration
17+
@injected_connection = injected_connection
1718
end
1819

20+
# Executes a cypher query, passing in the cypher statement, with parameters as an optional hash
21+
# e.g. Neo4j::Http::Cypherclient.execute_cypher("MATCH (n { foo: $foo }) LIMIT 1 RETURN n", { foo: "bar" })
1922
def execute_cypher(cypher, parameters = {})
23+
# By default the access mode is set to "WRITE", but can be set to "READ"
24+
# for improved routing performance on read only queries
25+
access_mode = parameters.delete(:access_mode) || @configuration.access_mode
26+
2027
request_body = {
2128
statements: [
22-
{statement: cypher,
23-
parameters: parameters.as_json}
29+
{
30+
statement: cypher,
31+
parameters: parameters.as_json
32+
}
2433
]
2534
}
2635

27-
response = connection.post(transaction_path, request_body)
36+
@connection = @injected_connection || connection(access_mode)
37+
response = @connection.post(transaction_path, request_body)
2838
results = check_errors!(cypher, response, parameters)
2939

3040
Neo4j::Http::Results.parse(results&.first || {})
3141
end
3242

33-
def connection
34-
build_connection
43+
def connection(access_mode)
44+
build_connection(access_mode)
3545
end
3646

3747
protected
3848

3949
delegate :auth_token, :transaction_path, to: :@configuration
4050
def check_errors!(cypher, response, parameters)
4151
raise Neo4j::Http::Errors::InvalidConnectionUrl, response.status if response.status == 404
52+
if response.body["errors"].any? { |error| error["message"][/Routing WRITE queries is not supported/] }
53+
raise Neo4j::Http::Errors::ReadOnlyError
54+
end
55+
4256
body = response.body || {}
4357
errors = body.fetch("errors", [])
4458
return body.fetch("results", {}) unless errors.present?
@@ -61,8 +75,10 @@ def find_error_class(code)
6175
Neo4j::Http::Errors::Neo4jCodedError
6276
end
6377

64-
def build_connection
65-
Faraday.new(url: @configuration.uri, headers: build_http_headers, request: build_request_options) do |f|
78+
def build_connection(access_mode)
79+
# https://neo4j.com/docs/http-api/current/actions/transaction-configuration/
80+
headers = build_http_headers.merge({"access-mode" => access_mode})
81+
Faraday.new(url: @configuration.uri, headers: headers, request: build_request_options) do |f|
6682
f.request :json # encode req bodies as JSON
6783
f.request :retry # retry transient failures
6884
f.response :json # decode response bodies as JSON

lib/neo4j/http/errors.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Errors
44
Neo4jError = Class.new(StandardError)
55
InvalidConnectionUrl = Class.new(Neo4jError)
66
Neo4jCodedError = Class.new(Neo4jError)
7+
ReadOnlyError = Class.new(Neo4jError)
78

89
# These are specific Errors Neo4j can raise
910
module Neo

lib/neo4j/http/node_client.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,16 @@ def delete_node(node)
4141
def find_node_by(label:, **attributes)
4242
selectors = attributes.map { |key, value| "#{key}: $attributes.#{key}" }.join(", ")
4343
cypher = "MATCH (node:#{label} { #{selectors} }) RETURN node LIMIT 1"
44-
results = @cypher_client.execute_cypher(cypher, attributes: attributes)
44+
results = @cypher_client.execute_cypher(cypher, attributes: attributes.merge(access_mode: "READ"))
4545
return if results.empty?
46+
4647
results.first&.fetch("node")
4748
end
4849

4950
def find_nodes_by(label:, attributes:, limit: 100)
5051
selectors = build_selectors(attributes)
5152
cypher = "MATCH (node:#{label}) where #{selectors} RETURN node LIMIT #{limit}"
52-
results = @cypher_client.execute_cypher(cypher, attributes: attributes)
53+
results = @cypher_client.execute_cypher(cypher, attributes: attributes.merge(access_mode: "READ"))
5354
results.map { |result| result["node"] }
5455
end
5556

lib/neo4j/http/relationship_client.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@ def find_relationship(from:, relationship:, to:)
5252
RETURN from, to, relationship
5353
CYPHER
5454

55-
results = @cypher_client.execute_cypher(cypher, from: from, to: to, relationship: relationship)
55+
results = @cypher_client.execute_cypher(
56+
cypher,
57+
from: from,
58+
to: to,
59+
relationship: relationship,
60+
access_mode: "READ"
61+
)
5662
results&.first
5763
end
5864

lib/neo4j/http/results.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ module Neo4j
44
module Http
55
class Results
66
# Example result set:
7-
# [{"columns"=>["n"], "data"=>[{"row"=>[{"name"=>"Foo", "uuid"=>"8c7dcfda-d848-4937-a91a-2e6debad2dd6"}], "meta"=>[{"id"=>242, "type"=>"node", "deleted"=>false}]}]}]
7+
# [{"columns"=>["n"],
8+
# "data"=>
9+
# [{"row"=>[{"name"=>"Foo", "uuid"=>"8c7dcfda-d848-4937-a91a-2e6debad2dd6"}],
10+
# "meta"=>[{"id"=>242, "type"=>"node", "deleted"=>false}]}]}]
811
#
912
def self.parse(results)
1013
columns = results["columns"]

lib/neo4j/http/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Neo4j
22
module Http
3-
VERSION = "1.0.1"
3+
VERSION = "1.0.2"
44
end
55
end

0 commit comments

Comments
 (0)