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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a0f96c5c92adae8e2b52b173e05240aaa8967d8184887f25e3b986abe7db6a6
4
- data.tar.gz: bf731d81af95a1ae12c6cc0d1bc01b4f0b13e3d563717d7ee97d3bdbbbc2c18f
3
+ metadata.gz: 7b64a8fd49d4d32ab4fd0dbda0f20005b68129fc1eee01de0f525424a19d1d28
4
+ data.tar.gz: eae9f56e2f7bbd832278cf1546c931824ea5f7bfa969e28cdf9dbe2e8a6e4ab6
5
5
  SHA512:
6
- metadata.gz: 448933d593a7cb931dca4fb128e4a7052792e2c6405f42a797e9f1e9f4bde557fb6845f123a0132e89a53b6f56310c2c0585af74a87acace334dd34d5f74b105
7
- data.tar.gz: 6865a729838c972eda26707ea12cddb846a21aea6d1b2c0cd252054054f2f5758b2c81bdd543ddbac5dfc1fd6ca127c31fd83a5a67f6e847230d7eec787d7375
6
+ metadata.gz: 061e522011314b8f3a712a1a13c504653e7b7979e17ff239298d98742f08ae3fea14cd9772970dd72dd1c9e361572d6c70b076f3b0e4ee552f9d1fc1e2a7e69d
7
+ data.tar.gz: 2d1fec853399e9ec52b74e3c5c7eae407a9affe39222f7719d993f098ef839cd2da72dfbb353f967c025c2d72b464dbdc3f56dadfa4e596b2d9ea32c43d6119f
@@ -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
@@ -8,3 +8,4 @@
8
8
  /tmp/
9
9
  Gemfile.lock
10
10
  gemfiles/*.lock
11
+ .byebug_history
data/Appraisals CHANGED
@@ -6,6 +6,12 @@ appraise "rails_edge" do
6
6
  end
7
7
  end
8
8
 
9
+ appraise "rails_70" do
10
+ gem "rails", '~> 7.0.0'
11
+ gem "railties", '~> 7.0.0'
12
+ gem "activesupport", '~> 7.0.0'
13
+ end
14
+
9
15
  appraise "rails_61" do
10
16
  gem "rails", '~> 6.1.0'
11
17
  gem "railties", '~> 6.1.0'
data/CHANGELOG.md CHANGED
@@ -1,17 +1,30 @@
1
- ## v0.6.0
2
1
 
3
- _<sup>(unreleased)</sup>_
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
- * Add support for Ruby 3.0 and Rails 6.0, 6.1
8
- * Add the multi domain adapter (744b8ea3)
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
- * ~~Generate fixture YAML files on `rails g artemis:query queryName` (#78)~~ Removed due to a bug for now.
13
- * Address warnings from Ruby 2.7 (408adcb3)
14
- * Avoid crashing when config/graphql.yml does not exist (@dlackty, #76)
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 [![Build Status](https://travis-ci.org/yuki24/artemis.svg?branch=master)](https://travis-ci.org/yuki24/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
- | `:test` | N/A (See Testing)
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: "../"
@@ -11,5 +11,6 @@ end
11
11
  gem "pry"
12
12
  gem "pry-byebug", platforms: :mri
13
13
  gem "curb", ">= 0.9.6"
14
+ gem "webrick"
14
15
 
15
16
  gemspec path: "../"
@@ -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 execute(document:, operation_name: nil, variables: {}, context: {})
24
- easy = Curl::Easy.new(uri.to_s)
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
- easy.timeout = timeout
32
- easy.multi = multi
33
- easy.headers = DEFAULT_HEADERS.merge(headers(context))
34
- easy.post_body = JSON.generate(body)
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
@@ -2,6 +2,12 @@
2
2
 
3
3
  require 'delegate'
4
4
 
5
+ begin
6
+ require "active_support/isolated_execution_state"
7
+ rescue LoadError
8
+ # no-op... Rails 7.0 requires this.
9
+ end
10
+
5
11
  require 'active_support/core_ext/numeric/time'
6
12
  require 'net/http/persistent'
7
13
 
@@ -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' => {}
@@ -167,17 +167,19 @@ module Artemis
167
167
  end
168
168
 
169
169
  def resolve_graphql_file_path(filename, fragment: false)
170
- namespace = name.underscore
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}/#{name.underscore}/*.graphql"] }
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
- private_constant :Callbacks, :Executor
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
@@ -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
- endpoint_names = Artemis.config_for_graphql(app).keys
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.register!(endpoint_name, {
52
- schema_path: app.root.join(config.artemis.schema_path, "#{endpoint_name}.json").to_s
53
- }.merge(options.symbolize_keys))
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
@@ -1,3 +1,3 @@
1
1
  module Artemis
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -37,7 +37,7 @@ class Artemis::QueryGenerator < Rails::Generators::Base
37
37
  end
38
38
 
39
39
  def target_query
40
- schema.find("Query").fields[query_type] ||
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? && "(#{ arguments.map {|name, type| "$#{name}: #{type.type.to_type_signature}" }.join(", ") })" %> {
2
- <%= target_query.name %><%= arguments.present? && "(#{ arguments.map {|name, type| "#{name}: $#{name}" }.join(", ") })" %> {
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
  }
@@ -6,7 +6,7 @@ describe 'Adapters' do
6
6
  FakeServer = ->(env) {
7
7
  case env['PATH_INFO']
8
8
  when '/slow_server'
9
- sleep 1.1
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
- body = {
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
- [200, {}, [body]]
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)
@@ -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 ArtistFragment)
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, :ArtistFragment) if Metaphysics.constants.include?(:ArtistFragment)
46
+ Metaphysics.send(:remove_const, :ArtistFields) if Metaphysics.constants.include?(:ArtistFields)
47
47
 
48
- query = Metaphysics::ArtistFragment
48
+ query = Metaphysics::ArtistFields
49
49
 
50
50
  expect(query.document.to_query_string).to eq(<<~GRAPHQL.strip)
51
- fragment Metaphysics__ArtistFragment on Artist {
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
- ...Metaphysics__ArtistFragment
89
+ ...Metaphysics__ArtistFields
90
90
  }
91
91
  }
92
92
 
93
- fragment Metaphysics__ArtistFragment on Artist {
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
@@ -3,6 +3,6 @@ query($size: Int!) {
3
3
  name
4
4
  bio
5
5
  birthday
6
- ...Metaphysics::ArtistFragment
6
+ ...Metaphysics::ArtistFields
7
7
  }
8
8
  }
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.6.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: 2021-09-03 00:00:00.000000000 Z
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/_artist_fragment.graphql
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.2.26
235
+ rubygems_version: 3.3.7
235
236
  signing_key:
236
237
  specification_version: 4
237
238
  summary: GraphQL on Rails