graphlient 0.7.0 → 0.9.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
  4. data/.github/workflows/ci.yml +33 -29
  5. data/.github/workflows/danger-comment.yml +10 -0
  6. data/.github/workflows/danger.yml +13 -22
  7. data/.github/workflows/rubocop.yml +19 -19
  8. data/.gitignore +59 -52
  9. data/.rspec +1 -1
  10. data/.rubocop.yml +26 -28
  11. data/.rubocop_todo.yml +72 -48
  12. data/CHANGELOG.md +128 -107
  13. data/CONTRIBUTING.md +125 -125
  14. data/Dangerfile +24 -24
  15. data/Gemfile +35 -27
  16. data/LICENSE +21 -21
  17. data/README.md +800 -508
  18. data/RELEASING.md +63 -63
  19. data/Rakefile +15 -15
  20. data/UPGRADING.md +23 -23
  21. data/graphlient.gemspec +19 -19
  22. data/lib/graphlient/adapters/http/adapter.rb +41 -41
  23. data/lib/graphlient/adapters/http/faraday_adapter.rb +79 -45
  24. data/lib/graphlient/adapters/http/http_adapter.rb +40 -39
  25. data/lib/graphlient/adapters/http.rb +3 -3
  26. data/lib/graphlient/adapters.rb +1 -1
  27. data/lib/graphlient/client.rb +98 -73
  28. data/lib/graphlient/errors/client_error.rb +6 -6
  29. data/lib/graphlient/errors/connection_failed_error.rb +6 -6
  30. data/lib/graphlient/errors/error.rb +13 -12
  31. data/lib/graphlient/errors/execution_error.rb +29 -29
  32. data/lib/graphlient/errors/faraday_server_error.rb +12 -12
  33. data/lib/graphlient/errors/graphql_error.rb +53 -52
  34. data/lib/graphlient/errors/http_options_error.rb +6 -6
  35. data/lib/graphlient/errors/http_server_error.rb +13 -13
  36. data/lib/graphlient/errors/server_error.rb +7 -7
  37. data/lib/graphlient/errors/timeout_error.rb +6 -6
  38. data/lib/graphlient/errors.rb +10 -10
  39. data/lib/graphlient/extensions/query.rb +15 -15
  40. data/lib/graphlient/extensions.rb +1 -1
  41. data/lib/graphlient/query/directive.rb +47 -0
  42. data/lib/graphlient/query/serializer/arguments.rb +67 -0
  43. data/lib/graphlient/query/serializer/directives.rb +17 -0
  44. data/lib/graphlient/query/serializer/evaluator.rb +15 -0
  45. data/lib/graphlient/query/serializer/fragments.rb +28 -0
  46. data/lib/graphlient/query/serializer/scalars.rb +63 -0
  47. data/lib/graphlient/query/serializer.rb +153 -0
  48. data/lib/graphlient/query.rb +29 -128
  49. data/lib/graphlient/schema.rb +27 -26
  50. data/lib/graphlient/version.rb +3 -3
  51. data/lib/graphlient.rb +8 -8
  52. data/spec/graphlient/adapters/http/faraday_adapter_spec.rb +172 -112
  53. data/spec/graphlient/adapters/http/http_adapter_spec.rb +60 -60
  54. data/spec/graphlient/client_dsl_spec.rb +153 -0
  55. data/spec/graphlient/client_query_spec.rb +369 -346
  56. data/spec/graphlient/client_schema_spec.rb +75 -55
  57. data/spec/graphlient/extensions/query_spec.rb +16 -16
  58. data/spec/graphlient/github_query_spec.rb +32 -32
  59. data/spec/graphlient/query_dsl_spec.rb +231 -0
  60. data/spec/graphlient/query_spec.rb +105 -105
  61. data/spec/graphlient/schema_spec.rb +56 -56
  62. data/spec/graphlient/static_client_query_spec.rb +75 -68
  63. data/spec/graphlient/webmock_client_query_spec.rb +41 -41
  64. data/spec/spec_helper.rb +26 -14
  65. data/spec/support/context/dummy_client.rb +33 -26
  66. data/spec/support/context/github_client.rb +18 -18
  67. data/spec/support/dummy_app.rb +21 -19
  68. data/spec/support/dummy_schema.rb +17 -17
  69. data/spec/support/fixtures/github/schema.yml +14282 -14282
  70. data/spec/support/fixtures/github/user.yml +76 -76
  71. data/spec/support/fixtures/github/viewer.yml +76 -76
  72. data/spec/support/fixtures/invoice_api.json +1288 -1288
  73. data/spec/support/mutations/create_invoice.rb +18 -18
  74. data/spec/support/queries/query.rb +48 -47
  75. data/spec/support/schema/github.json +44479 -44479
  76. data/spec/support/types/invoice_type.rb +13 -13
  77. data/spec/support/types/mutation_type.rb +5 -5
  78. data/spec/support/vcr.rb +9 -9
  79. metadata +13 -8
  80. data/Gemfile.danger +0 -5
