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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a0f96c5c92adae8e2b52b173e05240aaa8967d8184887f25e3b986abe7db6a6
4
- data.tar.gz: bf731d81af95a1ae12c6cc0d1bc01b4f0b13e3d563717d7ee97d3bdbbbc2c18f
3
+ metadata.gz: be41d5677f98282ad274f675f696c6d092fb7d0279d8986492a615e81c024d93
4
+ data.tar.gz: d9877242e814d7c20b24b217eb2a3c762699ae21d69ddf1e926e762c79854e59
5
5
  SHA512:
6
- metadata.gz: 448933d593a7cb931dca4fb128e4a7052792e2c6405f42a797e9f1e9f4bde557fb6845f123a0132e89a53b6f56310c2c0585af74a87acace334dd34d5f74b105
7
- data.tar.gz: 6865a729838c972eda26707ea12cddb846a21aea6d1b2c0cd252054054f2f5758b2c81bdd543ddbac5dfc1fd6ca127c31fd83a5a67f6e847230d7eec787d7375
6
+ metadata.gz: 3c16b35fa42042a378ea42920155fd4ea35d1289f65868be9883a50d3ba5dfd329978ea24847f4de4372c585bdb351ab8f4a6c65072fa7d49013d9948d5f08ed
7
+ data.tar.gz: a6c6034a1b524165b193a5dc96efa60a4516aa28a36fbb0a16b5fdec5bdcb868d4142f97c756cb62cfc46926d14de8aaf233b7c1876627e5806366088c898f57
@@ -5,23 +5,43 @@ on:
5
5
  - pull_request
6
6
 
7
7
  jobs:
8
- build:
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
- # - ruby_version: '2.6'
38
- # gemfile: gemfiles/rails_edge.gemfile
39
- # - ruby_version: '2.5'
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@v2
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
@@ -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,33 @@
1
- ## v0.6.0
1
+ ## Unreleased
2
2
 
3
- _<sup>(unreleased)</sup>_
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
- * Add support for Ruby 3.0 and Rails 6.0, 6.1
8
- * Add the multi domain adapter (744b8ea3)
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
- * ~~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)
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 [![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.8.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.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: 2021-09-03 00:00:00.000000000 Z
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/_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