graphql-client 0.18.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/graphql/client/collocated_enforcement.rb +1 -1
- data/lib/graphql/client/definition.rb +54 -42
- data/lib/graphql/client/definition_variables.rb +18 -11
- data/lib/graphql/client/document_types.rb +34 -20
- data/lib/graphql/client/erb.rb +1 -0
- data/lib/graphql/client/errors.rb +1 -1
- data/lib/graphql/client/http.rb +7 -1
- data/lib/graphql/client/log_subscriber.rb +14 -2
- data/lib/graphql/client/response.rb +8 -1
- data/lib/graphql/client/schema/enum_type.rb +17 -2
- data/lib/graphql/client/schema/object_type.rb +11 -11
- data/lib/graphql/client/schema.rb +3 -2
- data/lib/graphql/client/type_stack.rb +144 -0
- data/lib/graphql/client.rb +29 -10
- data/lib/rubocop/cop/graphql/heredoc.rb +3 -3
- data/lib/rubocop/cop/graphql/overfetch.rb +39 -23
- metadata +17 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aab80b9e2051c30118b8db5fc9dbd813926d8a180bf49371bf23e6a09aaaf4eb
|
|
4
|
+
data.tar.gz: 20de5ca5f098763481ffa6bfa79308b1db5339376878f98c7d3ecb8d4442d5cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81eb7e3021e3cdf8201e86971ed7d63f58d60e132d22b7c4ee35fd6b0a42fa730f4a9c9dd6638027f583d98638d270ef1d7994f0b5976ad4200a8c4164f58864
|
|
7
|
+
data.tar.gz: 7a752365bd27b213506bf6958c8accf9941f85bc8a7c73518950b431c1b7563584d29c0569f3a8a8a4c221e704c9ea3ff05333f005af004e60454b21f1acaaf9
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# graphql-client [](https://badge.fury.io/rb/graphql-client) [](https://github.com/github/graphql-client/actions?query=workflow)
|
|
1
|
+
# graphql-client [](https://badge.fury.io/rb/graphql-client) [](https://github.com/github-community-projects/graphql-client/actions?query=workflow)
|
|
2
2
|
|
|
3
3
|
GraphQL Client is a Ruby library for declaring, composing and executing GraphQL queries.
|
|
4
4
|
|
|
@@ -26,7 +26,7 @@ module GraphQL
|
|
|
26
26
|
return yield if Thread.current[:query_result_caller_location_ignore]
|
|
27
27
|
|
|
28
28
|
if (location.path != path) && !(WHITELISTED_GEM_NAMES.any? { |g| location.path.include?("gems/#{g}") })
|
|
29
|
-
error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://
|
|
29
|
+
error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://github.com/github-community-projects/graphql-client/blob/master/guides/collocated-call-sites.md")
|
|
30
30
|
error.set_backtrace(caller(2))
|
|
31
31
|
raise error
|
|
32
32
|
end
|
|
@@ -147,41 +147,58 @@ module GraphQL
|
|
|
147
147
|
# Internal: Nodes AST indexes.
|
|
148
148
|
def indexes
|
|
149
149
|
@indexes ||= begin
|
|
150
|
-
visitor =
|
|
151
|
-
definitions = index_node_definitions(visitor)
|
|
152
|
-
spreads = index_spreads(visitor)
|
|
150
|
+
visitor = DefinitionVisitor.new(document)
|
|
153
151
|
visitor.visit
|
|
154
|
-
{ definitions: definitions, spreads: spreads }
|
|
152
|
+
{ definitions: visitor.definitions, spreads: visitor.spreads }
|
|
155
153
|
end
|
|
156
154
|
end
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
class DefinitionVisitor < GraphQL::Language::Visitor
|
|
157
|
+
attr_reader :spreads, :definitions
|
|
159
158
|
|
|
160
|
-
def
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
schema_class.cast(obj.to_h, obj.errors)
|
|
166
|
-
else
|
|
167
|
-
raise TypeError, "unexpected #{obj.class}"
|
|
168
|
-
end
|
|
159
|
+
def initialize(doc)
|
|
160
|
+
super
|
|
161
|
+
@spreads = {}
|
|
162
|
+
@definitions = {}
|
|
163
|
+
@current_definition = nil
|
|
169
164
|
end
|
|
170
165
|
|
|
171
|
-
|
|
166
|
+
def on_field(node, parent)
|
|
167
|
+
@definitions[node] = @current_definition
|
|
168
|
+
@spreads[node] = get_spreads(node)
|
|
169
|
+
super
|
|
170
|
+
end
|
|
172
171
|
|
|
173
|
-
def
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
def on_fragment_definition(node, parent)
|
|
173
|
+
@current_definition = node
|
|
174
|
+
@definitions[node] = @current_definition
|
|
175
|
+
@spreads[node] = get_spreads(node)
|
|
176
|
+
super
|
|
177
|
+
ensure
|
|
178
|
+
@current_definition = nil
|
|
179
|
+
end
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
def on_operation_definition(node, parent)
|
|
182
|
+
@current_definition = node
|
|
183
|
+
@definitions[node] = @current_definition
|
|
184
|
+
@spreads[node] = get_spreads(node)
|
|
185
|
+
super
|
|
186
|
+
ensure
|
|
187
|
+
@current_definition = nil
|
|
188
|
+
end
|
|
183
189
|
|
|
184
|
-
|
|
190
|
+
def on_inline_fragment(node, parent)
|
|
191
|
+
@definitions[node] = @current_definition
|
|
192
|
+
super
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
EMPTY_SET = Set.new.freeze
|
|
198
|
+
|
|
199
|
+
def get_spreads(node)
|
|
200
|
+
node_spreads = flatten_spreads(node).map(&:name)
|
|
201
|
+
node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze
|
|
185
202
|
end
|
|
186
203
|
|
|
187
204
|
def flatten_spreads(node)
|
|
@@ -198,25 +215,20 @@ module GraphQL
|
|
|
198
215
|
end
|
|
199
216
|
spreads
|
|
200
217
|
end
|
|
218
|
+
end
|
|
201
219
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
definitions = {}
|
|
213
|
-
on_node = ->(node, _parent) { definitions[node] = current_definition }
|
|
214
|
-
visitor[GraphQL::Language::Nodes::Field] << on_node
|
|
215
|
-
visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node
|
|
216
|
-
visitor[GraphQL::Language::Nodes::InlineFragment] << on_node
|
|
217
|
-
visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node
|
|
218
|
-
definitions
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
def cast_object(obj)
|
|
223
|
+
if obj.class.is_a?(GraphQL::Client::Schema::ObjectType)
|
|
224
|
+
unless obj._spreads.include?(definition_node.name)
|
|
225
|
+
raise TypeError, "#{definition_node.name} is not included in #{obj.source_definition.name}"
|
|
226
|
+
end
|
|
227
|
+
schema_class.cast(obj.to_h, obj.errors)
|
|
228
|
+
else
|
|
229
|
+
raise TypeError, "unexpected #{obj.class}"
|
|
219
230
|
end
|
|
231
|
+
end
|
|
220
232
|
end
|
|
221
233
|
end
|
|
222
234
|
end
|
|
@@ -24,26 +24,33 @@ module GraphQL
|
|
|
24
24
|
|
|
25
25
|
sliced_document = GraphQL::Language::DefinitionSlice.slice(document, definition_name)
|
|
26
26
|
|
|
27
|
-
visitor =
|
|
28
|
-
|
|
27
|
+
visitor = VariablesVisitor.new(sliced_document, schema: schema)
|
|
28
|
+
visitor.visit
|
|
29
|
+
visitor.variables
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class VariablesVisitor < GraphQL::Language::Visitor
|
|
33
|
+
prepend GraphQL::Client::TypeStack
|
|
34
|
+
|
|
35
|
+
def initialize(*_args, **_kwargs)
|
|
36
|
+
super
|
|
37
|
+
@variables = {}
|
|
38
|
+
end
|
|
29
39
|
|
|
30
|
-
variables
|
|
40
|
+
attr_reader :variables
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
if definition =
|
|
34
|
-
existing_type = variables[node.name.to_sym]
|
|
42
|
+
def on_variable_identifier(node, parent)
|
|
43
|
+
if definition = @argument_definitions.last
|
|
44
|
+
existing_type = @variables[node.name.to_sym]
|
|
35
45
|
|
|
36
46
|
if existing_type && existing_type.unwrap != definition.type.unwrap
|
|
37
47
|
raise GraphQL::Client::ValidationError, "$#{node.name} was already declared as #{existing_type.unwrap}, but was #{definition.type.unwrap}"
|
|
38
48
|
elsif !(existing_type && existing_type.kind.non_null?)
|
|
39
|
-
variables[node.name.to_sym] = definition.type
|
|
49
|
+
@variables[node.name.to_sym] = definition.type
|
|
40
50
|
end
|
|
41
51
|
end
|
|
52
|
+
super
|
|
42
53
|
end
|
|
43
|
-
|
|
44
|
-
visitor.visit
|
|
45
|
-
|
|
46
|
-
variables
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
# Internal: Detect all variables used in a given operation or fragment
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "graphql"
|
|
3
|
+
require "graphql/client/type_stack"
|
|
3
4
|
|
|
4
5
|
module GraphQL
|
|
5
6
|
class Client
|
|
6
7
|
# Internal: Use schema to detect definition and field types.
|
|
7
8
|
module DocumentTypes
|
|
9
|
+
class AnalyzeTypesVisitor < GraphQL::Language::Visitor
|
|
10
|
+
prepend GraphQL::Client::TypeStack
|
|
11
|
+
attr_reader :fields
|
|
12
|
+
|
|
13
|
+
def initialize(*a, **kw)
|
|
14
|
+
@fields = {}
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def on_operation_definition(node, _parent)
|
|
19
|
+
@fields[node] = @object_types.last
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_fragment_definition(node, _parent)
|
|
24
|
+
@fields[node] = @object_types.last
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def on_inline_fragment(node, _parent)
|
|
29
|
+
@fields[node] = @object_types.last
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_field(node, _parent)
|
|
34
|
+
@fields[node] = @field_definitions.last.type
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
8
39
|
# Internal: Detect all types used in a given document
|
|
9
40
|
#
|
|
10
41
|
# schema - A GraphQL::Schema
|
|
@@ -20,32 +51,15 @@ module GraphQL
|
|
|
20
51
|
raise TypeError, "expected schema to be a GraphQL::Language::Nodes::Document, but was #{document.class}"
|
|
21
52
|
end
|
|
22
53
|
|
|
23
|
-
visitor =
|
|
24
|
-
type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor)
|
|
25
|
-
|
|
26
|
-
fields = {}
|
|
27
|
-
|
|
28
|
-
visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, _parent) do
|
|
29
|
-
fields[node] = type_stack.object_types.last
|
|
30
|
-
end
|
|
31
|
-
visitor[GraphQL::Language::Nodes::FragmentDefinition] << ->(node, _parent) do
|
|
32
|
-
fields[node] = type_stack.object_types.last
|
|
33
|
-
end
|
|
34
|
-
visitor[GraphQL::Language::Nodes::InlineFragment] << ->(node, _parent) do
|
|
35
|
-
fields[node] = type_stack.object_types.last
|
|
36
|
-
end
|
|
37
|
-
visitor[GraphQL::Language::Nodes::Field] << ->(node, _parent) do
|
|
38
|
-
fields[node] = type_stack.field_definitions.last.type
|
|
39
|
-
end
|
|
54
|
+
visitor = AnalyzeTypesVisitor.new(document, schema: schema)
|
|
40
55
|
visitor.visit
|
|
41
|
-
|
|
42
|
-
fields
|
|
56
|
+
visitor.fields
|
|
43
57
|
rescue StandardError => err
|
|
44
58
|
if err.is_a?(TypeError)
|
|
45
59
|
raise
|
|
46
60
|
end
|
|
47
61
|
# FIXME: TypeStack my crash on invalid documents
|
|
48
|
-
fields
|
|
62
|
+
visitor.fields
|
|
49
63
|
end
|
|
50
64
|
end
|
|
51
65
|
end
|
data/lib/graphql/client/erb.rb
CHANGED
data/lib/graphql/client/http.rb
CHANGED
|
@@ -14,7 +14,7 @@ module GraphQL
|
|
|
14
14
|
# Assumes GraphQL endpoint follows the express-graphql endpoint conventions.
|
|
15
15
|
# https://github.com/graphql/express-graphql#http-usage
|
|
16
16
|
#
|
|
17
|
-
# Production applications should consider implementing
|
|
17
|
+
# Production applications should consider implementing their own network
|
|
18
18
|
# adapter. This class exists for trivial stock usage and allows for minimal
|
|
19
19
|
# request header configuration.
|
|
20
20
|
class HTTP
|
|
@@ -45,6 +45,11 @@ module GraphQL
|
|
|
45
45
|
{}
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Public: full reponse from last request
|
|
49
|
+
#
|
|
50
|
+
# Returns Hash.
|
|
51
|
+
attr_reader :last_response
|
|
52
|
+
|
|
48
53
|
# Public: Make an HTTP request for GraphQL query.
|
|
49
54
|
#
|
|
50
55
|
# Implements Client's "execute" adapter interface.
|
|
@@ -71,6 +76,7 @@ module GraphQL
|
|
|
71
76
|
request.body = JSON.generate(body)
|
|
72
77
|
|
|
73
78
|
response = connection.request(request)
|
|
79
|
+
@last_response = response.to_hash
|
|
74
80
|
case response
|
|
75
81
|
when Net::HTTPOK, Net::HTTPBadRequest
|
|
76
82
|
JSON.parse(response.body)
|
|
@@ -16,11 +16,18 @@ module GraphQL
|
|
|
16
16
|
# GraphQL::Client::LogSubscriber.attach_to :graphql
|
|
17
17
|
#
|
|
18
18
|
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
19
|
+
SHOULD_USE_KWARGS = private_instance_methods.include?(:mode_from)
|
|
20
|
+
|
|
19
21
|
def query(event)
|
|
20
22
|
logger.info do
|
|
21
23
|
name = event.payload[:operation_name].gsub("__", "::")
|
|
22
24
|
type = event.payload[:operation_type].upcase
|
|
23
|
-
|
|
25
|
+
|
|
26
|
+
if SHOULD_USE_KWARGS
|
|
27
|
+
color("#{name} #{type} (#{event.duration.round(1)}ms)", nil, bold: true)
|
|
28
|
+
else
|
|
29
|
+
color("#{name} #{type} (#{event.duration.round(1)}ms)", nil, true)
|
|
30
|
+
end
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
logger.debug do
|
|
@@ -32,7 +39,12 @@ module GraphQL
|
|
|
32
39
|
logger.error do
|
|
33
40
|
name = event.payload[:operation_name].gsub("__", "::")
|
|
34
41
|
message = event.payload[:message]
|
|
35
|
-
|
|
42
|
+
|
|
43
|
+
if SHOULD_USE_KWARGS
|
|
44
|
+
color("#{name} ERROR: #{message}", nil, bold: true)
|
|
45
|
+
else
|
|
46
|
+
color("#{name} ERROR: #{message}", nil, true)
|
|
47
|
+
end
|
|
36
48
|
end
|
|
37
49
|
end
|
|
38
50
|
end
|
|
@@ -12,6 +12,7 @@ module GraphQL
|
|
|
12
12
|
# Returns Hash.
|
|
13
13
|
attr_reader :original_hash
|
|
14
14
|
alias_method :to_h, :original_hash
|
|
15
|
+
alias_method :to_hash, :original_hash
|
|
15
16
|
|
|
16
17
|
# Public: Wrapped ObjectType of data returned from the server.
|
|
17
18
|
#
|
|
@@ -30,12 +31,18 @@ module GraphQL
|
|
|
30
31
|
# Public: Hash of server specific extension metadata.
|
|
31
32
|
attr_reader :extensions
|
|
32
33
|
|
|
34
|
+
# Public: Complete response hash returned from server.
|
|
35
|
+
#
|
|
36
|
+
# Returns Hash
|
|
37
|
+
attr_reader :full_response
|
|
38
|
+
|
|
33
39
|
# Internal: Initialize base class.
|
|
34
|
-
def initialize(hash, data: nil, errors: Errors.new, extensions: {})
|
|
40
|
+
def initialize(hash, data: nil, errors: Errors.new, extensions: {}, full_response: nil)
|
|
35
41
|
@original_hash = hash
|
|
36
42
|
@data = data
|
|
37
43
|
@errors = errors
|
|
38
44
|
@extensions = extensions
|
|
45
|
+
@full_response = full_response
|
|
39
46
|
end
|
|
40
47
|
end
|
|
41
48
|
end
|
|
@@ -16,6 +16,10 @@ module GraphQL
|
|
|
16
16
|
@enum = enum
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def unknown_enum_value?
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
def respond_to_missing?(method_name, include_private = false)
|
|
20
24
|
if method_name[-1] == "?" && @enum.include?(method_name[0..-2])
|
|
21
25
|
true
|
|
@@ -37,6 +41,12 @@ module GraphQL
|
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
|
|
44
|
+
class UnexpectedEnumValue < String
|
|
45
|
+
def unknown_enum_value?
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
# Internal: Construct enum wrapper from another GraphQL::EnumType.
|
|
41
51
|
#
|
|
42
52
|
# type - GraphQL::EnumType instance
|
|
@@ -78,8 +88,13 @@ module GraphQL
|
|
|
78
88
|
def cast(value, _errors = nil)
|
|
79
89
|
case value
|
|
80
90
|
when String
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
if @values.key?(value)
|
|
92
|
+
@values[value]
|
|
93
|
+
elsif schema_module.raise_on_unknown_enum_value
|
|
94
|
+
raise Error, "unexpected enum value #{value}"
|
|
95
|
+
else
|
|
96
|
+
UnexpectedEnumValue.new(value).freeze
|
|
97
|
+
end
|
|
83
98
|
when NilClass
|
|
84
99
|
value
|
|
85
100
|
else
|
|
@@ -3,7 +3,6 @@ require "active_support/inflector"
|
|
|
3
3
|
require "graphql/client/error"
|
|
4
4
|
require "graphql/client/errors"
|
|
5
5
|
require "graphql/client/schema/base_type"
|
|
6
|
-
require "graphql/client/schema/possible_types"
|
|
7
6
|
|
|
8
7
|
module GraphQL
|
|
9
8
|
class Client
|
|
@@ -132,13 +131,13 @@ module GraphQL
|
|
|
132
131
|
case selected_ast_node
|
|
133
132
|
when GraphQL::Language::Nodes::InlineFragment
|
|
134
133
|
continue_selection = if selected_ast_node.type.nil?
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
true
|
|
135
|
+
else
|
|
136
|
+
type_condition = definition.client.get_type(selected_ast_node.type.name)
|
|
137
|
+
applicable_types = definition.client.possible_types(type_condition)
|
|
138
|
+
# continue if this object type is one of the types matching the fragment condition
|
|
139
|
+
applicable_types.include?(type)
|
|
140
|
+
end
|
|
142
141
|
|
|
143
142
|
if continue_selection
|
|
144
143
|
selected_ast_node.selections.each do |next_selected_ast_node|
|
|
@@ -192,6 +191,7 @@ module GraphQL
|
|
|
192
191
|
def to_h
|
|
193
192
|
@data
|
|
194
193
|
end
|
|
194
|
+
alias :to_hash :to_h
|
|
195
195
|
|
|
196
196
|
def _definer
|
|
197
197
|
@definer
|
|
@@ -255,13 +255,13 @@ module GraphQL
|
|
|
255
255
|
end
|
|
256
256
|
|
|
257
257
|
unless field
|
|
258
|
-
raise UnimplementedFieldError, "undefined field `#{e.name}' on #{type.graphql_name} type. https://
|
|
258
|
+
raise UnimplementedFieldError, "undefined field `#{e.name}' on #{type.graphql_name} type. https://github.com/github-community-projects/graphql-client/blob/master/guides/unimplemented-field-error.md"
|
|
259
259
|
end
|
|
260
260
|
|
|
261
261
|
if @data.key?(field.name)
|
|
262
|
-
raise ImplicitlyFetchedFieldError, "implicitly fetched field `#{field.name}' on #{type} type. https://
|
|
262
|
+
raise ImplicitlyFetchedFieldError, "implicitly fetched field `#{field.name}' on #{type} type. https://github.com/github-community-projects/graphql-client/blob/master/guides/implicitly-fetched-field-error.md"
|
|
263
263
|
else
|
|
264
|
-
raise UnfetchedFieldError, "unfetched field `#{field.name}' on #{type} type. https://
|
|
264
|
+
raise UnfetchedFieldError, "unfetched field `#{field.name}' on #{type} type. https://github.com/github-community-projects/graphql-client/blob/master/guides/unfetched-field-error.md"
|
|
265
265
|
end
|
|
266
266
|
end
|
|
267
267
|
end
|
|
@@ -43,7 +43,7 @@ module GraphQL
|
|
|
43
43
|
def set_class(type_name, klass)
|
|
44
44
|
class_name = normalize_type_name(type_name)
|
|
45
45
|
|
|
46
|
-
if
|
|
46
|
+
if const_defined?(class_name, false)
|
|
47
47
|
raise ArgumentError,
|
|
48
48
|
"Can't define #{class_name} to represent type #{type_name} " \
|
|
49
49
|
"because it's already defined"
|
|
@@ -66,9 +66,10 @@ module GraphQL
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
def self.generate(schema)
|
|
69
|
+
def self.generate(schema, raise_on_unknown_enum_value: true)
|
|
70
70
|
mod = Module.new
|
|
71
71
|
mod.extend ClassMethods
|
|
72
|
+
mod.define_singleton_method(:raise_on_unknown_enum_value) { raise_on_unknown_enum_value }
|
|
72
73
|
|
|
73
74
|
cache = {}
|
|
74
75
|
schema.types.each do |name, type|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
class Client
|
|
4
|
+
module TypeStack
|
|
5
|
+
# @return [GraphQL::Schema] the schema whose types are present in this document
|
|
6
|
+
attr_reader :schema
|
|
7
|
+
|
|
8
|
+
# When it enters an object (starting with query or mutation root), it's pushed on this stack.
|
|
9
|
+
# When it exits, it's popped off.
|
|
10
|
+
# @return [Array<GraphQL::ObjectType, GraphQL::Union, GraphQL::Interface>]
|
|
11
|
+
attr_reader :object_types
|
|
12
|
+
|
|
13
|
+
# When it enters a field, it's pushed on this stack (useful for nested fields, args).
|
|
14
|
+
# When it exits, it's popped off.
|
|
15
|
+
# @return [Array<GraphQL::Field>] fields which have been entered
|
|
16
|
+
attr_reader :field_definitions
|
|
17
|
+
|
|
18
|
+
# Directives are pushed on, then popped off while traversing the tree
|
|
19
|
+
# @return [Array<GraphQL::Node::Directive>] directives which have been entered
|
|
20
|
+
attr_reader :directive_definitions
|
|
21
|
+
|
|
22
|
+
# @return [Array<GraphQL::Node::Argument>] arguments which have been entered
|
|
23
|
+
attr_reader :argument_definitions
|
|
24
|
+
|
|
25
|
+
# @return [Array<String>] fields which have been entered (by their AST name)
|
|
26
|
+
attr_reader :path
|
|
27
|
+
|
|
28
|
+
# @param schema [GraphQL::Schema] the schema whose types to use when climbing this document
|
|
29
|
+
# @param visitor [GraphQL::Language::Visitor] a visitor to follow & watch the types
|
|
30
|
+
def initialize(document, schema:, **rest)
|
|
31
|
+
@schema = schema
|
|
32
|
+
@object_types = []
|
|
33
|
+
@field_definitions = []
|
|
34
|
+
@directive_definitions = []
|
|
35
|
+
@argument_definitions = []
|
|
36
|
+
@path = []
|
|
37
|
+
super(document, **rest)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_directive(node, parent)
|
|
41
|
+
directive_defn = @schema.directives[node.name]
|
|
42
|
+
@directive_definitions.push(directive_defn)
|
|
43
|
+
super(node, parent)
|
|
44
|
+
ensure
|
|
45
|
+
@directive_definitions.pop
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def on_field(node, parent)
|
|
49
|
+
parent_type = @object_types.last
|
|
50
|
+
parent_type = parent_type.unwrap
|
|
51
|
+
|
|
52
|
+
field_definition = @schema.get_field(parent_type, node.name)
|
|
53
|
+
@field_definitions.push(field_definition)
|
|
54
|
+
if !field_definition.nil?
|
|
55
|
+
next_object_type = field_definition.type
|
|
56
|
+
@object_types.push(next_object_type)
|
|
57
|
+
else
|
|
58
|
+
@object_types.push(nil)
|
|
59
|
+
end
|
|
60
|
+
@path.push(node.alias || node.name)
|
|
61
|
+
super(node, parent)
|
|
62
|
+
ensure
|
|
63
|
+
@field_definitions.pop
|
|
64
|
+
@object_types.pop
|
|
65
|
+
@path.pop
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def on_argument(node, parent)
|
|
69
|
+
if @argument_definitions.last
|
|
70
|
+
arg_type = @argument_definitions.last.type.unwrap
|
|
71
|
+
if arg_type.kind.input_object?
|
|
72
|
+
argument_defn = arg_type.arguments[node.name]
|
|
73
|
+
else
|
|
74
|
+
argument_defn = nil
|
|
75
|
+
end
|
|
76
|
+
elsif @directive_definitions.last
|
|
77
|
+
argument_defn = @directive_definitions.last.arguments[node.name]
|
|
78
|
+
elsif @field_definitions.last
|
|
79
|
+
argument_defn = @field_definitions.last.arguments[node.name]
|
|
80
|
+
else
|
|
81
|
+
argument_defn = nil
|
|
82
|
+
end
|
|
83
|
+
@argument_definitions.push(argument_defn)
|
|
84
|
+
@path.push(node.name)
|
|
85
|
+
super(node, parent)
|
|
86
|
+
ensure
|
|
87
|
+
@argument_definitions.pop
|
|
88
|
+
@path.pop
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def on_operation_definition(node, parent)
|
|
92
|
+
# eg, QueryType, MutationType
|
|
93
|
+
object_type = @schema.root_type_for_operation(node.operation_type)
|
|
94
|
+
@object_types.push(object_type)
|
|
95
|
+
@path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}")
|
|
96
|
+
super(node, parent)
|
|
97
|
+
ensure
|
|
98
|
+
@object_types.pop
|
|
99
|
+
@path.pop
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def on_inline_fragment(node, parent)
|
|
103
|
+
object_type = if node.type
|
|
104
|
+
@schema.get_type(node.type.name)
|
|
105
|
+
else
|
|
106
|
+
@object_types.last
|
|
107
|
+
end
|
|
108
|
+
if !object_type.nil?
|
|
109
|
+
object_type = object_type.unwrap
|
|
110
|
+
end
|
|
111
|
+
@object_types.push(object_type)
|
|
112
|
+
@path.push("...#{node.type ? " on #{node.type.to_query_string}" : ""}")
|
|
113
|
+
super(node, parent)
|
|
114
|
+
ensure
|
|
115
|
+
@object_types.pop
|
|
116
|
+
@path.pop
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def on_fragment_definition(node, parent)
|
|
120
|
+
object_type = if node.type
|
|
121
|
+
@schema.get_type(node.type.name)
|
|
122
|
+
else
|
|
123
|
+
@object_types.last
|
|
124
|
+
end
|
|
125
|
+
if !object_type.nil?
|
|
126
|
+
object_type = object_type.unwrap
|
|
127
|
+
end
|
|
128
|
+
@object_types.push(object_type)
|
|
129
|
+
@path.push("fragment #{node.name}")
|
|
130
|
+
super(node, parent)
|
|
131
|
+
ensure
|
|
132
|
+
@object_types.pop
|
|
133
|
+
@path.pop
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def on_fragment_spread(node, parent)
|
|
137
|
+
@path.push("... #{node.name}")
|
|
138
|
+
super(node, parent)
|
|
139
|
+
ensure
|
|
140
|
+
@path.pop
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/graphql/client.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
require "active_support/inflector"
|
|
3
3
|
require "active_support/notifications"
|
|
4
4
|
require "graphql"
|
|
5
|
+
require "graphql/client/type_stack"
|
|
5
6
|
require "graphql/client/collocated_enforcement"
|
|
6
7
|
require "graphql/client/definition_variables"
|
|
7
8
|
require "graphql/client/definition"
|
|
@@ -90,7 +91,7 @@ module GraphQL
|
|
|
90
91
|
result
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def initialize(schema:, execute: nil, enforce_collocated_callers: false)
|
|
94
|
+
def initialize(schema:, execute: nil, enforce_collocated_callers: false, raise_on_unknown_enum_value: true)
|
|
94
95
|
@schema = self.class.load_schema(schema)
|
|
95
96
|
@execute = execute
|
|
96
97
|
@document = GraphQL::Language::Nodes::Document.new(definitions: [])
|
|
@@ -99,8 +100,13 @@ module GraphQL
|
|
|
99
100
|
@enforce_collocated_callers = enforce_collocated_callers
|
|
100
101
|
if schema.is_a?(Class)
|
|
101
102
|
@possible_types = schema.possible_types
|
|
103
|
+
key, _types = @possible_types.first
|
|
104
|
+
# GraphQL-Ruby 2.3.5+ has classes here instead of strings
|
|
105
|
+
if key.is_a?(Module)
|
|
106
|
+
@possible_types = @possible_types.transform_keys(&:graphql_name)
|
|
107
|
+
end
|
|
102
108
|
end
|
|
103
|
-
@types = Schema.generate(@schema)
|
|
109
|
+
@types = Schema.generate(@schema, raise_on_unknown_enum_value: raise_on_unknown_enum_value)
|
|
104
110
|
end
|
|
105
111
|
|
|
106
112
|
# A cache of the schema's merged possible types
|
|
@@ -337,7 +343,7 @@ module GraphQL
|
|
|
337
343
|
end
|
|
338
344
|
|
|
339
345
|
if allow_dynamic_queries == false && definition.name.nil?
|
|
340
|
-
raise DynamicQueryError, "expected definition to be assigned to a static constant https://
|
|
346
|
+
raise DynamicQueryError, "expected definition to be assigned to a static constant https://github.com/github-community-projects/graphql-client/blob/master/guides/dynamic-query-error.md"
|
|
341
347
|
end
|
|
342
348
|
|
|
343
349
|
variables = deep_stringify_keys(variables)
|
|
@@ -374,12 +380,12 @@ module GraphQL
|
|
|
374
380
|
error_payload = payload.merge(message: error["message"], error: error)
|
|
375
381
|
ActiveSupport::Notifications.instrument("error.graphql", error_payload)
|
|
376
382
|
end
|
|
377
|
-
|
|
378
383
|
Response.new(
|
|
379
384
|
result,
|
|
380
385
|
data: definition.new(data, Errors.new(errors, ["data"])),
|
|
381
386
|
errors: Errors.new(errors),
|
|
382
|
-
extensions: extensions
|
|
387
|
+
extensions: extensions,
|
|
388
|
+
full_response: execute.respond_to?("last_response") ? execute.last_response : nil
|
|
383
389
|
)
|
|
384
390
|
end
|
|
385
391
|
|
|
@@ -402,7 +408,6 @@ module GraphQL
|
|
|
402
408
|
|
|
403
409
|
doc.definitions.map do |node|
|
|
404
410
|
deps = Set.new
|
|
405
|
-
definitions = document_dependencies.definitions.map { |x| [x.name, x] }.to_h
|
|
406
411
|
|
|
407
412
|
queue = [node.name]
|
|
408
413
|
while name = queue.shift
|
|
@@ -425,12 +430,26 @@ module GraphQL
|
|
|
425
430
|
end.to_h
|
|
426
431
|
end
|
|
427
432
|
|
|
433
|
+
class GatherNamesVisitor < GraphQL::Language::Visitor
|
|
434
|
+
def initialize(node)
|
|
435
|
+
@names = []
|
|
436
|
+
super
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
attr_reader :names
|
|
440
|
+
|
|
441
|
+
def on_fragment_spread(node, parent)
|
|
442
|
+
@names << node.name
|
|
443
|
+
super
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
428
447
|
def find_definition_dependencies(node)
|
|
429
|
-
|
|
430
|
-
visitor = Language::Visitor.new(node)
|
|
431
|
-
visitor[Language::Nodes::FragmentSpread] << -> (node, parent) { names << node.name }
|
|
448
|
+
visitor = GatherNamesVisitor.new(node)
|
|
432
449
|
visitor.visit
|
|
433
|
-
names.
|
|
450
|
+
names = visitor.names
|
|
451
|
+
names.uniq!
|
|
452
|
+
names
|
|
434
453
|
end
|
|
435
454
|
|
|
436
455
|
def deep_freeze_json_object(obj)
|
|
@@ -5,7 +5,7 @@ module RuboCop
|
|
|
5
5
|
module Cop
|
|
6
6
|
module GraphQL
|
|
7
7
|
# Public: Cop for enforcing non-interpolated GRAPHQL heredocs.
|
|
8
|
-
class Heredoc <
|
|
8
|
+
class Heredoc < Base
|
|
9
9
|
def on_dstr(node)
|
|
10
10
|
check_str(node)
|
|
11
11
|
end
|
|
@@ -19,11 +19,11 @@ module RuboCop
|
|
|
19
19
|
return unless node.location.expression.source =~ /^<<(-|~)?GRAPHQL/
|
|
20
20
|
|
|
21
21
|
node.each_child_node(:begin) do |begin_node|
|
|
22
|
-
add_offense(begin_node,
|
|
22
|
+
add_offense(begin_node, message: "Do not interpolate variables into GraphQL queries, " \
|
|
23
23
|
"used variables instead.")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
add_offense(node,
|
|
26
|
+
add_offense(node, message: "GraphQL heredocs should be quoted. <<-'GRAPHQL'")
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def autocorrect(node)
|
|
@@ -8,7 +8,7 @@ module RuboCop
|
|
|
8
8
|
module Cop
|
|
9
9
|
module GraphQL
|
|
10
10
|
# Public: Rubocop for catching overfetched fields in ERB templates.
|
|
11
|
-
class Overfetch <
|
|
11
|
+
class Overfetch < Base
|
|
12
12
|
if defined?(RangeHelp)
|
|
13
13
|
# rubocop 0.53 moved the #source_range method into this module
|
|
14
14
|
include RangeHelp
|
|
@@ -21,19 +21,12 @@ module RuboCop
|
|
|
21
21
|
query, = ::GraphQL::Client::ViewModule.extract_graphql_section(erb)
|
|
22
22
|
return unless query
|
|
23
23
|
|
|
24
|
-
aliases = {}
|
|
25
|
-
fields = {}
|
|
26
|
-
ranges = {}
|
|
27
|
-
|
|
28
24
|
# TODO: Use GraphQL client parser
|
|
29
25
|
document = ::GraphQL.parse(query.gsub(/::/, "__"))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
fields[name] ||= 0
|
|
35
|
-
field_aliases(name).each { |n| (aliases[n] ||= []) << name }
|
|
36
|
-
ranges[name] ||= source_range(processed_source.buffer, node.line, 0)
|
|
26
|
+
visitor = OverfetchVisitor.new(document) do |line_num|
|
|
27
|
+
# `source_range` is private to this object,
|
|
28
|
+
# so yield back out to it to get this info:
|
|
29
|
+
source_range(processed_source.buffer, line_num, 0)
|
|
37
30
|
end
|
|
38
31
|
visitor.visit
|
|
39
32
|
|
|
@@ -41,30 +34,53 @@ module RuboCop
|
|
|
41
34
|
method_names = method_names_for(*node)
|
|
42
35
|
|
|
43
36
|
method_names.each do |method_name|
|
|
44
|
-
aliases.fetch(method_name, []).each do |field_name|
|
|
45
|
-
fields[field_name] += 1
|
|
37
|
+
visitor.aliases.fetch(method_name, []).each do |field_name|
|
|
38
|
+
visitor.fields[field_name] += 1
|
|
46
39
|
end
|
|
47
40
|
end
|
|
48
41
|
end
|
|
49
42
|
|
|
50
|
-
fields.each do |field, count|
|
|
43
|
+
visitor.fields.each do |field, count|
|
|
51
44
|
next if count > 0
|
|
52
|
-
add_offense(
|
|
45
|
+
add_offense(visitor.ranges[field], message: "GraphQL field '#{field}' query but was not used in template.")
|
|
53
46
|
end
|
|
54
47
|
end
|
|
55
48
|
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
class OverfetchVisitor < ::GraphQL::Language::Visitor
|
|
50
|
+
def initialize(doc, &range_for_line)
|
|
51
|
+
super(doc)
|
|
52
|
+
@range_for_line = range_for_line
|
|
53
|
+
@fields = {}
|
|
54
|
+
@aliases = {}
|
|
55
|
+
@ranges = {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
attr_reader :fields, :aliases, :ranges
|
|
59
|
+
|
|
60
|
+
def on_field(node, parent)
|
|
61
|
+
name = node.alias || node.name
|
|
62
|
+
fields[name] ||= 0
|
|
63
|
+
field_aliases(name).each { |n| (aliases[n] ||= []) << name }
|
|
64
|
+
ranges[name] ||= @range_for_line.call(node.line)
|
|
65
|
+
super
|
|
66
|
+
end
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
names << "#{name}?"
|
|
68
|
+
private
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
def field_aliases(name)
|
|
71
|
+
names = Set.new
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
names << name
|
|
74
|
+
names << "#{name}?"
|
|
75
|
+
|
|
76
|
+
names << underscore_name = ActiveSupport::Inflector.underscore(name)
|
|
77
|
+
names << "#{underscore_name}?"
|
|
78
|
+
|
|
79
|
+
names
|
|
80
|
+
end
|
|
66
81
|
end
|
|
67
82
|
|
|
83
|
+
|
|
68
84
|
def method_names_for(*node)
|
|
69
85
|
receiver, method_name, *_args = node
|
|
70
86
|
method_names = []
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graphql-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.26.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitHub
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -30,14 +29,14 @@ dependencies:
|
|
|
30
29
|
requirements:
|
|
31
30
|
- - ">="
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
32
|
+
version: 1.13.0
|
|
34
33
|
type: :runtime
|
|
35
34
|
prerelease: false
|
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
36
|
requirements:
|
|
38
37
|
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
39
|
+
version: 1.13.0
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
42
41
|
name: actionpack
|
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -100,48 +99,42 @@ dependencies:
|
|
|
100
99
|
requirements:
|
|
101
100
|
- - "~>"
|
|
102
101
|
- !ruby/object:Gem::Version
|
|
103
|
-
version:
|
|
102
|
+
version: 13.2.1
|
|
104
103
|
type: :development
|
|
105
104
|
prerelease: false
|
|
106
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
106
|
requirements:
|
|
108
107
|
- - "~>"
|
|
109
108
|
- !ruby/object:Gem::Version
|
|
110
|
-
version:
|
|
109
|
+
version: 13.2.1
|
|
111
110
|
- !ruby/object:Gem::Dependency
|
|
112
111
|
name: rubocop-github
|
|
113
112
|
requirement: !ruby/object:Gem::Requirement
|
|
114
113
|
requirements:
|
|
115
|
-
- - "
|
|
116
|
-
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0.10'
|
|
118
|
-
- - "<="
|
|
114
|
+
- - ">="
|
|
119
115
|
- !ruby/object:Gem::Version
|
|
120
|
-
version: 0
|
|
116
|
+
version: '0'
|
|
121
117
|
type: :development
|
|
122
118
|
prerelease: false
|
|
123
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
124
120
|
requirements:
|
|
125
|
-
- - "
|
|
126
|
-
- !ruby/object:Gem::Version
|
|
127
|
-
version: '0.10'
|
|
128
|
-
- - "<="
|
|
121
|
+
- - ">="
|
|
129
122
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: 0
|
|
123
|
+
version: '0'
|
|
131
124
|
- !ruby/object:Gem::Dependency
|
|
132
125
|
name: rubocop
|
|
133
126
|
requirement: !ruby/object:Gem::Requirement
|
|
134
127
|
requirements:
|
|
135
128
|
- - "~>"
|
|
136
129
|
- !ruby/object:Gem::Version
|
|
137
|
-
version:
|
|
130
|
+
version: 1.75.8
|
|
138
131
|
type: :development
|
|
139
132
|
prerelease: false
|
|
140
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
141
134
|
requirements:
|
|
142
135
|
- - "~>"
|
|
143
136
|
- !ruby/object:Gem::Version
|
|
144
|
-
version:
|
|
137
|
+
version: 1.75.8
|
|
145
138
|
description: A Ruby library for declaring, composing and executing GraphQL queries
|
|
146
139
|
email: engineering@github.com
|
|
147
140
|
executables: []
|
|
@@ -182,14 +175,15 @@ files:
|
|
|
182
175
|
- lib/graphql/client/schema/scalar_type.rb
|
|
183
176
|
- lib/graphql/client/schema/skip_directive.rb
|
|
184
177
|
- lib/graphql/client/schema/union_type.rb
|
|
178
|
+
- lib/graphql/client/type_stack.rb
|
|
185
179
|
- lib/graphql/client/view_module.rb
|
|
186
180
|
- lib/rubocop/cop/graphql/heredoc.rb
|
|
187
181
|
- lib/rubocop/cop/graphql/overfetch.rb
|
|
188
|
-
homepage: https://github.com/github/graphql-client
|
|
182
|
+
homepage: https://github.com/github-community-projects/graphql-client
|
|
189
183
|
licenses:
|
|
190
184
|
- MIT
|
|
191
|
-
metadata:
|
|
192
|
-
|
|
185
|
+
metadata:
|
|
186
|
+
rubygems_mfa_required: 'true'
|
|
193
187
|
rdoc_options: []
|
|
194
188
|
require_paths:
|
|
195
189
|
- lib
|
|
@@ -204,8 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
204
198
|
- !ruby/object:Gem::Version
|
|
205
199
|
version: '0'
|
|
206
200
|
requirements: []
|
|
207
|
-
rubygems_version: 3.
|
|
208
|
-
signing_key:
|
|
201
|
+
rubygems_version: 3.6.7
|
|
209
202
|
specification_version: 4
|
|
210
203
|
summary: GraphQL Client
|
|
211
204
|
test_files: []
|