data/README.md CHANGED
@@ -1,508 +1,800 @@
1
- # Graphlient
2
-
3
- [![Gem Version](https://badge.fury.io/rb/graphlient.svg)](https://badge.fury.io/rb/graphlient)
4
- [![Build Status](https://github.com/ashkan18/graphlient/actions/workflows/ci.yml/badge.svg)](https://github.com/ashkan18/graphlient/actions/workflows/ci.yml)
5
-
6
- A friendlier Ruby client for consuming GraphQL-based APIs. Built on top of your usual [graphql-client](https://github.com/github/graphql-client), but with better defaults, more consistent error handling, and using the [faraday](https://github.com/lostisland/faraday) HTTP client.
7
-
8
- # Table of Contents
9
-
10
- - [Installation](#installation)
11
- - [Usage](#usage)
12
- - [Schema storing and loading on disk](#schema-storing-and-loading-on-disk)
13
- - [Error Handling](#error-handling)
14
- - [Executing Parameterized Queries and Mutations](#executing-parameterized-queries-and-mutations)
15
- - [Parse and Execute Queries Separately](#parse-and-execute-queries-separately)
16
- - [Dynamic vs. Static Queries](#dynamic-vs-static-queries)
17
- - [Generate Queries with Graphlient::Query](#generate-queries-with-graphlientquery)
18
- - [Create API Client Classes with Graphlient::Extension::Query](#create-api-client-classes-with-graphlientextensionquery)
19
- - [Swapping the HTTP Stack](#swapping-the-http-stack)
20
- - [Testing with Graphlient and RSpec](#testing-with-graphlient-and-rspec)
21
- - [License](#license)
22
-
23
- ## Installation
24
-
25
- Add the following line to your Gemfile.
26
-
27
- ```ruby
28
- gem 'graphlient'
29
- ```
30
-
31
- ## Usage
32
-
33
- Create a new instance of `Graphlient::Client` with a URL and optional headers/http_options.
34
-
35
- ```ruby
36
- client = Graphlient::Client.new('https://test-graphql.biz/graphql',
37
- headers: {
38
- 'Authorization' => 'Bearer 123'
39
- },
40
- http_options: {
41
- read_timeout: 20,
42
- write_timeout: 30
43
- }
44
- )
45
- ```
46
-
47
- | http_options | default | type |
48
- |---------------|---------|---------|
49
- | read_timeout | nil | seconds |
50
- | write_timeout | nil | seconds |
51
-
52
- The schema is available automatically via `.schema`.
53
-
54
- ```ruby
55
- client.schema # GraphQL::Schema
56
- ```
57
-
58
- Make queries with `query`, which takes a String or a block for the query definition.
59
-
60
- With a String.
61
-
62
- ```ruby
63
- response = client.query <<~GRAPHQL
64
- query {
65
- invoice(id: 10) {
66
- id
67
- total
68
- line_items {
69
- price
70
- item_type
71
- }
72
- }
73
- }
74
- GRAPHQL
75
- ```
76
-
77
- With a block.
78
-
79
- ```ruby
80
- response = client.query do
81
- query do
82
- invoice(id: 10) do
83
- id
84
- total
85
- line_items do
86
- price
87
- item_type
88
- end
89
- end
90
- end
91
- end
92
- ```
93
-
94
- This will call the endpoint setup in the configuration with `POST`, the `Authorization` header and `query` as follows.
95
-
96
- ```graphql
97
- query {
98
- invoice(id: 10) {
99
- id
100
- total
101
- line_items {
102
- price
103
- item_type
104
- }
105
- }
106
- }
107
- ```
108
-
109
- A successful response object always contains data which can be iterated upon. The following example returns the first line item's price.
110
-
111
- ```ruby
112
- response.data.invoice.line_items.first.price
113
- ```
114
-
115
- You can also execute mutations the same way.
116
-
117
- ```ruby
118
- response = client.query do
119
- mutation do
120
- createInvoice(input: { fee_in_cents: 12_345 }) do
121
- id
122
- fee_in_cents
123
- end
124
- end
125
- end
126
- ```
127
-
128
- The successful response contains data in `response.data`. The following example returns the newly created invoice's ID.
129
-
130
- ```ruby
131
- response.data.create_invoice.first.id
132
- ```
133
-
134
- ### Schema storing and loading on disk
135
-
136
- To reduce requests to graphql API you can cache schema:
137
-
138
- ```ruby
139
- client = Client.new(url, schema_path: 'config/your_graphql_schema.json')
140
- client.schema.dump! # you only need to call this when graphql schema changes
141
- ```
142
-
143
- ### Error Handling
144
-
145
- Unlike graphql-client, Graphlient will always raise an exception unless the query has succeeded.
146
-
147
- * [Graphlient::Errors::ClientError](lib/graphlient/errors/client_error.rb): all client-side query validation failures based on current schema
148
- * [Graphlient::Errors::GraphQLError](lib/graphlient/errors/graphql_error.rb): all GraphQL API errors, with a humanly readable collection of problems
149
- * [Graphlient::Errors::ExecutionError](lib/graphlient/errors/execution_error.rb): all GraphQL execution errors, with a humanly readable collection of problems
150
- * [Graphlient::Errors::ServerError](lib/graphlient/errors/server_error.rb): all transport errors raised by HTTP Adapters. You can access `inner_exception`, `status_code` and `response` on these errors to get more details on what went wrong
151
- * [Graphlient::Errors::FaradayServerError](lib/graphlient/errors/faraday_server_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
152
- * [Graphlient::Errors::HttpServerError](lib/graphlient/errors/http_server_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
153
- * [Graphlient::Errors::ConnectionFailedError](lib/graphlient/errors/connection_failed_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
154
- * [Graphlient::Errors::TimeoutError](lib/graphlient/errors/timeout_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
155
- * [Graphlient::Errors::HttpOptionsError](lib/graphlient/errors/http_options_error.rb): all NoMethodError raised by HTTP Adapters when given options in `http_options` are invalid
156
-
157
-
158
- All errors inherit from `Graphlient::Errors::Error` if you need to handle them in bulk.
159
-
160
- ### Executing Parameterized Queries and Mutations
161
-
162
- Graphlient can execute parameterized queries and mutations by providing variables as query parameters.
163
-
164
- The following query accepts an array of IDs.
165
-
166
- With a String.
167
-
168
- ```ruby
169
- query = <<-GRAPHQL
170
- query($ids: [Int]) {
171
- invoices(ids: $ids) {
172
- id
173
- fee_in_cents
174
- }
175
- }
176
- GRAPHQL
177
- variables = { ids: [42] }
178
-
179
- client.query(query, variables)
180
- ```
181
-
182
- With a block.
183
-
184
- ```ruby
185
- client.query(ids: [42]) do
186
- query(ids: [:int]) do
187
- invoices(ids: :ids) do
188
- id
189
- fee_in_cents
190
- end
191
- end
192
- end
193
- ```
194
- Graphlient supports following Scalar types for parameterized queries by default:
195
- - `:id` maps to `ID`
196
- - `:boolean` maps to `Boolean`
197
- - `:float` maps to `Float`
198
- - `:int` maps to `Int`
199
- - `:string` maps to `String`
200
-
201
- You can use any of the above types with `!` to make it required or use them in `[]` for array parameters.
202
-
203
- For any other custom types, graphlient will simply use `to_s` of the symbol provided for the type, so `query(ids: [:InvoiceType!])` will result in `query($ids: [InvoiceType!])`.
204
-
205
- The following mutation accepts a custom type that requires `fee_in_cents`.
206
-
207
- ```ruby
208
- client.query(input: { fee_in_cents: 12_345 }) do
209
- mutation(input: :createInvoiceInput!) do
210
- createInvoice(input: :input) do
211
- id
212
- fee_in_cents
213
- end
214
- end
215
- end
216
- ```
217
-
218
- ### Parse and Execute Queries Separately
219
-
220
- You can `parse` and `execute` queries separately with optional variables. This is highly recommended as parsing a query and validating a query on every request adds performance overhead. Parsing queries early allows validation errors to be discovered before request time and avoids many potential security issues.
221
-
222
-
223
- ```ruby
224
- # parse a query, returns a GraphQL::Client::OperationDefinition
225
- query = client.parse do
226
- query(ids: [:int]) do
227
- invoices(ids: :ids) do
228
- id
229
- fee_in_cents
230
- end
231
- end
232
- end
233
-
234
- # execute a query, returns a GraphQL::Client::Response
235
- client.execute query, ids: [42]
236
- ```
237
-
238
- Or pass in a string instead of a block:
239
-
240
- ```ruby
241
- # parse a query, returns a GraphQL::Client::OperationDefinition
242
- query = client.parse <<~GRAPHQL
243
- query($some_id: Int) {
244
- invoice(id: $some_id) {
245
- id
246
- feeInCents
247
- }
248
- }
249
- GRAPHQL
250
-
251
- # execute a query, returns a GraphQL::Client::Response
252
- client.execute query, ids: [42]
253
- ```
254
-
255
- ### Dynamic vs. Static Queries
256
-
257
- Graphlient uses [graphql-client](https://github.com/github/graphql-client), which [recommends](https://github.com/github/graphql-client/blob/master/guides/dynamic-query-error.md) building queries as static module members along with dynamic variables during execution. This can be accomplished with graphlient the same way.
258
-
259
- Create a new instance of `Graphlient::Client` with a URL and optional headers.
260
-
261
- ```ruby
262
- module SWAPI
263
- Client = Graphlient::Client.new('https://test-graphql.biz/graphql',
264
- headers: {
265
- 'Authorization' => 'Bearer 123'
266
- },
267
- allow_dynamic_queries: false
268
- )
269
- end
270
- ```
271
-
272
- The schema is available automatically via `.schema`.
273
-
274
- ```ruby
275
- SWAPI::Client.schema # GraphQL::Schema
276
- ```
277
-
278
- Define a query.
279
-
280
- ```ruby
281
- module SWAPI
282
- InvoiceQuery = Client.parse do
283
- query(id: :int) do
284
- invoice(id: :id) do
285
- id
286
- fee_in_cents
287
- end
288
- end
289
- end
290
- end
291
- ```
292
-
293
- Execute the query.
294
-
295
- ```ruby
296
- response = SWAPI::Client.execute(SWAPI::InvoiceQuery, id: 42)
297
- ```
298
-
299
- Note that in the example above the client is created with `allow_dynamic_queries: false` (only allow static queries), while graphlient defaults to `allow_dynamic_queries: true` (allow dynamic queries). This option is marked deprecated, but we're proposing to remove it and default it to `true` in [graphql-client#128](https://github.com/github/graphql-client/issues/128).
300
-
301
- ### Generate Queries with Graphlient::Query
302
-
303
- You can directly use `Graphlient::Query` to generate raw GraphQL queries.
304
-
305
- ```ruby
306
- query = Graphlient::Query.new do
307
- query do
308
- invoice(id: 10) do
309
- line_items
310
- end
311
- end
312
- end
313
-
314
- query.to_s
315
- # "\nquery {\n invoice(id: 10){\n line_items\n }\n }\n"
316
- ```
317
-
318
- ### Use of Fragments
319
-
320
- [Fragments](https://github.com/github/graphql-client#defining-queries) should be referred by constant:
321
-
322
- ```ruby
323
- module Fragments
324
- Invoice = client.parse <<~'GRAPHQL'
325
- fragment on Invoice {
326
- id
327
- feeInCents
328
- }
329
- GRAPHQL
330
- end
331
- ```
332
-
333
- `Graphlient` offers the syntax below to refer to the original constant:
334
- * Triple underscore `___` to refer to the fragment
335
- * Double underscore `__` for namespace separator
336
-
337
- In this example, `Fragments::Invoice` would be referred as follows:
338
-
339
- ```ruby
340
- invoice_query = client.parse do
341
- query do
342
- invoice(id: 10) do
343
- id
344
- ___Graphlient__InvoiceFragment
345
- end
346
- end
347
- end
348
- ```
349
-
350
- The wrapped response only allows access to fields that have been explicitly asked for.
351
- In this example, while `id` has been referenced directly in the main query, `feeInCents` has been spread via fragment and trying to access it in the original wrapped response will throw [`GraphQL::Client::ImplicitlyFetchedFieldError`](https://github.com/github/graphql-client/blob/master/guides/implicitly-fetched-field-error.md) (to prevent data leaks between components).
352
-
353
- ```ruby
354
- response = client.execute(invoice_query)
355
- result = response.data.invoice
356
- result.to_h
357
- # {"id" => 10, "feeInCents"=> 20000}
358
- result.id
359
- # 10
360
- result.fee_in_cents
361
- # raises GraphQL::Client::ImplicitlyFetchedFieldError
362
- ```
363
-
364
- `feeInCents` cannot be fetched directly from the main query, but from the fragment as shown below:
365
-
366
- ```ruby
367
- invoice = Fragments::Invoice.new(result)
368
- invoice.id
369
- # 10
370
- invoice.fee_in_cents
371
- # 20000
372
- ```
373
-
374
- ### Create API Client Classes with Graphlient::Extension::Query
375
-
376
- You can include `Graphlient::Extensions::Query` in your class. This will add a new `method_missing` method to your context which will be used to generate GraphQL queries.
377
-
378
- ```ruby
379
- include Graphlient::Extensions::Query
380
-
381
- query = query do
382
- invoice(id: 10) do
383
- line_items
384
- end
385
- end
386
-
387
- query.to_s
388
- # "\nquery{\n invoice(id: 10){\n line_items\n }\n }\n"
389
- ```
390
-
391
- ### Swapping the HTTP Stack
392
-
393
- You can swap the default Faraday adapter for `Net::HTTP`.
394
-
395
- ```ruby
396
- client = Graphlient::Client.new('https://test-graphql.biz/graphql',
397
- http: Graphlient::Adapters::HTTP::HTTPAdapter
398
- )
399
- ```
400
-
401
- ### Testing with Graphlient and RSpec
402
-
403
- Use Graphlient inside your RSpec tests in a Rails application or with `Rack::Test` against your actual application.
404
-
405
- ```ruby
406
- require 'spec_helper'
407
-
408
- describe App do
409
- include Rack::Test::Methods
410
-
411
- def app
412
- # ...
413
- end
414
-
415
- let(:client) do
416
- Graphlient::Client.new('http://test-graphql.biz/graphql') do |client|
417
- client.http do |h|
418
- h.connection do |c|
419
- c.adapter Faraday::Adapter::Rack, app
420
- end
421
- end
422
- end
423
- end
424
-
425
- context 'an invoice' do
426
- let(:result) do
427
- client.query do
428
- query do
429
- invoice(id: 10) do
430
- id
431
- end
432
- end
433
- end
434
- end
435
-
436
- it 'can be retrieved' do
437
- expect(result.data.invoice.id).to eq 10
438
- end
439
- end
440
- end
441
- ```
442
-
443
- Alternately you can `stub_request` with Webmock.
444
-
445
- ```ruby
446
- describe App do
447
- let(:url) { 'http://example.com/graphql' }
448
- let(:client) { Graphlient::Client.new(url) }
449
-
450
- before do
451
- stub_request(:post, url).to_return(
452
- status: 200,
453
- body: DummySchema.execute(GraphQL::Introspection::INTROSPECTION_QUERY).to_json
454
- )
455
- end
456
-
457
- it 'retrieves schema' do
458
- expect(client.schema).to be_a Graphlient::Schema
459
- end
460
- end
461
- ```
462
-
463
- In order to stub the response to actual queries, [dump the schema into a JSON file](#schema-storing-and-loading-on-disk) and specify it via schema_path as follows.
464
-
465
- ```ruby
466
- describe App do
467
- let(:url) { 'http://graph.biz/graphql' }
468
- let(:client) { Graphlient::Client.new(url, schema_path: 'spec/support/fixtures/invoice_api.json') }
469
- let(:query) do
470
- <<~GRAPHQL
471
- query{
472
- invoice(id: 42) {
473
- id
474
- feeInCents
475
- }
476
- }
477
- GRAPHQL
478
- end
479
- let(:json_response) do
480
- {
481
- 'data' => {
482
- 'invoice' => {
483
- 'id' => '42',
484
- 'feeInCents' => 2000
485
- }
486
- }
487
- }.to_json
488
- end
489
-
490
- before do
491
- stub_request(:post, url).to_return(
492
- status: 200,
493
- body: json_response
494
- )
495
- end
496
-
497
- it 'returns invoice fees' do
498
- response = client.query(query)
499
- expect(response.data).to be_truthy
500
- expect(response.data.invoice.id).to eq('42')
501
- expect(response.data.invoice.fee_in_cents).to eq(2000)
502
- end
503
- end
504
- ```
505
-
506
- ## License
507
-
508
- MIT License, see [LICENSE](LICENSE)
1
+ # Graphlient
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/graphlient.svg)](https://badge.fury.io/rb/graphlient)
4
+ [![Build Status](https://github.com/ashkan18/graphlient/actions/workflows/ci.yml/badge.svg)](https://github.com/ashkan18/graphlient/actions/workflows/ci.yml)
5
+
6
+ A friendlier Ruby client for consuming GraphQL-based APIs. Built on top of your usual [graphql-client](https://github.com/github-community-projects/graphql-client), but with better defaults, more consistent error handling, and using the [faraday](https://github.com/lostisland/faraday) HTTP client.
7
+
8
+ # Table of Contents
9
+
10
+ - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [Schema Storing and Loading on Disk](#schema-storing-and-loading-on-disk)
13
+ - [Preloading Schema Once](#preloading-schema-once)
14
+ - [Error Handling](#error-handling)
15
+ - [Executing Parameterized Queries and Mutations](#executing-parameterized-queries-and-mutations)
16
+ - [Parse and Execute Queries Separately](#parse-and-execute-queries-separately)
17
+ - [Build Query Strings without Validation](#build-query-strings-without-validation)
18
+ - [Dynamic vs. Static Queries](#dynamic-vs-static-queries)
19
+ - [Generate Queries with Graphlient::Query](#generate-queries-with-graphlientquery)
20
+ - [Fragment Spreads and Definitions in the DSL](#fragment-spreads-and-definitions-in-the-dsl)
21
+ - [Inline Fragments in the DSL](#inline-fragments-in-the-dsl)
22
+ - [Directives in the DSL](#directives-in-the-dsl)
23
+ - [Custom Scalar Types](#custom-scalar-types)
24
+ - [Create API Client Classes with Graphlient::Extension::Query](#create-api-client-classes-with-graphlientextensionquery)
25
+ - [Swapping the HTTP Stack](#swapping-the-http-stack)
26
+ - [Testing with Graphlient and RSpec](#testing-with-graphlient-and-rspec)
27
+ - [License](#license)
28
+
29
+ ## Installation
30
+
31
+ Add the following line to your Gemfile.
32
+
33
+ ```ruby
34
+ gem 'graphlient'
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ Create a new instance of `Graphlient::Client` with a URL and optional headers/http_options.
40
+
41
+ ```ruby
42
+ client = Graphlient::Client.new('https://test-graphql.biz/graphql',
43
+ headers: {
44
+ 'Authorization' => 'Bearer 123'
45
+ },
46
+ http_options: {
47
+ read_timeout: 20,
48
+ write_timeout: 30
49
+ }
50
+ )
51
+ ```
52
+
53
+ | http_options | default | type |
54
+ | ------------- | ------- | ------- |
55
+ | read_timeout | nil | seconds |
56
+ | write_timeout | nil | seconds |
57
+
58
+ The schema is available automatically via `.schema`.
59
+
60
+ ```ruby
61
+ client.schema # GraphQL::Schema
62
+ ```
63
+
64
+ Make queries with `query`, which takes a String or a block for the query definition.
65
+
66
+ With a String.
67
+
68
+ ```ruby
69
+ response = client.query <<~GRAPHQL
70
+ query {
71
+ invoice(id: 10) {
72
+ id
73
+ total
74
+ line_items {
75
+ price
76
+ item_type
77
+ }
78
+ }
79
+ }
80
+ GRAPHQL
81
+ ```
82
+
83
+ With a block.
84
+
85
+ ```ruby
86
+ response = client.query do
87
+ query do
88
+ invoice(id: 10) do
89
+ id
90
+ total
91
+ line_items do
92
+ price
93
+ item_type
94
+ end
95
+ end
96
+ end
97
+ end
98
+ ```
99
+
100
+ This will call the endpoint setup in the configuration with `POST`, the `Authorization` header and `query` as follows.
101
+
102
+ ```graphql
103
+ query {
104
+ invoice(id: 10) {
105
+ id
106
+ total
107
+ line_items {
108
+ price
109
+ item_type
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ A successful response object always contains data which can be iterated upon. The following example returns the first line item's price.
116
+
117
+ ```ruby
118
+ response.data.invoice.line_items.first.price
119
+ ```
120
+
121
+ You can also execute mutations the same way.
122
+
123
+ ```ruby
124
+ response = client.query do
125
+ mutation do
126
+ createInvoice(input: { fee_in_cents: 12_345 }) do
127
+ id
128
+ fee_in_cents
129
+ end
130
+ end
131
+ end
132
+ ```
133
+
134
+ The successful response contains data in `response.data`. The following example returns the newly created invoice's ID.
135
+
136
+ ```ruby
137
+ response.data.create_invoice.first.id
138
+ ```
139
+
140
+ ### Schema storing and loading on disk
141
+
142
+ To reduce requests to graphql API you can cache schema:
143
+
144
+ ```ruby
145
+ client = Client.new(url, schema_path: 'config/your_graphql_schema.json')
146
+ client.schema.dump! # you only need to call this when graphql schema changes
147
+ ```
148
+
149
+ ### Preloading Schema Once
150
+
151
+ Even if caching the schema on disk, instantiating `Graphlient::Client` often can be both time and memory intensive due to loading the schema for each instance. This is especially true if the schema is a large file. To get around these performance issues, instantiate your schema once and pass it in as a configuration option.
152
+
153
+ One time in an initializer
154
+
155
+ ```ruby
156
+ schema = Graphlient::Schema.new(
157
+ 'https://graphql.foo.com/graphql', 'lib/graphql_schema_foo.json'
158
+ )
159
+ ```
160
+
161
+ Pass in each time you initialize a client
162
+
163
+ ```
164
+ client = Graphlient::Client.new(
165
+ 'https://graphql.foo.com/graphql',
166
+ schema: schema,
167
+ headers: {
168
+ 'Authorization' => 'Bearer 123',
169
+ }
170
+ )
171
+ ```
172
+
173
+ ### Error Handling
174
+
175
+ Unlike graphql-client, Graphlient will always raise an exception unless the query has succeeded.
176
+
177
+ * [Graphlient::Errors::ClientError](lib/graphlient/errors/client_error.rb): all client-side query validation failures based on current schema
178
+ * [Graphlient::Errors::GraphQLError](lib/graphlient/errors/graphql_error.rb): all GraphQL API errors, with a humanly readable collection of problems
179
+ * [Graphlient::Errors::ExecutionError](lib/graphlient/errors/execution_error.rb): all GraphQL execution errors, with a humanly readable collection of problems
180
+ * [Graphlient::Errors::ServerError](lib/graphlient/errors/server_error.rb): all transport errors raised by HTTP Adapters. You can access `inner_exception`, `status_code` and `response` on these errors to get more details on what went wrong
181
+ * [Graphlient::Errors::FaradayServerError](lib/graphlient/errors/faraday_server_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
182
+ * [Graphlient::Errors::HttpServerError](lib/graphlient/errors/http_server_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
183
+ * [Graphlient::Errors::ConnectionFailedError](lib/graphlient/errors/connection_failed_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
184
+ * [Graphlient::Errors::TimeoutError](lib/graphlient/errors/timeout_error.rb): all client-side timeouts raised by the Faraday adapter. This does not inherit from `ServerError` because no server response or status code is available; rescue `TimeoutError` separately
185
+ * [Graphlient::Errors::HttpOptionsError](lib/graphlient/errors/http_options_error.rb): all NoMethodError raised by HTTP Adapters when given options in `http_options` are invalid
186
+
187
+
188
+ All errors inherit from `Graphlient::Errors::Error` if you need to handle them in bulk.
189
+
190
+ ### Executing Parameterized Queries and Mutations
191
+
192
+ Graphlient can execute parameterized queries and mutations by providing variables as query parameters.
193
+
194
+ The following query accepts an array of IDs.
195
+
196
+ With a String.
197
+
198
+ ```ruby
199
+ query = <<-GRAPHQL
200
+ query($ids: [Int]) {
201
+ invoices(ids: $ids) {
202
+ id
203
+ fee_in_cents
204
+ }
205
+ }
206
+ GRAPHQL
207
+ variables = { ids: [42] }
208
+
209
+ client.query(query, variables)
210
+ ```
211
+
212
+ With a block.
213
+
214
+ ```ruby
215
+ client.query(ids: [42]) do
216
+ query(ids: [:int]) do
217
+ invoices(ids: :ids) do
218
+ id
219
+ fee_in_cents
220
+ end
221
+ end
222
+ end
223
+ ```
224
+
225
+ Graphlient supports following Scalar types for parameterized queries by default:
226
+
227
+ - `:id` maps to `ID`
228
+ - `:boolean` maps to `Boolean`
229
+ - `:float` maps to `Float`
230
+ - `:int` maps to `Int`
231
+ - `:string` maps to `String`
232
+
233
+ You can use any of the above types with `!` to make it required or use them in `[]` for array parameters.
234
+
235
+ For any other custom types, graphlient will simply use `to_s` of the symbol provided for the type, so `query(ids: [:InvoiceType!])` will result in `query($ids: [InvoiceType!])`.
236
+
237
+ The following mutation accepts a custom type that requires `fee_in_cents`.
238
+
239
+ ```ruby
240
+ client.query(input: { fee_in_cents: 12_345 }) do
241
+ mutation(input: :createInvoiceInput!) do
242
+ createInvoice(input: :input) do
243
+ id
244
+ fee_in_cents
245
+ end
246
+ end
247
+ end
248
+ ```
249
+
250
+ ### Parse and Execute Queries Separately
251
+
252
+ You can `parse` and `execute` queries separately with optional variables. This is highly recommended as parsing a query and validating a query on every request adds performance overhead. Parsing queries early allows validation errors to be discovered before request time and avoids many potential security issues.
253
+
254
+ ```ruby
255
+ # parse a query, returns a GraphQL::Client::OperationDefinition
256
+ query = client.parse do
257
+ query(ids: [:int]) do
258
+ invoices(ids: :ids) do
259
+ id
260
+ fee_in_cents
261
+ end
262
+ end
263
+ end
264
+
265
+ # execute a query, returns a GraphQL::Client::Response
266
+ client.execute query, ids: [42]
267
+ ```
268
+
269
+ Or pass in a string instead of a block:
270
+
271
+ ```ruby
272
+ # parse a query, returns a GraphQL::Client::OperationDefinition
273
+ query = client.parse <<~GRAPHQL
274
+ query($some_id: Int) {
275
+ invoice(id: $some_id) {
276
+ id
277
+ feeInCents
278
+ }
279
+ }
280
+ GRAPHQL
281
+
282
+ # execute a query, returns a GraphQL::Client::Response
283
+ client.execute query, ids: [42]
284
+ ```
285
+
286
+ ### Build Query Strings without Validation
287
+
288
+ `Client#to_query_string` serializes a DSL block into a GraphQL query string and
289
+ returns it as a plain `String`. It uses the same DSL serializer (`Graphlient::Query`)
290
+ that powers `client.query` and `client.parse`, but it stops there — no schema is
291
+ loaded, no graphql-client validation runs, no HTTP call is made.
292
+
293
+ ```ruby
294
+ client = Graphlient::Client.new('https://example.com/graphql',
295
+ headers: { 'Authorization' => 'Bearer 123' }
296
+ )
297
+
298
+ query_str = client.to_query_string do
299
+ query(id: :int) do
300
+ invoice(id: :id) do
301
+ id
302
+ feeInCents
303
+ end
304
+ end
305
+ end
306
+
307
+ # => "query($id: Int){\n invoice(id: $id){\n id\n feeInCents\n }\n }"
308
+ ```
309
+
310
+ For fragment-free queries the string can be fed back to `client.execute`:
311
+
312
+ ```ruby
313
+ client.execute(query_str, id: 42)
314
+ ```
315
+
316
+ Queries containing fragment spreads (`spread :Name` → `...Name`) cannot go back
317
+ through the gem — graphql-client requires fragments to be pre-registered module
318
+ constants, not named strings. Pass those directly to your own HTTP client instead.
319
+
320
+ This is also the escape hatch if you want to replace the graphql-client dependency
321
+ entirely. `to_query_string` gives you a standard GraphQL document — from there you
322
+ own the transport: Faraday, Net::HTTP, anything else. You get full control over
323
+ headers, retries, connection pooling, and middleware without any graphql-client
324
+ overhead.
325
+
326
+ ```ruby
327
+ conn = Faraday.new('https://example.com/graphql',
328
+ headers: { 'Authorization' => 'Bearer 123', 'Content-Type' => 'application/json' }
329
+ )
330
+ response = conn.post('/', { query: query_str, variables: { id: 42 } }.to_json)
331
+ ```
332
+
333
+ ### Dynamic vs. Static Queries
334
+
335
+ Graphlient uses [graphql-client](https://github.com/github-community-projects/graphql-client), which [recommends](https://github.com/github-community-projects/graphql-client/blob/master/guides/dynamic-query-error.md) building queries as static module members along with dynamic variables during execution. This can be accomplished with graphlient the same way.
336
+
337
+ Create a new instance of `Graphlient::Client` with a URL and optional headers.
338
+
339
+ ```ruby
340
+ module SWAPI
341
+ Client = Graphlient::Client.new('https://test-graphql.biz/graphql',
342
+ headers: {
343
+ 'Authorization' => 'Bearer 123'
344
+ },
345
+ allow_dynamic_queries: false
346
+ )
347
+ end
348
+ ```
349
+
350
+ The schema is available automatically via `.schema`.
351
+
352
+ ```ruby
353
+ SWAPI::Client.schema # GraphQL::Schema
354
+ ```
355
+
356
+ Define a query.
357
+
358
+ ```ruby
359
+ module SWAPI
360
+ InvoiceQuery = Client.parse do
361
+ query(id: :int) do
362
+ invoice(id: :id) do
363
+ id
364
+ fee_in_cents
365
+ end
366
+ end
367
+ end
368
+ end
369
+ ```
370
+
371
+ Execute the query.
372
+
373
+ ```ruby
374
+ response = SWAPI::Client.execute(SWAPI::InvoiceQuery, id: 42)
375
+ ```
376
+
377
+ Note that in the example above the client is created with `allow_dynamic_queries: false` (only allow static queries), while graphlient defaults to `allow_dynamic_queries: true` (allow dynamic queries). This option is marked deprecated, but we're proposing to remove it and default it to `true` in [graphql-client#128](https://github.com/github-community-projects/graphql-client/issues/128).
378
+
379
+ ### Generate Queries with Graphlient::Query
380
+
381
+ You can directly use `Graphlient::Query` to generate raw GraphQL queries.
382
+
383
+ ```ruby
384
+ query = Graphlient::Query.new do
385
+ query do
386
+ invoice(id: 10) do
387
+ line_items
388
+ end
389
+ end
390
+ end
391
+
392
+ query.to_s
393
+ # "\nquery {\n invoice(id: 10){\n line_items\n }\n }\n"
394
+ ```
395
+
396
+ ### Fragment Spreads and Definitions in the DSL
397
+
398
+ Use `spread` to insert a named fragment spread (`...FragmentName`) in a DSL block:
399
+
400
+ ```ruby
401
+ client.query do
402
+ query do
403
+ invoice(id: 10) do
404
+ id
405
+ spread :InvoiceFields # → ...InvoiceFields
406
+ end
407
+ end
408
+ end
409
+ ```
410
+
411
+ Define the fragment body inline with `fragment` — graphlient assembles the complete
412
+ query string automatically, no external tooling needed:
413
+
414
+ ```ruby
415
+ client.query do
416
+ fragment(:InvoiceFields, on: :Invoice) do
417
+ id
418
+ feeInCents
419
+ end
420
+
421
+ query do
422
+ invoice(id: 10) do
423
+ spread :InvoiceFields
424
+ end
425
+ end
426
+ end
427
+ ```
428
+
429
+ Produces and sends:
430
+
431
+ ```graphql
432
+ query {
433
+ invoice(id: 10) {
434
+ ...InvoiceFields
435
+ }
436
+ }
437
+
438
+ fragment InvoiceFields on Invoice {
439
+ id
440
+ feeInCents
441
+ }
442
+ ```
443
+
444
+ Because the fragment is defined inline in the same query block, graphql-client treats it
445
+ as part of the same document — all fragment fields are directly accessible on the response
446
+ wrapper with no extra wrapping step:
447
+
448
+ ```ruby
449
+ response = client.query do
450
+ fragment(:InvoiceFields, on: :Invoice) do
451
+ id
452
+ feeInCents
453
+ end
454
+
455
+ query do
456
+ invoice(id: 10) do
457
+ spread :InvoiceFields
458
+ end
459
+ end
460
+ end
461
+
462
+ response.data.invoice.id # 10
463
+ response.data.invoice.fee_in_cents # 20000
464
+ ```
465
+
466
+ Multiple fragments are supported. Fragments are scoped to the query call — no global
467
+ registry, no cross-contamination between requests.
468
+
469
+ You can also apply a directive to a spread (see [Directives in the DSL](#directives-in-the-dsl)):
470
+
471
+ ```ruby
472
+ spread :InvoiceFields, _skip(if: :skip_invoice)
473
+ # → ...InvoiceFields @skip(if: $skip_invoice)
474
+ ```
475
+
476
+ ### Use of Fragments (graphql-client style)
477
+
478
+ [Fragments](https://github.com/github-community-projects/graphql-client#defining-queries) should be referred by constant:
479
+
480
+ ```ruby
481
+ module Fragments
482
+ Invoice = client.parse <<~'GRAPHQL'
483
+ fragment on Invoice {
484
+ id
485
+ feeInCents
486
+ }
487
+ GRAPHQL
488
+ end
489
+ ```
490
+
491
+ `Graphlient` offers the syntax below to refer to the original constant:
492
+ * Triple underscore `___` to refer to the fragment
493
+ * Double underscore `__` for namespace separator
494
+
495
+ In this example, `Fragments::Invoice` would be referred as follows:
496
+
497
+ ```ruby
498
+ invoice_query = client.parse do
499
+ query do
500
+ invoice(id: 10) do
501
+ id
502
+ ___Fragments__Invoice
503
+ end
504
+ end
505
+ end
506
+ ```
507
+
508
+ The wrapped response only allows access to fields that have been explicitly asked for.
509
+ In this example, while `id` has been referenced directly in the main query, `feeInCents`
510
+ has been spread via an **external fragment constant** and trying to access it in the
511
+ original wrapped response will throw
512
+ [`GraphQL::Client::ImplicitlyFetchedFieldError`](https://github.com/github-community-projects/graphql-client/blob/master/guides/implicitly-fetched-field-error.md).
513
+ This is graphql-client's component-isolation mechanism: each fragment constant "owns" the
514
+ fields it declares, preventing accidental data access across component boundaries.
515
+
516
+ ```ruby
517
+ response = client.execute(invoice_query)
518
+ result = response.data.invoice
519
+ result.to_h
520
+ # {"id" => 10, "feeInCents"=> 20000}
521
+ result.id
522
+ # 10
523
+ result.fee_in_cents
524
+ # raises GraphQL::Client::ImplicitlyFetchedFieldError
525
+ ```
526
+
527
+ `feeInCents` cannot be fetched directly from the main query, but from the fragment as shown below:
528
+
529
+ ```ruby
530
+ invoice = Fragments::Invoice.new(result)
531
+ invoice.id
532
+ # 10
533
+ invoice.fee_in_cents
534
+ # 20000
535
+ ```
536
+
537
+ > **Note:** This component-isolation behaviour only applies to external fragment constants
538
+ > (the `___` / `__` pattern). Fragments defined inline via the `fragment` DSL in the same
539
+ > query block are not subject to this restriction — their fields are accessible directly
540
+ > on the operation response (see [Fragment Spreads and Definitions in the DSL](#fragment-spreads-and-definitions-in-the-dsl)).
541
+
542
+ ### Inline Fragments in the DSL
543
+
544
+ Use `spread(on: :TypeName)` for inline fragments (`... on Type { }`), useful for union
545
+ types and interface implementations. It's the same `spread` verb as named fragment
546
+ spreads, and the same `on:` keyword as `fragment(name, on:)`:
547
+
548
+ ```ruby
549
+ client.query do
550
+ query do
551
+ invoice(id: 10) do
552
+ spread(on: :PaidInvoice) do
553
+ amountPaid
554
+ end
555
+ spread(on: :UnpaidInvoice) do
556
+ amountDue
557
+ end
558
+ end
559
+ end
560
+ end
561
+ ```
562
+
563
+ Produces:
564
+
565
+ ```graphql
566
+ query {
567
+ invoice(id: 10) {
568
+ ... on PaidInvoice {
569
+ amountPaid
570
+ }
571
+ ... on UnpaidInvoice {
572
+ amountDue
573
+ }
574
+ }
575
+ }
576
+ ```
577
+
578
+ Directives can be applied to inline fragments too (see [Directives in the DSL](#directives-in-the-dsl)):
579
+
580
+ ```ruby
581
+ spread(_skip(if: :skip_drafts), on: :DraftInvoice) { draftId }
582
+ # → ... on DraftInvoice @skip(if: $skip_drafts) { draftId }
583
+ ```
584
+
585
+ ### Directives in the DSL
586
+
587
+ Apply GraphQL directives to fields, spreads, and inline fragments using the `_name`
588
+ convention — any method starting with `_` followed by a lowercase letter is treated
589
+ as a directive (`_skip` → `@skip`, `_include` → `@include`, `_myDirective` → `@myDirective`).
590
+
591
+ **On a field:**
592
+
593
+ ```ruby
594
+ client.query(some_id: :int, skip_fee: :boolean!) do
595
+ query(some_id: :int, skip_fee: :boolean!) do
596
+ invoice(id: :some_id) do
597
+ id
598
+ feeInCents _skip(if: :skip_fee) # → feeInCents @skip(if: $skip_fee)
599
+ end
600
+ end
601
+ end
602
+ ```
603
+
604
+ **On a fragment spread:**
605
+
606
+ ```ruby
607
+ spread :InvoiceFields, _skip(if: :skip_invoice)
608
+ # → ...InvoiceFields @skip(if: $skip_invoice)
609
+ ```
610
+
611
+ **On an inline fragment:**
612
+
613
+ ```ruby
614
+ spread(_skip(if: :skip_drafts), on: :DraftInvoice) { draftId }
615
+ # → ... on DraftInvoice @skip(if: $skip_drafts) { draftId }
616
+ ```
617
+
618
+ **Multiple directives on one field:**
619
+
620
+ ```ruby
621
+ feeInCents _skip(if: :skip_fee), _include(if: :show_cents)
622
+ # → feeInCents @skip(if: $skip_fee) @include(if: $show_cents)
623
+ ```
624
+
625
+ **No-argument directive:**
626
+
627
+ ```ruby
628
+ legacyField _deprecated
629
+ # → legacyField @deprecated
630
+ ```
631
+
632
+ The directive value is a plain Ruby method call that returns a `Directive` object
633
+ before the field method runs, ensuring the directive appears in the correct position
634
+ in the output string regardless of Ruby's evaluation order.
635
+
636
+ ### Custom Scalar Types
637
+
638
+ By default, graphlient maps `:int → Int`, `:float → Float`, `:string → String`, and
639
+ `:boolean → Boolean` for variable type declarations. Register additional scalar types
640
+ in the client initialiser block:
641
+
642
+ ```ruby
643
+ client = Graphlient::Client.new('https://example.com/graphql') do |c|
644
+ c.scalar :date, 'Date'
645
+ c.scalar :uuid, 'UUID'
646
+ c.scalar :decimal, 'Decimal'
647
+ end
648
+ ```
649
+
650
+ Use the registered symbol in variable declarations:
651
+
652
+ ```ruby
653
+ client.query(created_after: Date.today.iso8601, order_id: :uuid) do
654
+ query(created_after: :date, order_id: :uuid!) do
655
+ orders(created_after: :created_after, id: :order_id) do
656
+ id
657
+ total
658
+ end
659
+ end
660
+ end
661
+ # → query($created_after: Date, $order_id: UUID!) { ... }
662
+ ```
663
+
664
+ Non-null variants work with `!`: `:date!` → `Date!`.
665
+
666
+ ### Create API Client Classes with Graphlient::Extension::Query
667
+
668
+ You can include `Graphlient::Extensions::Query` in your class. This will add a new `method_missing` method to your context which will be used to generate GraphQL queries.
669
+
670
+ ```ruby
671
+ include Graphlient::Extensions::Query
672
+
673
+ query = query do
674
+ invoice(id: 10) do
675
+ line_items
676
+ end
677
+ end
678
+
679
+ query.to_s
680
+ # "\nquery{\n invoice(id: 10){\n line_items\n }\n }\n"
681
+ ```
682
+
683
+ ### Swapping the HTTP Stack
684
+
685
+ You can swap the default Faraday adapter for `Net::HTTP`.
686
+
687
+ ```ruby
688
+ client = Graphlient::Client.new('https://test-graphql.biz/graphql',
689
+ http: Graphlient::Adapters::HTTP::HTTPAdapter
690
+ )
691
+ ```
692
+
693
+ ### Testing with Graphlient and RSpec
694
+
695
+ Use Graphlient inside your RSpec tests in a Rails application or with `Rack::Test` against your actual application.
696
+
697
+ ```ruby
698
+ require 'spec_helper'
699
+
700
+ describe App do
701
+ include Rack::Test::Methods
702
+
703
+ def app
704
+ # ...
705
+ end
706
+
707
+ let(:client) do
708
+ Graphlient::Client.new('http://test-graphql.biz/graphql') do |client|
709
+ client.http do |h|
710
+ h.connection do |c|
711
+ c.adapter Faraday::Adapter::Rack, app
712
+ end
713
+ end
714
+ end
715
+ end
716
+
717
+ context 'an invoice' do
718
+ let(:result) do
719
+ client.query do
720
+ query do
721
+ invoice(id: 10) do
722
+ id
723
+ end
724
+ end
725
+ end
726
+ end
727
+
728
+ it 'can be retrieved' do
729
+ expect(result.data.invoice.id).to eq 10
730
+ end
731
+ end
732
+ end
733
+ ```
734
+
735
+ Alternately you can `stub_request` with Webmock.
736
+
737
+ ```ruby
738
+ describe App do
739
+ let(:url) { 'http://example.com/graphql' }
740
+ let(:client) { Graphlient::Client.new(url) }
741
+
742
+ before do
743
+ stub_request(:post, url).to_return(
744
+ status: 200,
745
+ body: DummySchema.execute(GraphQL::Introspection::INTROSPECTION_QUERY).to_json
746
+ )
747
+ end
748
+
749
+ it 'retrieves schema' do
750
+ expect(client.schema).to be_a Graphlient::Schema
751
+ end
752
+ end
753
+ ```
754
+
755
+ In order to stub the response to actual queries, [dump the schema into a JSON file](#schema-storing-and-loading-on-disk) and specify it via schema_path as follows.
756
+
757
+ ```ruby
758
+ describe App do
759
+ let(:url) { 'http://graph.biz/graphql' }
760
+ let(:client) { Graphlient::Client.new(url, schema_path: 'spec/support/fixtures/invoice_api.json') }
761
+ let(:query) do
762
+ <<~GRAPHQL
763
+ query{
764
+ invoice(id: 42) {
765
+ id
766
+ feeInCents
767
+ }
768
+ }
769
+ GRAPHQL
770
+ end
771
+ let(:json_response) do
772
+ {
773
+ 'data' => {
774
+ 'invoice' => {
775
+ 'id' => '42',
776
+ 'feeInCents' => 2000
777
+ }
778
+ }
779
+ }.to_json
780
+ end
781
+
782
+ before do
783
+ stub_request(:post, url).to_return(
784
+ status: 200,
785
+ body: json_response
786
+ )
787
+ end
788
+
789
+ it 'returns invoice fees' do
790
+ response = client.query(query)
791
+ expect(response.data).to be_truthy
792
+ expect(response.data.invoice.id).to eq('42')
793
+ expect(response.data.invoice.fee_in_cents).to eq(2000)
794
+ end
795
+ end
796
+ ```
797
+
798
+ ## License
799
+
800
+ MIT License, see [LICENSE](LICENSE)