artemis 0.6.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +92 -10
- data/.gitignore +1 -0
- data/Appraisals +6 -0
- data/CHANGELOG.md +23 -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: be41d5677f98282ad274f675f696c6d092fb7d0279d8986492a615e81c024d93
|
4
|
+
data.tar.gz: d9877242e814d7c20b24b217eb2a3c762699ae21d69ddf1e926e762c79854e59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c16b35fa42042a378ea42920155fd4ea35d1289f65868be9883a50d3ba5dfd329978ea24847f4de4372c585bdb351ab8f4a6c65072fa7d49013d9948d5f08ed
|
7
|
+
data.tar.gz: a6c6034a1b524165b193a5dc96efa60a4516aa28a36fbb0a16b5fdec5bdcb868d4142f97c756cb62cfc46926d14de8aaf233b7c1876627e5806366088c898f57
|
data/.github/workflows/ruby.yml
CHANGED
@@ -5,23 +5,43 @@ on:
|
|
5
5
|
- pull_request
|
6
6
|
|
7
7
|
jobs:
|
8
|
-
|
8
|
+
mri:
|
9
9
|
strategy:
|
10
10
|
matrix:
|
11
11
|
ruby_version:
|
12
|
+
- '3.2'
|
13
|
+
- '3.1'
|
12
14
|
- '3.0'
|
13
15
|
- '2.7'
|
14
16
|
- '2.6'
|
15
|
-
- '2.5'
|
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
|
21
22
|
- gemfiles/rails_51.gemfile
|
22
23
|
- gemfiles/rails_50.gemfile
|
23
|
-
# - gemfiles/rails_edge.gemfile
|
24
24
|
exclude:
|
25
|
+
- ruby_version: '3.2'
|
26
|
+
gemfile: gemfiles/rails_61.gemfile
|
27
|
+
- ruby_version: '3.2'
|
28
|
+
gemfile: gemfiles/rails_60.gemfile
|
29
|
+
- ruby_version: '3.2'
|
30
|
+
gemfile: gemfiles/rails_52.gemfile
|
31
|
+
- ruby_version: '3.2'
|
32
|
+
gemfile: gemfiles/rails_51.gemfile
|
33
|
+
- ruby_version: '3.2'
|
34
|
+
gemfile: gemfiles/rails_50.gemfile
|
35
|
+
- ruby_version: '3.1'
|
36
|
+
gemfile: gemfiles/rails_61.gemfile
|
37
|
+
- ruby_version: '3.1'
|
38
|
+
gemfile: gemfiles/rails_60.gemfile
|
39
|
+
- ruby_version: '3.1'
|
40
|
+
gemfile: gemfiles/rails_52.gemfile
|
41
|
+
- ruby_version: '3.1'
|
42
|
+
gemfile: gemfiles/rails_51.gemfile
|
43
|
+
- ruby_version: '3.1'
|
44
|
+
gemfile: gemfiles/rails_50.gemfile
|
25
45
|
- ruby_version: '3.0'
|
26
46
|
gemfile: gemfiles/rails_52.gemfile
|
27
47
|
- ruby_version: '3.0'
|
@@ -34,15 +54,13 @@ jobs:
|
|
34
54
|
gemfile: gemfiles/rails_51.gemfile
|
35
55
|
- ruby_version: '2.7'
|
36
56
|
gemfile: gemfiles/rails_50.gemfile
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
# gemfile: gemfiles/rails_edge.gemfile
|
41
|
-
runs-on: ubuntu-18.04
|
57
|
+
- ruby_version: '2.6'
|
58
|
+
gemfile: gemfiles/rails_70.gemfile
|
59
|
+
runs-on: ubuntu-22.04
|
42
60
|
env:
|
43
61
|
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
44
62
|
steps:
|
45
|
-
- uses: actions/checkout@
|
63
|
+
- uses: actions/checkout@v3
|
46
64
|
- name: Install curl
|
47
65
|
run: sudo apt-get install curl libcurl4-openssl-dev
|
48
66
|
- name: Set up Ruby
|
@@ -51,3 +69,67 @@ jobs:
|
|
51
69
|
ruby-version: ${{ matrix.ruby_version }}
|
52
70
|
bundler-cache: true
|
53
71
|
- run: bundle exec rake
|
72
|
+
|
73
|
+
rails_edge:
|
74
|
+
needs:
|
75
|
+
- mri
|
76
|
+
runs-on: ubuntu-22.04
|
77
|
+
env:
|
78
|
+
BUNDLE_GEMFILE: gemfiles/rails_edge.gemfile
|
79
|
+
steps:
|
80
|
+
- uses: actions/checkout@v3
|
81
|
+
- name: Install curl
|
82
|
+
run: sudo apt-get install curl libcurl4-openssl-dev
|
83
|
+
- name: Set up Ruby
|
84
|
+
uses: ruby/setup-ruby@v1
|
85
|
+
with:
|
86
|
+
ruby-version: 3.2
|
87
|
+
bundler-cache: true
|
88
|
+
- run: bundle exec rake || echo "Rails edge test is done."
|
89
|
+
|
90
|
+
ruby_edge:
|
91
|
+
needs:
|
92
|
+
- mri
|
93
|
+
strategy:
|
94
|
+
matrix:
|
95
|
+
ruby_version:
|
96
|
+
- 'ruby-head'
|
97
|
+
gemfile:
|
98
|
+
- gemfiles/rails_edge.gemfile
|
99
|
+
- gemfiles/rails_70.gemfile
|
100
|
+
runs-on: ubuntu-22.04
|
101
|
+
env:
|
102
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
103
|
+
steps:
|
104
|
+
- uses: actions/checkout@v3
|
105
|
+
- name: Install curl
|
106
|
+
run: sudo apt-get install curl libcurl4-openssl-dev
|
107
|
+
- name: Set up Ruby
|
108
|
+
uses: ruby/setup-ruby@v1
|
109
|
+
with:
|
110
|
+
ruby-version: ${{ matrix.ruby_version }}
|
111
|
+
bundler-cache: true
|
112
|
+
- run: bundle exec rake || echo "Ruby edge test is done."
|
113
|
+
|
114
|
+
# The curb gem does not work well with JRuby, so skipping for now...
|
115
|
+
# jruby:
|
116
|
+
# needs:
|
117
|
+
# - mri
|
118
|
+
# strategy:
|
119
|
+
# matrix:
|
120
|
+
# ruby_version:
|
121
|
+
# - 'jruby-9.4'
|
122
|
+
# - 'jruby-head'
|
123
|
+
# gemfile:
|
124
|
+
# - gemfiles/rails_70.gemfile
|
125
|
+
# runs-on: ubuntu-22.04
|
126
|
+
# env:
|
127
|
+
# BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
128
|
+
# steps:
|
129
|
+
# - uses: actions/checkout@v3
|
130
|
+
# - name: Set up Ruby
|
131
|
+
# uses: ruby/setup-ruby@v1
|
132
|
+
# with:
|
133
|
+
# ruby-version: ${{ matrix.ruby_version }}
|
134
|
+
# bundler-cache: true
|
135
|
+
# - run: bundle exec rake || echo "JRuby test is done."
|
data/.gitignore
CHANGED
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,17 +1,33 @@
|
|
1
|
-
##
|
1
|
+
## Unreleased
|
2
2
|
|
3
|
-
|
3
|
+
- New entries come here...
|
4
|
+
|
5
|
+
## [v0.7.0](https://github.com/yuki24/artemis/tree/v0.7.0)
|
6
|
+
|
7
|
+
_<sup>released at 2022-03-05 08:24:45 UTC</sup>_
|
8
|
+
|
9
|
+
#### Features
|
10
|
+
|
11
|
+
- Add support for Ruby 3.1 and Rails 7.0
|
12
|
+
- Add support for [the Multiplex query](https://graphql-ruby.org/queries/multiplex.html)
|
13
|
+
- Do not allow the usage of the `multi_domain` adapter to be nested ([<tt>9b7b520</tt>](https://github.com/yuki24/artemis/commit/9b7b5202c9fbe424d4ca22f05dc9c9759b5202c3))
|
14
|
+
- Do not require fragment files to end with `_fragment.graphql` ([<tt>3c6c0fa</tt>](https://github.com/yuki24/artemis/commit/3c6c0fa))
|
15
|
+
- Allow for overriding the namespace used for resolving graphql file paths ([<tt>bd18762</tt>](https://github.com/yuki24/artemis/commit/bd18762))
|
16
|
+
|
17
|
+
## [v0.6.0](https://github.com/yuki24/artemis/tree/v0.6.0)
|
18
|
+
|
19
|
+
_<sup>released at 2021-09-03 04:17:55 UTC</sup>_
|
4
20
|
|
5
21
|
#### Features
|
6
22
|
|
7
|
-
|
8
|
-
|
23
|
+
- Add support for Ruby 3.0 and Rails 6.0, 6.1
|
24
|
+
- Add the multi domain adapter ([<tt>744b8ea</tt>](https://github.com/yuki24/artemis/commit/744b8ea35795b4e6cc4fdc1ebb63dd9a4e9819f0))
|
9
25
|
|
10
26
|
#### Fixes
|
11
27
|
|
12
|
-
|
13
|
-
|
14
|
-
|
28
|
+
- ~~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.
|
29
|
+
- Address warnings from Ruby 2.7 ([<tt>408adcb</tt>](https://github.com/yuki24/artemis/commit/408adcb3f39912f7afb7b3690a52f1d593662b7b))
|
30
|
+
- Avoid crashing when config/graphql.yml does not exist ([@dlackty](https://github.com/dlackty), [#76](https://github.com/yuki24/artemis/pull/76))
|
15
31
|
|
16
32
|
## [v0.5.2](https://github.com/yuki24/artemis/tree/v0.5.2)
|
17
33
|
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Artemis [![
|
1
|
+
# Artemis [![build](https://github.com/yuki24/artemis/actions/workflows/ruby.yml/badge.svg)](https://github.com/yuki24/artemis/actions/workflows/ruby.yml) [![Gem Version](https://badge.fury.io/rb/artemis.svg)](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.8.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: 2023-01-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
|