artemis 0.6.0 → 0.7.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/.github/workflows/ruby.yml +14 -3
- data/.gitignore +1 -0
- data/Appraisals +6 -0
- data/CHANGELOG.md +20 -7
- data/README.md +67 -2
- data/gemfiles/rails_70.gemfile +13 -0
- data/gemfiles/rails_edge.gemfile +1 -0
- data/lib/artemis/adapters/curb_adapter.rb +16 -6
- data/lib/artemis/adapters/multi_domain_adapter.rb +15 -0
- data/lib/artemis/adapters/net_http_adapter.rb +25 -16
- data/lib/artemis/adapters/net_http_persistent_adapter.rb +6 -0
- data/lib/artemis/adapters/test_adapter.rb +21 -2
- data/lib/artemis/client.rb +40 -6
- data/lib/artemis/railtie.rb +14 -6
- data/lib/artemis/version.rb +1 -1
- data/lib/generators/artemis/query/query_generator.rb +1 -1
- data/lib/generators/artemis/query/templates/query.graphql +2 -2
- data/spec/adapters_spec.rb +123 -12
- data/spec/autoloading_spec.rb +4 -4
- data/spec/client_spec.rb +38 -2
- data/spec/fixtures/metaphysics/{_artist_fragment.graphql → _artist_fields.graphql} +0 -0
- data/spec/fixtures/metaphysics/artists.graphql +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b64a8fd49d4d32ab4fd0dbda0f20005b68129fc1eee01de0f525424a19d1d28
|
|
4
|
+
data.tar.gz: eae9f56e2f7bbd832278cf1546c931824ea5f7bfa969e28cdf9dbe2e8a6e4ab6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 061e522011314b8f3a712a1a13c504653e7b7979e17ff239298d98742f08ae3fea14cd9772970dd72dd1c9e361572d6c70b076f3b0e4ee552f9d1fc1e2a7e69d
|
|
7
|
+
data.tar.gz: 2d1fec853399e9ec52b74e3c5c7eae407a9affe39222f7719d993f098ef839cd2da72dfbb353f967c025c2d72b464dbdc3f56dadfa4e596b2d9ea32c43d6119f
|
data/.github/workflows/ruby.yml
CHANGED
|
@@ -9,12 +9,13 @@ jobs:
|
|
|
9
9
|
strategy:
|
|
10
10
|
matrix:
|
|
11
11
|
ruby_version:
|
|
12
|
+
- '3.1'
|
|
12
13
|
- '3.0'
|
|
13
14
|
- '2.7'
|
|
14
15
|
- '2.6'
|
|
15
|
-
- '2.5'
|
|
16
16
|
# - 'jruby-9.2.17.0'
|
|
17
17
|
gemfile:
|
|
18
|
+
- gemfiles/rails_70.gemfile
|
|
18
19
|
- gemfiles/rails_61.gemfile
|
|
19
20
|
- gemfiles/rails_60.gemfile
|
|
20
21
|
- gemfiles/rails_52.gemfile
|
|
@@ -22,6 +23,16 @@ jobs:
|
|
|
22
23
|
- gemfiles/rails_50.gemfile
|
|
23
24
|
# - gemfiles/rails_edge.gemfile
|
|
24
25
|
exclude:
|
|
26
|
+
- ruby_version: '3.1'
|
|
27
|
+
gemfile: gemfiles/rails_61.gemfile
|
|
28
|
+
- ruby_version: '3.1'
|
|
29
|
+
gemfile: gemfiles/rails_60.gemfile
|
|
30
|
+
- ruby_version: '3.1'
|
|
31
|
+
gemfile: gemfiles/rails_52.gemfile
|
|
32
|
+
- ruby_version: '3.1'
|
|
33
|
+
gemfile: gemfiles/rails_51.gemfile
|
|
34
|
+
- ruby_version: '3.1'
|
|
35
|
+
gemfile: gemfiles/rails_50.gemfile
|
|
25
36
|
- ruby_version: '3.0'
|
|
26
37
|
gemfile: gemfiles/rails_52.gemfile
|
|
27
38
|
- ruby_version: '3.0'
|
|
@@ -34,10 +45,10 @@ jobs:
|
|
|
34
45
|
gemfile: gemfiles/rails_51.gemfile
|
|
35
46
|
- ruby_version: '2.7'
|
|
36
47
|
gemfile: gemfiles/rails_50.gemfile
|
|
48
|
+
- ruby_version: '2.6'
|
|
49
|
+
gemfile: gemfiles/rails_70.gemfile
|
|
37
50
|
# - ruby_version: '2.6'
|
|
38
51
|
# gemfile: gemfiles/rails_edge.gemfile
|
|
39
|
-
# - ruby_version: '2.5'
|
|
40
|
-
# gemfile: gemfiles/rails_edge.gemfile
|
|
41
52
|
runs-on: ubuntu-18.04
|
|
42
53
|
env:
|
|
43
54
|
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
data/.gitignore
CHANGED
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
## v0.6.0
|
|
2
1
|
|
|
3
|
-
|
|
2
|
+
## v0.7.0
|
|
3
|
+
|
|
4
|
+
_<sup>unreleased</sup>_
|
|
5
|
+
|
|
6
|
+
#### Features
|
|
7
|
+
|
|
8
|
+
- Add support for Ruby 3.1 and Rails 7.0
|
|
9
|
+
- Add support for [the Multiplex query](https://graphql-ruby.org/queries/multiplex.html)
|
|
10
|
+
- Do not allow the usage of the `multi_domain` adapter to be nested ([<tt>9b7b520</tt>](https://github.com/yuki24/artemis/commit/9b7b5202c9fbe424d4ca22f05dc9c9759b5202c3))
|
|
11
|
+
- Do not require fragment files to end with `_fragment.graphql` ([<tt>3c6c0fa</tt>](https://github.com/yuki24/artemis/commit/3c6c0fa))
|
|
12
|
+
- Allow for overriding the namespace used for resolving graphql file paths ([<tt>bd18762</tt>](https://github.com/yuki24/artemis/commit/bd18762))
|
|
13
|
+
|
|
14
|
+
## [v0.6.0](https://github.com/yuki24/artemis/tree/v0.6.0)
|
|
15
|
+
|
|
16
|
+
_<sup>released at 2021-09-03 04:17:55 UTC</sup>_
|
|
4
17
|
|
|
5
18
|
#### Features
|
|
6
19
|
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
- Add support for Ruby 3.0 and Rails 6.0, 6.1
|
|
21
|
+
- Add the multi domain adapter ([<tt>744b8ea</tt>](https://github.com/yuki24/artemis/commit/744b8ea35795b4e6cc4fdc1ebb63dd9a4e9819f0))
|
|
9
22
|
|
|
10
23
|
#### Fixes
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
- ~~Generate fixture YAML files on `rails g artemis:query queryName` ([#78](https://github.com/yuki24/artemis/pull/78))~~ Removed due to a bug for now.
|
|
26
|
+
- Address warnings from Ruby 2.7 ([<tt>408adcb</tt>](https://github.com/yuki24/artemis/commit/408adcb3f39912f7afb7b3690a52f1d593662b7b))
|
|
27
|
+
- Avoid crashing when config/graphql.yml does not exist ([@dlackty](https://github.com/dlackty), [#76](https://github.com/yuki24/artemis/pull/76))
|
|
15
28
|
|
|
16
29
|
## [v0.5.2](https://github.com/yuki24/artemis/tree/v0.5.2)
|
|
17
30
|
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Artemis [](https://github.com/yuki24/artemis/actions/workflows/ruby.yml) [](https://rubygems.org/gems/artemis)
|
|
2
2
|
|
|
3
3
|
Artemis is a GraphQL client that is designed to fit well on Rails.
|
|
4
4
|
|
|
@@ -92,6 +92,32 @@ Artemis assumes that the files related to GraphQL are organized in a certain way
|
|
|
92
92
|
└──vendor/graphql/schema/artsy.json
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### Fragments
|
|
96
|
+
Fragments are defined in defined in a standard way in a file named `_artwork_fragment.graphql` with the standard convention:
|
|
97
|
+
|
|
98
|
+
```graphql
|
|
99
|
+
fragment on Artwork {
|
|
100
|
+
id,
|
|
101
|
+
name,
|
|
102
|
+
artist_id
|
|
103
|
+
# other artwork fields here
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The way of calling an Artemis fragment on other queries or models is with a **Rails convention**. Let us suppose we have the Artist model and its corresponding artwork. The way of nesting or calling the artwork on the artist model would look like this:
|
|
108
|
+
|
|
109
|
+
```graphql
|
|
110
|
+
fragment on Artist {
|
|
111
|
+
id,
|
|
112
|
+
name,
|
|
113
|
+
artworks {
|
|
114
|
+
...Artsy::ArtworkFragment
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Where `Artsy` is the name of the folder/module.
|
|
120
|
+
|
|
95
121
|
## Callbacks
|
|
96
122
|
|
|
97
123
|
You can use the `before_execute` callback to intercept outgoing requests and the `after_execute` callback to observe the
|
|
@@ -121,6 +147,44 @@ class Artsy < Artemis::Client
|
|
|
121
147
|
end
|
|
122
148
|
```
|
|
123
149
|
|
|
150
|
+
## Multi domain support
|
|
151
|
+
|
|
152
|
+
Services like Shopify provide
|
|
153
|
+
[a different endpoint per customer](https://shopify.dev/api/admin/graphql/reference#graphql-endpoint) (e.g.
|
|
154
|
+
`https://{shop}.myshopify.com`). In order to switch the endpoint on a per-request basis, you will have to use the
|
|
155
|
+
`:multi_domain` adapter. This is a wrapper adapter that relies on an actual HTTP adapter such as `:net_http` and
|
|
156
|
+
`:curb` so that e.g. it can maintain multiple connections for each endpoint if necessary. This could be configured
|
|
157
|
+
as shown below:
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
default: &default
|
|
161
|
+
# Specify the :multi_domain adapter:
|
|
162
|
+
adapter: :multi_domain
|
|
163
|
+
|
|
164
|
+
# Other configurations such as `timeout` and `pool_size` are passed down to the underlying adapter:
|
|
165
|
+
timeout: 10
|
|
166
|
+
pool_size: 25
|
|
167
|
+
|
|
168
|
+
# Additional adapter-specific configurations could be configured as `adapter_options`:
|
|
169
|
+
adapter_options:
|
|
170
|
+
# Here you can configure the actual adapter to use. By default, it is set to :net_http. Available adapters are
|
|
171
|
+
# :net_http, :net_http_persistent, :curb, and :test. You can not nest the use of the `:multi_domain` adapter.
|
|
172
|
+
adapter: :net_http
|
|
173
|
+
|
|
174
|
+
development:
|
|
175
|
+
shopify:
|
|
176
|
+
<<: *default
|
|
177
|
+
|
|
178
|
+
...
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Upon making a request you will also have to specify the `url` option:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
# Makes a request to https://myawesomeshop.myshopify.com:
|
|
185
|
+
Shopify.with_context(url: "https://myawesomeshop.myshopify.com").product(id: "...")
|
|
186
|
+
```
|
|
187
|
+
|
|
124
188
|
## Configuration
|
|
125
189
|
|
|
126
190
|
You can configure the GraphQL client using the following options. Those configurations are found in the
|
|
@@ -143,7 +207,8 @@ There are four adapter options available. Choose the adapter that best fits on y
|
|
|
143
207
|
| `:curb` | HTTP/1.1, **HTTP/2** | **Yes** | **Fastest** | [`curb 0.9.6+`][curb]<br>[`libcurl 7.64.0+`][curl]<br>[`nghttp2 1.0.0+`][nghttp]
|
|
144
208
|
| `:net_http` (default) | HTTP/1.1 only | No | Slow | **None**
|
|
145
209
|
| `:net_http_persistent` | HTTP/1.1 only | **Yes** | **Fast** | [`net-http-persistent 3.0.0+`][nhp]
|
|
146
|
-
| `:
|
|
210
|
+
| `:multi_domain` | See [multi domain support](#multi-domain-support)
|
|
211
|
+
| `:test` | See [Testing](#testing)
|
|
147
212
|
|
|
148
213
|
#### Third-party adapters
|
|
149
214
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "pry"
|
|
6
|
+
gem "pry-byebug", platforms: :mri
|
|
7
|
+
gem "curb", ">= 0.9.6"
|
|
8
|
+
gem "webrick"
|
|
9
|
+
gem "rails", "~> 7.0.0"
|
|
10
|
+
gem "railties", "~> 7.0.0"
|
|
11
|
+
gem "activesupport", "~> 7.0.0"
|
|
12
|
+
|
|
13
|
+
gemspec path: "../"
|
data/gemfiles/rails_edge.gemfile
CHANGED
|
@@ -20,18 +20,28 @@ module Artemis
|
|
|
20
20
|
@multi.pipeline = Curl::CURLPIPE_MULTIPLEX if defined?(Curl::CURLPIPE_MULTIPLEX)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def
|
|
24
|
-
|
|
23
|
+
def multiplex(queries, context: {})
|
|
24
|
+
make_request({ _json: queries }, context)
|
|
25
|
+
end
|
|
25
26
|
|
|
27
|
+
def execute(document:, operation_name: nil, variables: {}, context: {})
|
|
26
28
|
body = {}
|
|
27
29
|
body["query"] = document.to_query_string
|
|
28
30
|
body["variables"] = variables if variables.any?
|
|
29
31
|
body["operationName"] = operation_name if operation_name
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
make_request(body, context)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def make_request(body, context)
|
|
39
|
+
easy = Curl::Easy.new(uri.to_s)
|
|
40
|
+
|
|
41
|
+
easy.timeout = timeout
|
|
42
|
+
easy.multi = multi
|
|
43
|
+
easy.headers = DEFAULT_HEADERS.merge(headers(context))
|
|
44
|
+
easy.post_body = JSON.generate(body)
|
|
35
45
|
|
|
36
46
|
if defined?(Curl::CURLPIPE_MULTIPLEX)
|
|
37
47
|
# This ensures libcurl waits for the connection to reveal if it is
|
|
@@ -8,6 +8,10 @@ module Artemis
|
|
|
8
8
|
attr_reader :adapter
|
|
9
9
|
|
|
10
10
|
def initialize(_uri, service_name: , timeout: , pool_size: , adapter_options: {})
|
|
11
|
+
if adapter_options[:adapter] == :multi_domain
|
|
12
|
+
raise ArgumentError, "You can not use the :multi_domain adapter with the :multi_domain adapter."
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
@connection_by_url = {}
|
|
12
16
|
@service_name = service_name.to_s
|
|
13
17
|
@timeout = timeout
|
|
@@ -16,6 +20,17 @@ module Artemis
|
|
|
16
20
|
@mutex_for_connection = Mutex.new
|
|
17
21
|
end
|
|
18
22
|
|
|
23
|
+
def multiplex(queries, context: {})
|
|
24
|
+
url = context[:url]
|
|
25
|
+
|
|
26
|
+
if url.nil?
|
|
27
|
+
raise ArgumentError, 'The MultiDomain adapter requires a url on every request. Please specify a url with a context: ' \
|
|
28
|
+
'Client.multiplex(url: "https://awesomeshop.domain.conm") { ... }'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
connection_for_url(url).multiplex(queries, context: context)
|
|
32
|
+
end
|
|
33
|
+
|
|
19
34
|
# Makes an HTTP request for GraphQL query.
|
|
20
35
|
def execute(document:, operation_name: nil, variables: {}, context: {})
|
|
21
36
|
url = context[:url]
|
|
@@ -9,20 +9,39 @@ require 'artemis/exceptions'
|
|
|
9
9
|
module Artemis
|
|
10
10
|
module Adapters
|
|
11
11
|
class NetHttpAdapter < AbstractAdapter
|
|
12
|
+
def multiplex(queries, context: {})
|
|
13
|
+
make_request({ _json: queries }, context)
|
|
14
|
+
end
|
|
15
|
+
|
|
12
16
|
# Makes an HTTP request for GraphQL query.
|
|
13
17
|
def execute(document:, operation_name: nil, variables: {}, context: {})
|
|
14
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
|
15
|
-
|
|
16
|
-
request.basic_auth(uri.user, uri.password) if uri.user || uri.password
|
|
17
|
-
|
|
18
|
-
DEFAULT_HEADERS.merge(headers(context)).each { |name, value| request[name] = value }
|
|
19
|
-
|
|
20
18
|
body = {}
|
|
21
19
|
body["query"] = document.to_query_string
|
|
22
20
|
body["variables"] = variables if variables.any?
|
|
23
21
|
body["operationName"] = operation_name if operation_name
|
|
22
|
+
|
|
23
|
+
make_request(body, context)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns a fresh Net::HTTP object that creates a new connection.
|
|
27
|
+
def connection
|
|
28
|
+
Net::HTTP.new(uri.host, uri.port).tap do |client|
|
|
29
|
+
client.use_ssl = uri.scheme == "https"
|
|
30
|
+
client.open_timeout = timeout
|
|
31
|
+
client.read_timeout = timeout
|
|
32
|
+
client.write_timeout = timeout if client.respond_to?(:write_timeout=)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def make_request(body, context)
|
|
39
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
40
|
+
request.basic_auth(uri.user, uri.password) if uri.user || uri.password
|
|
24
41
|
request.body = JSON.generate(body)
|
|
25
42
|
|
|
43
|
+
DEFAULT_HEADERS.merge(headers(context)).each { |name, value| request[name] = value }
|
|
44
|
+
|
|
26
45
|
response = connection.request(request)
|
|
27
46
|
|
|
28
47
|
case response.code.to_i
|
|
@@ -34,16 +53,6 @@ module Artemis
|
|
|
34
53
|
{ "errors" => [{ "message" => "#{response.code} #{response.message}" }] }
|
|
35
54
|
end
|
|
36
55
|
end
|
|
37
|
-
|
|
38
|
-
# Returns a fresh Net::HTTP object that creates a new connection.
|
|
39
|
-
def connection
|
|
40
|
-
Net::HTTP.new(uri.host, uri.port).tap do |client|
|
|
41
|
-
client.use_ssl = uri.scheme == "https"
|
|
42
|
-
client.open_timeout = timeout
|
|
43
|
-
client.read_timeout = timeout
|
|
44
|
-
client.write_timeout = timeout if client.respond_to?(:write_timeout=)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
56
|
end
|
|
48
57
|
end
|
|
49
58
|
end
|
|
@@ -12,12 +12,25 @@ module Artemis
|
|
|
12
12
|
self.responses = []
|
|
13
13
|
|
|
14
14
|
Request = Struct.new(:document, :operation_name, :variables, :context)
|
|
15
|
+
Multiplex = Struct.new(:queries, :context)
|
|
15
16
|
|
|
16
|
-
private_constant :Request
|
|
17
|
+
private_constant :Request, :Multiplex
|
|
17
18
|
|
|
18
19
|
def initialize(*)
|
|
19
20
|
end
|
|
20
21
|
|
|
22
|
+
def multiplex(queries, context: {})
|
|
23
|
+
self.requests << Multiplex.new(queries, context)
|
|
24
|
+
|
|
25
|
+
queries.map do |query|
|
|
26
|
+
result = responses.detect do |mock|
|
|
27
|
+
query[:operationName] == mock.operation_name && (mock.arguments == :__unspecified__ || query[:variables] == mock.arguments)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result&.data || fake_response
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
21
34
|
def execute(**arguments)
|
|
22
35
|
self.requests << Request.new(*arguments.values_at(:document, :operation_name, :variables, :context))
|
|
23
36
|
|
|
@@ -25,7 +38,13 @@ module Artemis
|
|
|
25
38
|
arguments[:operation_name] == mock.operation_name && (mock.arguments == :__unspecified__ || arguments[:variables] == mock.arguments)
|
|
26
39
|
end
|
|
27
40
|
|
|
28
|
-
response&.data ||
|
|
41
|
+
response&.data || fake_response
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def fake_response
|
|
47
|
+
{
|
|
29
48
|
'data' => { 'test' => 'data' },
|
|
30
49
|
'errors' => [],
|
|
31
50
|
'extensions' => {}
|
data/lib/artemis/client.rb
CHANGED
|
@@ -167,17 +167,19 @@ module Artemis
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
def resolve_graphql_file_path(filename, fragment: false)
|
|
170
|
-
|
|
171
|
-
filename = filename.to_s.underscore
|
|
170
|
+
filename = filename.to_s.underscore
|
|
172
171
|
|
|
173
172
|
graphql_file_paths.detect do |path|
|
|
174
|
-
path.end_with?("#{namespace}/#{filename}.graphql") ||
|
|
175
|
-
(fragment && filename.end_with?('fragment') && path.end_with?("#{namespace}/_#{filename}.graphql"))
|
|
173
|
+
path.end_with?("#{namespace}/#{filename}.graphql") || (fragment && path.end_with?("#{namespace}/_#{filename}.graphql"))
|
|
176
174
|
end
|
|
177
175
|
end
|
|
178
176
|
|
|
179
177
|
def graphql_file_paths
|
|
180
|
-
@graphql_file_paths ||= query_paths.flat_map {|path| Dir["#{path}/#{
|
|
178
|
+
@graphql_file_paths ||= query_paths.flat_map {|path| Dir["#{path}/#{namespace}/*.graphql"] }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def namespace
|
|
182
|
+
name.underscore
|
|
181
183
|
end
|
|
182
184
|
|
|
183
185
|
def preload!
|
|
@@ -218,6 +220,19 @@ module Artemis
|
|
|
218
220
|
new(default_context).execute(query, context: context, **arguments)
|
|
219
221
|
end
|
|
220
222
|
|
|
223
|
+
def multiplex(**context, &block)
|
|
224
|
+
queue = MultiplexQueue.new
|
|
225
|
+
wrapped_executor = Executor.new(queue, callbacks, default_context.deep_merge(context))
|
|
226
|
+
api_client = ::GraphQL::Client.new(schema: endpoint.schema, execute: wrapped_executor)
|
|
227
|
+
|
|
228
|
+
service_client = new
|
|
229
|
+
service_client.instance_variable_set(:@client, api_client)
|
|
230
|
+
|
|
231
|
+
block.call(service_client)
|
|
232
|
+
|
|
233
|
+
connection.multiplex(queue.queries, context: context)
|
|
234
|
+
end
|
|
235
|
+
|
|
221
236
|
private
|
|
222
237
|
|
|
223
238
|
# Looks up the GraphQL file that matches the given +const_name+ and sets it to a constant. If the files it not
|
|
@@ -345,6 +360,25 @@ module Artemis
|
|
|
345
360
|
end
|
|
346
361
|
end
|
|
347
362
|
|
|
348
|
-
|
|
363
|
+
class MultiplexQueue
|
|
364
|
+
attr_reader :queries
|
|
365
|
+
|
|
366
|
+
def initialize
|
|
367
|
+
@queries = []
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def execute(document:, operation_name: nil, variables: {}, context: {}) #:nodoc:
|
|
371
|
+
@queries << {
|
|
372
|
+
query: document.to_query_string,
|
|
373
|
+
variables: variables.presence || {},
|
|
374
|
+
operationName: operation_name,
|
|
375
|
+
context: context
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
{}
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private_constant :Callbacks, :Executor, :MultiplexQueue
|
|
349
383
|
end
|
|
350
384
|
end
|
data/lib/artemis/railtie.rb
CHANGED
|
@@ -35,10 +35,9 @@ module Artemis
|
|
|
35
35
|
files_to_watch = Artemis::Client.query_paths.map {|path| [path, config.artemis.graphql_extentions] }.to_h
|
|
36
36
|
|
|
37
37
|
app.reloaders << ActiveSupport::FileUpdateChecker.new([], files_to_watch) do
|
|
38
|
-
|
|
39
|
-
endpoint_names.each do |endpoint_name|
|
|
38
|
+
Artemis.config_for_graphql(app).each_key do |endpoint_name|
|
|
40
39
|
Artemis::Client.query_paths.each do |path|
|
|
41
|
-
FileUtils.touch("#{path}/#{endpoint_name}.rb")
|
|
40
|
+
FileUtils.touch("#{path}/#{endpoint_name}.rb") if File.exist?("#{path}/#{endpoint_name}.rb")
|
|
42
41
|
end
|
|
43
42
|
end
|
|
44
43
|
end
|
|
@@ -48,9 +47,12 @@ module Artemis
|
|
|
48
47
|
initializer 'graphql.client.load_config' do |app|
|
|
49
48
|
if Pathname.new("#{app.paths["config"].existent.first}/graphql.yml").exist?
|
|
50
49
|
Artemis.config_for_graphql(app).each do |endpoint_name, options|
|
|
51
|
-
Artemis::GraphQLEndpoint
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
Artemis::GraphQLEndpoint
|
|
51
|
+
.register!(
|
|
52
|
+
endpoint_name,
|
|
53
|
+
schema_path: app.root.join(config.artemis.schema_path, "#{endpoint_name}.json").to_s,
|
|
54
|
+
**options.symbolize_keys
|
|
55
|
+
)
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
end
|
|
@@ -58,6 +60,12 @@ module Artemis
|
|
|
58
60
|
initializer 'graphql.client.preload', after: 'graphql.client.load_config' do |app|
|
|
59
61
|
if app.config.eager_load && app.config.cache_classes
|
|
60
62
|
Artemis::GraphQLEndpoint.registered_services.each do |endpoint_name|
|
|
63
|
+
begin
|
|
64
|
+
require endpoint_name # Rails 7.0 requires this.
|
|
65
|
+
rescue LoadError
|
|
66
|
+
# no-op...
|
|
67
|
+
end
|
|
68
|
+
|
|
61
69
|
endpoint_name.camelize.constantize.preload!
|
|
62
70
|
end
|
|
63
71
|
end
|
data/lib/artemis/version.rb
CHANGED
|
@@ -37,7 +37,7 @@ class Artemis::QueryGenerator < Rails::Generators::Base
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def target_query
|
|
40
|
-
schema.
|
|
40
|
+
schema.query.fields[query_type] ||
|
|
41
41
|
raise(GraphQL::Schema::Finder::MemberNotFoundError, "Could not find type `#{query_type}` in schema.")
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
query<%= arguments.present?
|
|
2
|
-
<%= target_query.name %><%= arguments.present?
|
|
1
|
+
query<%= arguments.present? ? "(#{ arguments.map {|name, type| "$#{name}: #{type.type.to_type_signature}" }.join(", ") })" : "" %> {
|
|
2
|
+
<%= target_query.name %><%= arguments.present? ? "(#{ arguments.map {|name, type| "#{name}: $#{name}" }.join(", ") })" : "" %> {
|
|
3
3
|
# Add fields here...
|
|
4
4
|
}
|
|
5
5
|
}
|
data/spec/adapters_spec.rb
CHANGED
|
@@ -6,7 +6,7 @@ describe 'Adapters' do
|
|
|
6
6
|
FakeServer = ->(env) {
|
|
7
7
|
case env['PATH_INFO']
|
|
8
8
|
when '/slow_server'
|
|
9
|
-
sleep
|
|
9
|
+
sleep 2.1
|
|
10
10
|
|
|
11
11
|
[200, {}, ['{}']]
|
|
12
12
|
when '/500'
|
|
@@ -15,6 +15,9 @@ describe 'Adapters' do
|
|
|
15
15
|
body = {
|
|
16
16
|
data: {
|
|
17
17
|
body: "Endpoint switched.",
|
|
18
|
+
headers: env.select {|key, val| key.match("^HTTP.*|^CONTENT.*|^AUTHORIZATION.*") }
|
|
19
|
+
.collect {|key, val| [key.gsub(/^HTTP_/, ''), val.downcase] }
|
|
20
|
+
.to_h,
|
|
18
21
|
},
|
|
19
22
|
errors: [],
|
|
20
23
|
extensions: {}
|
|
@@ -22,18 +25,35 @@ describe 'Adapters' do
|
|
|
22
25
|
|
|
23
26
|
[200, {}, [body]]
|
|
24
27
|
else
|
|
25
|
-
|
|
26
|
-
data: {
|
|
27
|
-
body: JSON.parse(env['rack.input'].read),
|
|
28
|
-
headers: env.select {|key, val| key.match("^HTTP.*|^CONTENT.*|^AUTHORIZATION.*") }
|
|
29
|
-
.collect {|key, val| [key.gsub(/^HTTP_/, ''), val.downcase] }
|
|
30
|
-
.to_h,
|
|
31
|
-
},
|
|
32
|
-
errors: [],
|
|
33
|
-
extensions: {}
|
|
34
|
-
}.to_json
|
|
28
|
+
request_body = JSON.parse(env['rack.input'].read)
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
response_body = if request_body['_json']
|
|
31
|
+
request_body['_json'].map do |query|
|
|
32
|
+
{
|
|
33
|
+
data: {
|
|
34
|
+
body: query,
|
|
35
|
+
headers: env.select {|key, val| key.match("^HTTP.*|^CONTENT.*|^AUTHORIZATION.*") }
|
|
36
|
+
.collect {|key, val| [key.gsub(/^HTTP_/, ''), val.downcase] }
|
|
37
|
+
.to_h,
|
|
38
|
+
},
|
|
39
|
+
errors: [],
|
|
40
|
+
extensions: {}
|
|
41
|
+
}
|
|
42
|
+
end.to_json
|
|
43
|
+
else
|
|
44
|
+
{
|
|
45
|
+
data: {
|
|
46
|
+
body: request_body,
|
|
47
|
+
headers: env.select {|key, val| key.match("^HTTP.*|^CONTENT.*|^AUTHORIZATION.*") }
|
|
48
|
+
.collect {|key, val| [key.gsub(/^HTTP_/, ''), val.downcase] }
|
|
49
|
+
.to_h,
|
|
50
|
+
},
|
|
51
|
+
errors: [],
|
|
52
|
+
extensions: {}
|
|
53
|
+
}.to_json
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
[200, {}, [response_body]]
|
|
37
57
|
end
|
|
38
58
|
}
|
|
39
59
|
|
|
@@ -102,6 +122,51 @@ describe 'Adapters' do
|
|
|
102
122
|
end.to raise_error(timeout_error)
|
|
103
123
|
end
|
|
104
124
|
end
|
|
125
|
+
|
|
126
|
+
describe '#multiplex' do
|
|
127
|
+
it 'makes an HTTP request with multiple queries' do
|
|
128
|
+
response = adapter.multiplex(
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
query: GraphQL::Client::IntrospectionDocument.to_query_string,
|
|
132
|
+
operationName: 'IntrospectionQuery',
|
|
133
|
+
variables: {
|
|
134
|
+
id: 'yayoi-kusama'
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
context: {
|
|
139
|
+
user_id: 1
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
introspection_query = response[0]
|
|
144
|
+
|
|
145
|
+
expect(introspection_query['data']['body']['query']).to eq(GraphQL::Client::IntrospectionDocument.to_query_string)
|
|
146
|
+
expect(introspection_query['data']['body']['variables']).to eq('id' => 'yayoi-kusama')
|
|
147
|
+
expect(introspection_query['data']['body']['operationName']).to eq('IntrospectionQuery')
|
|
148
|
+
expect(introspection_query['data']['headers']['CONTENT_TYPE']).to eq('application/json')
|
|
149
|
+
expect(introspection_query['data']['headers']['ACCEPT']).to eq('application/json')
|
|
150
|
+
expect(introspection_query['errors']).to eq([])
|
|
151
|
+
expect(introspection_query['extensions']).to eq({})
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'raises an error when it receives a server error' do
|
|
155
|
+
adapter.uri = URI.parse('http://localhost:8000/500')
|
|
156
|
+
|
|
157
|
+
expect do
|
|
158
|
+
adapter.multiplex([])
|
|
159
|
+
end.to raise_error(Artemis::GraphQLServerError, "Received server error status 500: Server error")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'allows for overriding timeout' do
|
|
163
|
+
adapter.uri = URI.parse('http://localhost:8000/slow_server')
|
|
164
|
+
|
|
165
|
+
expect do
|
|
166
|
+
adapter.multiplex([])
|
|
167
|
+
end.to raise_error(timeout_error)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
105
170
|
end
|
|
106
171
|
|
|
107
172
|
describe Artemis::Adapters::NetHttpAdapter do
|
|
@@ -129,6 +194,52 @@ describe 'Adapters' do
|
|
|
129
194
|
expect(response['extensions']).to eq({})
|
|
130
195
|
end
|
|
131
196
|
|
|
197
|
+
it 'can make a multiplex (the graphql feature, not HTTP/2) request' do
|
|
198
|
+
response = adapter.multiplex(
|
|
199
|
+
[
|
|
200
|
+
{
|
|
201
|
+
query: GraphQL::Client::IntrospectionDocument.to_query_string,
|
|
202
|
+
operationName: 'IntrospectionQuery',
|
|
203
|
+
variables: {
|
|
204
|
+
id: 'yayoi-kusama'
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
context: {
|
|
209
|
+
url: 'http://localhost:8000/test_multi_domain'
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
expect(response['data']['body']).to eq("Endpoint switched.")
|
|
214
|
+
expect(response['errors']).to eq([])
|
|
215
|
+
expect(response['extensions']).to eq({})
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'can make a multiplex request with custom HTTP headers' do
|
|
219
|
+
response = adapter.multiplex(
|
|
220
|
+
[
|
|
221
|
+
{
|
|
222
|
+
query: GraphQL::Client::IntrospectionDocument.to_query_string,
|
|
223
|
+
operationName: 'IntrospectionQuery',
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
context: {
|
|
227
|
+
headers: {
|
|
228
|
+
Authorization: "Token token",
|
|
229
|
+
},
|
|
230
|
+
url: 'http://localhost:8000/test_multi_domain'
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect(response['data']['headers']['AUTHORIZATION']).to eq("token token")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it 'raises an error when adapter_options.adapter is set to :multi domain' do
|
|
238
|
+
expect do
|
|
239
|
+
Artemis::Adapters::MultiDomainAdapter.new('ignored', service_name: nil, timeout: 0.5, pool_size: 5, adapter_options: { adapter: :multi_domain })
|
|
240
|
+
end.to raise_error(ArgumentError, 'You can not use the :multi_domain adapter with the :multi_domain adapter.')
|
|
241
|
+
end
|
|
242
|
+
|
|
132
243
|
it 'raises an error when context.url is not specified' do
|
|
133
244
|
expect do
|
|
134
245
|
adapter.execute(document: GraphQL::Client::IntrospectionDocument)
|
data/spec/autoloading_spec.rb
CHANGED
|
@@ -15,7 +15,7 @@ describe "#{GraphQL::Client} Autoloading" do
|
|
|
15
15
|
|
|
16
16
|
describe ".preload!" do
|
|
17
17
|
it "preloads all the graphQL files in the query paths" do
|
|
18
|
-
%i(Artist Artwork
|
|
18
|
+
%i(Artist Artists Artwork ArtistFields)
|
|
19
19
|
.select {|const_name| Metaphysics.constants.include?(const_name) }
|
|
20
20
|
.each {|const_name| Metaphysics.send(:remove_const, const_name) }
|
|
21
21
|
|
|
@@ -43,12 +43,12 @@ describe "#{GraphQL::Client} Autoloading" do
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
it "dynamically loads the matching GraphQL fragment and sets it to a constant" do
|
|
46
|
-
Metaphysics.send(:remove_const, :
|
|
46
|
+
Metaphysics.send(:remove_const, :ArtistFields) if Metaphysics.constants.include?(:ArtistFields)
|
|
47
47
|
|
|
48
|
-
query = Metaphysics::
|
|
48
|
+
query = Metaphysics::ArtistFields
|
|
49
49
|
|
|
50
50
|
expect(query.document.to_query_string).to eq(<<~GRAPHQL.strip)
|
|
51
|
-
fragment
|
|
51
|
+
fragment Metaphysics__ArtistFields on Artist {
|
|
52
52
|
hometown
|
|
53
53
|
deathday
|
|
54
54
|
}
|
data/spec/client_spec.rb
CHANGED
|
@@ -86,11 +86,11 @@ describe GraphQL::Client do
|
|
|
86
86
|
name
|
|
87
87
|
bio
|
|
88
88
|
birthday
|
|
89
|
-
...
|
|
89
|
+
...Metaphysics__ArtistFields
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
fragment
|
|
93
|
+
fragment Metaphysics__ArtistFields on Artist {
|
|
94
94
|
hometown
|
|
95
95
|
deathday
|
|
96
96
|
}
|
|
@@ -171,6 +171,42 @@ describe GraphQL::Client do
|
|
|
171
171
|
end
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
+
it "can batch multiple requests using Multiplex" do
|
|
175
|
+
responses = Metaphysics.multiplex do |queue|
|
|
176
|
+
queue.artist(id: "yayoi-kusama", context: { headers: { Authorization: 'bearer ...' } })
|
|
177
|
+
queue.artwork
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
artist_query, artwork_query = requests[0].queries
|
|
181
|
+
|
|
182
|
+
expect(artist_query[:operationName]).to eq('Metaphysics__Artist')
|
|
183
|
+
expect(artist_query[:variables]).to eq('id' => 'yayoi-kusama')
|
|
184
|
+
expect(artist_query[:context]).to eq({ headers: { Authorization: 'bearer ...' } })
|
|
185
|
+
expect(artist_query[:query]).to eq(<<~GRAPHQL.strip)
|
|
186
|
+
query Metaphysics__Artist($id: String!) {
|
|
187
|
+
artist(id: $id) {
|
|
188
|
+
name
|
|
189
|
+
bio
|
|
190
|
+
birthday
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
GRAPHQL
|
|
194
|
+
|
|
195
|
+
expect(artwork_query[:operationName]).to eq('Metaphysics__Artwork')
|
|
196
|
+
expect(artwork_query[:variables]).to be_empty
|
|
197
|
+
expect(artwork_query[:context]).to eq({})
|
|
198
|
+
expect(artwork_query[:query]).to eq(<<~GRAPHQL.strip)
|
|
199
|
+
query Metaphysics__Artwork {
|
|
200
|
+
artwork(id: "yayoi-kusama-pumpkin-yellow-and-black") {
|
|
201
|
+
title
|
|
202
|
+
artist {
|
|
203
|
+
name
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
GRAPHQL
|
|
208
|
+
end
|
|
209
|
+
|
|
174
210
|
private
|
|
175
211
|
|
|
176
212
|
def requests
|
|
File without changes
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: artemis
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon Allured
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 2022-03-05 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- gemfiles/rails_52.gemfile
|
|
168
168
|
- gemfiles/rails_60.gemfile
|
|
169
169
|
- gemfiles/rails_61.gemfile
|
|
170
|
+
- gemfiles/rails_70.gemfile
|
|
170
171
|
- gemfiles/rails_edge.gemfile
|
|
171
172
|
- lib/artemis.rb
|
|
172
173
|
- lib/artemis/adapters.rb
|
|
@@ -201,7 +202,7 @@ files:
|
|
|
201
202
|
- spec/client_spec.rb
|
|
202
203
|
- spec/endpoint_spec.rb
|
|
203
204
|
- spec/fixtures/metaphysics.rb
|
|
204
|
-
- spec/fixtures/metaphysics/
|
|
205
|
+
- spec/fixtures/metaphysics/_artist_fields.graphql
|
|
205
206
|
- spec/fixtures/metaphysics/artist.graphql
|
|
206
207
|
- spec/fixtures/metaphysics/artists.graphql
|
|
207
208
|
- spec/fixtures/metaphysics/artwork.graphql
|
|
@@ -231,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
231
232
|
- !ruby/object:Gem::Version
|
|
232
233
|
version: '0'
|
|
233
234
|
requirements: []
|
|
234
|
-
rubygems_version: 3.
|
|
235
|
+
rubygems_version: 3.3.7
|
|
235
236
|
signing_key:
|
|
236
237
|
specification_version: 4
|
|
237
238
|
summary: GraphQL on Rails
|