graphql 2.1.2 → 2.1.4

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2b3d848591e918d34df3851a973b6510912d6472f5847f493b5b0bfed4df997
4
- data.tar.gz: 3fcf17d2bc7259ef01cbe6d85aa23b31ccbed2321700c0474124e5eb6f0792d2
3
+ metadata.gz: 6c20f3924267df0e3f7b2a0b65a642572677d6f5cb041c37a098f8bb0cfaa32e
4
+ data.tar.gz: 8dacd2476d5471297a82348551ea8e34429d14edef9dbb1c2c8f34073c18abfc
5
5
  SHA512:
6
- metadata.gz: 8b2b258c108210008131e610ff0e486817ceebea1782d4f329f1f776ac8c70bdc03177f22a746c2f918d9a1c15e3b6e3971f2569cd3d2c15c6dd714e5a5d38d7
7
- data.tar.gz: 28e7f3c992987507e74e42320133b710aa6ca6af593dd2b6300359e30728ad415ff27e7671b5e8cfedb4b1a44324a06bc9fdd4ab95535c1f99e142aafebc3ea5
6
+ metadata.gz: f1fb9466a19755ce3e7f0ee9693428d1b2e5c8017287909e624975e49727376729ec5fda28318953ec6fda606a693f842c1caad5419dc410938a59506c994fae
7
+ data.tar.gz: d048fc02aece1781f4d15a700bc2cde8c1baf929917f35a9ae23567d5f720dabd5f4d18e47e3b8af2d9d6ecc6dde146f750a08b28de9fef769a960dc60ac0444
@@ -52,13 +52,6 @@ module GraphQL
52
52
  # { Class => Boolean }
53
53
  @lazy_cache = {}
54
54
  @lazy_cache.compare_by_identity
55
-
56
- @gathered_selections_cache = Hash.new { |h, k|
57
- cache = {}
58
- cache.compare_by_identity
59
- h[k] = cache
60
- }
61
- @gathered_selections_cache.compare_by_identity
62
55
  end
63
56
 
64
57
  def final_result
@@ -98,7 +91,7 @@ module GraphQL
98
91
  @response = nil
99
92
  else
100
93
  call_method_on_directives(:resolve, runtime_object, root_operation.directives) do # execute query level directives
101
- gathered_selections = gather_selections(runtime_object, root_type, nil, root_operation.selections)
94
+ gathered_selections = gather_selections(runtime_object, root_type, root_operation.selections)
102
95
  # This is kind of a hack -- `gathered_selections` is an Array if any of the selections
103
96
  # require isolation during execution (because of runtime directives). In that case,
104
97
  # make a new, isolated result hash for writing the result into. (That isolated response
@@ -143,18 +136,11 @@ module GraphQL
143
136
  nil
144
137
  end
145
138
 
146
- def gather_selections(owner_object, owner_type, ast_node_for_caching, selections, selections_to_run = nil, selections_by_name = nil)
147
- if ast_node_for_caching && (cached_selections = @gathered_selections_cache[ast_node_for_caching][owner_type])
148
- return cached_selections
149
- end
150
- selections_by_name ||= {} # allocate this default here so we check the cache first
151
-
152
- should_cache = true
139
+ def gather_selections(owner_object, owner_type, selections, selections_to_run = nil, selections_by_name = {})
153
140
 
154
141
  selections.each do |node|
155
142
  # Skip gathering this if the directive says so
156
143
  if !directives_include?(node, owner_object, owner_type)
157
- should_cache = false
158
144
  next
159
145
  end
160
146
 
@@ -179,7 +165,6 @@ module GraphQL
179
165
  if @runtime_directive_names.any? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) }
180
166
  next_selections = {}
181
167
  next_selections[:graphql_directives] = node.directives
182
- should_cache = false
183
168
  if selections_to_run
184
169
  selections_to_run << next_selections
185
170
  else
@@ -197,28 +182,24 @@ module GraphQL
197
182
  type_defn = schema.get_type(node.type.name, context)
198
183
 
199
184
  if query.warden.possible_types(type_defn).include?(owner_type)
200
- gather_selections(owner_object, owner_type, nil, node.selections, selections_to_run, next_selections)
185
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
201
186
  end
202
187
  else
203
188
  # it's an untyped fragment, definitely continue
204
- gather_selections(owner_object, owner_type, nil, node.selections, selections_to_run, next_selections)
189
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
205
190
  end
206
191
  when GraphQL::Language::Nodes::FragmentSpread
207
192
  fragment_def = query.fragments[node.name]
208
193
  type_defn = query.get_type(fragment_def.type.name)
209
194
  if query.warden.possible_types(type_defn).include?(owner_type)
210
- gather_selections(owner_object, owner_type, nil, fragment_def.selections, selections_to_run, next_selections)
195
+ gather_selections(owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections)
211
196
  end
212
197
  else
213
198
  raise "Invariant: unexpected selection class: #{node.class}"
214
199
  end
215
200
  end
216
201
  end
217
- result = selections_to_run || selections_by_name
218
- if should_cache
219
- @gathered_selections_cache[ast_node_for_caching][owner_type] = result
220
- end
221
- result
202
+ selections_to_run || selections_by_name
222
203
  end
223
204
 
224
205
  NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
@@ -619,7 +600,7 @@ module GraphQL
619
600
  response_hash = GraphQLResultHash.new(result_name, selection_result, is_non_null)
620
601
  set_result(selection_result, result_name, response_hash, true, is_non_null)
621
602
 
622
- gathered_selections = gather_selections(continue_value, current_type, ast_node, next_selections)
603
+ gathered_selections = gather_selections(continue_value, current_type, next_selections)
623
604
  # There are two possibilities for `gathered_selections`:
624
605
  # 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
625
606
  # This case is handled below, and the result can be written right into the main `response_hash` above.
@@ -80,6 +80,22 @@ module GraphQL
80
80
  selection(field_name, selected_type: selected_type, arguments: arguments).selected?
81
81
  end
82
82
 
83
+ # True if this node has a selection with alias matching `alias_name`.
84
+ # If `alias_name` is a String, it is treated as a GraphQL-style (camelized)
85
+ # field name and used verbatim. If `alias_name` is a Symbol, it is
86
+ # treated as a Ruby-style (underscored) name and camelized before comparing.
87
+ #
88
+ # If `arguments:` is provided, each provided key/value will be matched
89
+ # against the arguments in the next selection. This method will return false
90
+ # if any of the given `arguments:` are not present and matching in the next selection.
91
+ # (But, the next selection may contain _more_ than the given arguments.)
92
+ # @param alias_name [String, Symbol]
93
+ # @param arguments [Hash] Arguments which must match in the selection
94
+ # @return [Boolean]
95
+ def selects_alias?(alias_name, arguments: nil)
96
+ alias_selection(alias_name, arguments: arguments).selected?
97
+ end
98
+
83
99
  # @return [Boolean] True if this lookahead represents a field that was requested
84
100
  def selected?
85
101
  true
@@ -105,6 +121,7 @@ module GraphQL
105
121
  .tap(&:flatten!)
106
122
  end
107
123
 
124
+
108
125
  if (match_by_orig_name = all_fields.find { |f| f.original_name == field_name })
109
126
  match_by_orig_name
110
127
  else
@@ -114,23 +131,29 @@ module GraphQL
114
131
  @query.get_field(selected_type, guessed_name)
115
132
  end
116
133
  end
134
+ lookahead_for_selection(next_field_defn, selected_type, arguments)
135
+ end
117
136
 
118
- if next_field_defn
119
- next_nodes = []
120
- @ast_nodes.each do |ast_node|
121
- ast_node.selections.each do |selection|
122
- find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes)
123
- end
124
- end
137
+ # Like {#selection}, but for aliases.
138
+ # It returns a null object (check with {#selected?})
139
+ # @return [GraphQL::Execution::Lookahead]
140
+ def alias_selection(alias_name, selected_type: @selected_type, arguments: nil)
141
+ alias_cache_key = [alias_name, arguments]
142
+ return alias_selections[key] if alias_selections.key?(alias_name)
125
143
 
126
- if next_nodes.any?
127
- Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
128
- else
129
- NULL_LOOKAHEAD
130
- end
131
- else
132
- NULL_LOOKAHEAD
144
+ alias_node = lookup_alias_node(ast_nodes, alias_name)
145
+ return NULL_LOOKAHEAD unless alias_node
146
+
147
+ next_field_defn = @query.get_field(selected_type, alias_node.name)
148
+
149
+ alias_arguments = @query.arguments_for(alias_node, next_field_defn)
150
+ if alias_arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments)
151
+ alias_arguments = alias_arguments.keyword_arguments
133
152
  end
153
+
154
+ return NULL_LOOKAHEAD if arguments && arguments != alias_arguments
155
+
156
+ alias_selections[alias_cache_key] = lookahead_for_selection(next_field_defn, selected_type, alias_arguments, alias_name)
134
157
  end
135
158
 
136
159
  # Like {#selection}, but for all nodes.
@@ -258,7 +281,7 @@ module GraphQL
258
281
  end
259
282
  find_selections(subselections_by_type, subselections_on_type, on_type, ast_selection.selections, arguments)
260
283
  when GraphQL::Language::Nodes::FragmentSpread
261
- frag_defn = @query.fragments[ast_selection.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{ast_selection.name} (found: #{@query.fragments.keys})")
284
+ frag_defn = lookup_fragment(ast_selection)
262
285
  # Again, assuming a valid AST
263
286
  on_type = @query.get_type(frag_defn.type.name)
264
287
  subselections_on_type = subselections_by_type[on_type] ||= {}
@@ -271,11 +294,11 @@ module GraphQL
271
294
 
272
295
  # If a selection on `node` matches `field_name` (which is backed by `field_defn`)
273
296
  # and matches the `arguments:` constraints, then add that node to `matches`
274
- def find_selected_nodes(node, field_defn, arguments:, matches:)
297
+ def find_selected_nodes(node, field_name, field_defn, arguments:, matches:, alias_name: NOT_CONFIGURED)
275
298
  return if skipped_by_directive?(node)
276
299
  case node
277
300
  when GraphQL::Language::Nodes::Field
278
- if node.name == field_defn.graphql_name
301
+ if node.name == field_name && (NOT_CONFIGURED.equal?(alias_name) || node.alias == alias_name)
279
302
  if arguments.nil? || arguments.empty?
280
303
  # No constraint applied
281
304
  matches << node
@@ -284,10 +307,10 @@ module GraphQL
284
307
  end
285
308
  end
286
309
  when GraphQL::Language::Nodes::InlineFragment
287
- node.selections.each { |s| find_selected_nodes(s, field_defn, arguments: arguments, matches: matches) }
310
+ node.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name) }
288
311
  when GraphQL::Language::Nodes::FragmentSpread
289
- frag_defn = @query.fragments[node.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})")
290
- frag_defn.selections.each { |s| find_selected_nodes(s, field_defn, arguments: arguments, matches: matches) }
312
+ frag_defn = lookup_fragment(node)
313
+ frag_defn.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name) }
291
314
  else
292
315
  raise "Unexpected selection comparison on #{node.class.name} (#{node})"
293
316
  end
@@ -306,6 +329,50 @@ module GraphQL
306
329
  query_kwargs.key?(arg_name_sym) && query_kwargs[arg_name_sym] == arg_value
307
330
  end
308
331
  end
332
+
333
+ def lookahead_for_selection(field_defn, selected_type, arguments, alias_name = NOT_CONFIGURED)
334
+ return NULL_LOOKAHEAD unless field_defn
335
+
336
+ next_nodes = []
337
+ field_name = field_defn.name
338
+ @ast_nodes.each do |ast_node|
339
+ ast_node.selections.each do |selection|
340
+ find_selected_nodes(selection, field_name, field_defn, arguments: arguments, matches: next_nodes, alias_name: alias_name)
341
+ end
342
+ end
343
+
344
+ return NULL_LOOKAHEAD if next_nodes.empty?
345
+
346
+ Lookahead.new(query: @query, ast_nodes: next_nodes, field: field_defn, owner_type: selected_type)
347
+ end
348
+
349
+ def alias_selections
350
+ return @alias_selections if defined?(@alias_selections)
351
+ @alias_selections ||= {}
352
+ end
353
+
354
+ def lookup_alias_node(nodes, name)
355
+ return if nodes.empty?
356
+
357
+ nodes.flat_map(&:children)
358
+ .flat_map { |child| unwrap_fragments(child) }
359
+ .find { |child| child.is_a?(GraphQL::Language::Nodes::Field) && child.alias == name }
360
+ end
361
+
362
+ def unwrap_fragments(node)
363
+ case node
364
+ when GraphQL::Language::Nodes::InlineFragment
365
+ node.children
366
+ when GraphQL::Language::Nodes::FragmentSpread
367
+ lookup_fragment(node).children
368
+ else
369
+ [node]
370
+ end
371
+ end
372
+
373
+ def lookup_fragment(ast_selection)
374
+ @query.fragments[ast_selection.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{ast_selection.name} (found: #{@query.fragments.keys})")
375
+ end
309
376
  end
310
377
  end
311
378
  end
@@ -12,10 +12,14 @@ module GraphQL
12
12
  attr_reader :id
13
13
  # @return [Object] The value found with this ID
14
14
  attr_reader :object
15
- def initialize(argument:, id:, object:)
15
+ # @return [GraphQL::Query::Context]
16
+ attr_reader :context
17
+
18
+ def initialize(argument:, id:, object:, context:)
16
19
  @id = id
17
20
  @argument = argument
18
21
  @object = object
22
+ @context = context
19
23
  super("No object found for `#{argument.graphql_name}: #{id.inspect}`")
20
24
  end
21
25
  end
@@ -22,10 +22,11 @@ module GraphQL
22
22
  attr_reader :context
23
23
 
24
24
  def context=(new_ctx)
25
- current_runtime_state = Thread.current[:__graphql_runtime_info]
26
- query_runtime_state = current_runtime_state[new_ctx.query]
27
- @was_authorized_by_scope_items = query_runtime_state.was_authorized_by_scope_items
28
25
  @context = new_ctx
26
+ if @was_authorized_by_scope_items.nil?
27
+ @was_authorized_by_scope_items = detect_was_authorized_by_scope_items
28
+ end
29
+ @context
29
30
  end
30
31
 
31
32
  # @return [Object] the object this collection belongs to
@@ -90,13 +91,7 @@ module GraphQL
90
91
  else
91
92
  default_page_size
92
93
  end
93
- @was_authorized_by_scope_items = if @context
94
- current_runtime_state = Thread.current[:__graphql_runtime_info]
95
- query_runtime_state = current_runtime_state[@context.query]
96
- query_runtime_state.was_authorized_by_scope_items
97
- else
98
- nil
99
- end
94
+ @was_authorized_by_scope_items = detect_was_authorized_by_scope_items
100
95
  end
101
96
 
102
97
  def was_authorized_by_scope_items?
@@ -226,6 +221,16 @@ module GraphQL
226
221
 
227
222
  private
228
223
 
224
+ def detect_was_authorized_by_scope_items
225
+ if @context &&
226
+ (current_runtime_state = Thread.current[:__graphql_runtime_info]) &&
227
+ (query_runtime_state = current_runtime_state[@context.query])
228
+ query_runtime_state.was_authorized_by_scope_items
229
+ else
230
+ nil
231
+ end
232
+ end
233
+
229
234
  # @param argument [nil, Integer] `first` or `last`, as provided by the client
230
235
  # @param max_page_size [nil, Integer]
231
236
  # @return [nil, Integer] `nil` if the input was `nil`, otherwise a value between `0` and `max_page_size`
@@ -13,8 +13,7 @@ module GraphQL
13
13
  end
14
14
 
15
15
  def relation_count(relation)
16
- # Mongo's `.count` doesn't apply limit or skip, which we need. So we have to load _everything_!
17
- relation.to_a.count
16
+ relation.all.count(relation.options.slice(:limit, :skip))
18
17
  end
19
18
 
20
19
  def null_relation(relation)
@@ -237,6 +237,10 @@ module GraphQL
237
237
  @storage.key?(ns)
238
238
  end
239
239
 
240
+ def logger
241
+ @query && @query.logger
242
+ end
243
+
240
244
  def inspect
241
245
  "#<Query::Context ...>"
242
246
  end
@@ -3,6 +3,8 @@ module GraphQL
3
3
  class Query
4
4
  # This object can be `ctx` in places where there is no query
5
5
  class NullContext
6
+ include Singleton
7
+
6
8
  class NullQuery
7
9
  def after_lazy(value)
8
10
  yield(value)
@@ -27,16 +29,6 @@ module GraphQL
27
29
  def interpreter?
28
30
  true
29
31
  end
30
-
31
- class << self
32
- extend Forwardable
33
-
34
- def instance
35
- @instance ||= self.new
36
- end
37
-
38
- def_delegators :instance, :query, :warden, :schema, :interpreter?, :dataloader, :[], :fetch, :dig, :key?
39
- end
40
32
  end
41
33
  end
42
34
  end
data/lib/graphql/query.rb CHANGED
@@ -162,6 +162,14 @@ module GraphQL
162
162
 
163
163
  @result_values = nil
164
164
  @executed = false
165
+
166
+ @logger = if context && context[:logger] == false
167
+ Logger.new(IO::NULL)
168
+ elsif context && (l = context[:logger])
169
+ l
170
+ else
171
+ schema.default_logger
172
+ end
165
173
  end
166
174
 
167
175
  # If a document was provided to `GraphQL::Schema#execute` instead of the raw query string, we will need to get it from the document
@@ -369,6 +377,8 @@ module GraphQL
369
377
  end
370
378
  end
371
379
 
380
+ attr_reader :logger
381
+
372
382
  private
373
383
 
374
384
  def find_operation(operations, operation_name)
@@ -432,14 +432,12 @@ module GraphQL
432
432
  builder = self
433
433
 
434
434
  field_definitions.each do |field_definition|
435
- type_name = resolve_type_name(field_definition.type)
436
435
  resolve_method_name = -"resolve_field_#{field_definition.name}"
437
436
  schema_field_defn = owner.field(
438
437
  field_definition.name,
439
438
  description: field_definition.description,
440
439
  type: type_resolver.call(field_definition.type),
441
440
  null: true,
442
- connection: type_name.end_with?("Connection"),
443
441
  connection_extension: nil,
444
442
  deprecation_reason: build_deprecation_reason(field_definition.directives),
445
443
  ast_node: field_definition,
@@ -487,15 +485,6 @@ module GraphQL
487
485
  }
488
486
  resolve_type_proc
489
487
  end
490
-
491
- def resolve_type_name(type)
492
- case type
493
- when GraphQL::Language::Nodes::TypeName
494
- return type.name
495
- else
496
- resolve_type_name(type.of_type)
497
- end
498
- end
499
488
  end
500
489
 
501
490
  private_constant :Builder
@@ -6,6 +6,18 @@ module GraphQL
6
6
  description "Requires that exactly one field must be supplied and that field must not be `null`."
7
7
  locations(GraphQL::Schema::Directive::INPUT_OBJECT)
8
8
  default_directive true
9
+
10
+ def initialize(...)
11
+ super
12
+
13
+ owner.extend(IsOneOf)
14
+ end
15
+
16
+ module IsOneOf
17
+ def one_of?
18
+ true
19
+ end
20
+ end
9
21
  end
10
22
  end
11
23
  end
@@ -119,7 +119,7 @@ module GraphQL
119
119
  # - lazy resolution
120
120
  # Probably, those won't be needed here, since these are configuration arguments,
121
121
  # not runtime arguments.
122
- @arguments = self.class.coerce_arguments(nil, arguments, Query::NullContext)
122
+ @arguments = self.class.coerce_arguments(nil, arguments, Query::NullContext.instance)
123
123
  end
124
124
 
125
125
  def graphql_name
@@ -68,7 +68,7 @@ module GraphQL
68
68
  end
69
69
 
70
70
  # @return [Array<GraphQL::Schema::EnumValue>] Possible values of this enum
71
- def enum_values(context = GraphQL::Query::NullContext)
71
+ def enum_values(context = GraphQL::Query::NullContext.instance)
72
72
  inherited_values = superclass.respond_to?(:enum_values) ? superclass.enum_values(context) : nil
73
73
  visible_values = []
74
74
  warden = Warden.from_context(context)
@@ -110,7 +110,7 @@ module GraphQL
110
110
  end
111
111
 
112
112
  # @return [Hash<String => GraphQL::Schema::EnumValue>] Possible values of this enum, keyed by name.
113
- def values(context = GraphQL::Query::NullContext)
113
+ def values(context = GraphQL::Query::NullContext.instance)
114
114
  enum_values(context).each_with_object({}) { |val, obj| obj[val.graphql_name] = val }
115
115
  end
116
116
 
@@ -12,9 +12,10 @@ module GraphQL
12
12
  if ret_type.respond_to?(:scope_items)
13
13
  scoped_items = ret_type.scope_items(value, context)
14
14
  if !scoped_items.equal?(value) && !ret_type.reauthorize_scoped_objects
15
- current_runtime_state = Thread.current[:__graphql_runtime_info]
16
- query_runtime_state = current_runtime_state[context.query]
17
- query_runtime_state.was_authorized_by_scope_items = true
15
+ if (current_runtime_state = Thread.current[:__graphql_runtime_info]) &&
16
+ (query_runtime_state = current_runtime_state[context.query])
17
+ query_runtime_state.was_authorized_by_scope_items = true
18
+ end
18
19
  end
19
20
  scoped_items
20
21
  else
@@ -138,7 +138,7 @@ module GraphQL
138
138
  # As a last ditch, try to force loading the return type:
139
139
  type.unwrap.name
140
140
  end
141
- @connection = return_type_name.end_with?("Connection")
141
+ @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
142
142
  else
143
143
  @connection
144
144
  end
@@ -54,11 +54,11 @@ module GraphQL
54
54
  end
55
55
  end
56
56
 
57
- def field_arguments(context = GraphQL::Query::NullContext)
57
+ def field_arguments(context = GraphQL::Query::NullContext.instance)
58
58
  dummy.arguments(context)
59
59
  end
60
60
 
61
- def get_field_argument(name, context = GraphQL::Query::NullContext)
61
+ def get_field_argument(name, context = GraphQL::Query::NullContext.instance)
62
62
  dummy.get_argument(name, context)
63
63
  end
64
64
 
@@ -79,7 +79,7 @@ module GraphQL
79
79
  end
80
80
 
81
81
  def self.one_of?
82
- directives.any? { |d| d.is_a?(GraphQL::Schema::Directive::OneOf) }
82
+ false # Re-defined when `OneOf` is added
83
83
  end
84
84
 
85
85
  def unwrap_value(value)
@@ -20,6 +20,15 @@ module GraphQL
20
20
  # - Added as class methods to this interface
21
21
  # - Added as class methods to all child interfaces
22
22
  def definition_methods(&block)
23
+ # Use an instance variable to tell whether it's been included previously or not;
24
+ # You can't use constant detection because constants are brought into scope
25
+ # by `include`, which has already happened at this point.
26
+ if !defined?(@_definition_methods)
27
+ defn_methods_module = Module.new
28
+ @_definition_methods = defn_methods_module
29
+ const_set(:DefinitionMethods, defn_methods_module)
30
+ extend(self::DefinitionMethods)
31
+ end
23
32
  self::DefinitionMethods.module_eval(&block)
24
33
  end
25
34
 
@@ -47,20 +56,11 @@ module GraphQL
47
56
 
48
57
  child_class.type_membership_class(self.type_membership_class)
49
58
  child_class.ancestors.reverse_each do |ancestor|
50
- if ancestor.const_defined?(:DefinitionMethods)
59
+ if ancestor.const_defined?(:DefinitionMethods) && ancestor != child_class
51
60
  child_class.extend(ancestor::DefinitionMethods)
52
61
  end
53
62
  end
54
63
 
55
- # Use an instance variable to tell whether it's been included previously or not;
56
- # You can't use constant detection because constants are brought into scope
57
- # by `include`, which has already happened at this point.
58
- if !child_class.instance_variable_defined?(:@_definition_methods)
59
- defn_methods_module = Module.new
60
- child_class.instance_variable_set(:@_definition_methods, defn_methods_module)
61
- child_class.const_set(:DefinitionMethods, defn_methods_module)
62
- child_class.extend(child_class::DefinitionMethods)
63
- end
64
64
  child_class.introspection(introspection)
65
65
  child_class.description(description)
66
66
  # If interfaces are mixed into each other, only define this class once
@@ -176,7 +176,6 @@ module GraphQL
176
176
  while (of_type = unwrapped_field_hash["ofType"])
177
177
  unwrapped_field_hash = of_type
178
178
  end
179
- type_name = unwrapped_field_hash["name"]
180
179
 
181
180
  type_defn.field(
182
181
  field_hash["name"],
@@ -186,7 +185,6 @@ module GraphQL
186
185
  null: true,
187
186
  camelize: false,
188
187
  connection_extension: nil,
189
- connection: type_name.end_with?("Connection"),
190
188
  ) do
191
189
  if field_hash["args"].any?
192
190
  loader.build_arguments(self, field_hash["args"], type_resolver)
@@ -109,7 +109,7 @@ module GraphQL
109
109
  end
110
110
 
111
111
  # @return [Hash<String => GraphQL::Schema::Argument] Arguments defined on this thing, keyed by name. Includes inherited definitions
112
- def arguments(context = GraphQL::Query::NullContext)
112
+ def arguments(context = GraphQL::Query::NullContext.instance)
113
113
  if own_arguments.any?
114
114
  own_arguments_that_apply = {}
115
115
  own_arguments.each do |name, args_entry|
@@ -133,7 +133,7 @@ module GraphQL
133
133
  end
134
134
 
135
135
  module InheritedArguments
136
- def arguments(context = GraphQL::Query::NullContext)
136
+ def arguments(context = GraphQL::Query::NullContext.instance)
137
137
  own_arguments = super
138
138
  inherited_arguments = superclass.arguments(context)
139
139
 
@@ -166,7 +166,7 @@ module GraphQL
166
166
  end
167
167
 
168
168
 
169
- def get_argument(argument_name, context = GraphQL::Query::NullContext)
169
+ def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
170
170
  warden = Warden.from_context(context)
171
171
  for ancestor in ancestors
172
172
  if ancestor.respond_to?(:own_arguments) &&
@@ -181,7 +181,7 @@ module GraphQL
181
181
  end
182
182
 
183
183
  module FieldConfigured
184
- def arguments(context = GraphQL::Query::NullContext)
184
+ def arguments(context = GraphQL::Query::NullContext.instance)
185
185
  own_arguments = super
186
186
  if @resolver_class
187
187
  inherited_arguments = @resolver_class.field_arguments(context)
@@ -236,7 +236,7 @@ module GraphQL
236
236
  end
237
237
 
238
238
  # @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name.
239
- def get_argument(argument_name, context = GraphQL::Query::NullContext)
239
+ def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
240
240
  warden = Warden.from_context(context)
241
241
  if (arg_config = own_arguments[argument_name]) && (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden))
242
242
  visible_arg
@@ -379,44 +379,52 @@ module GraphQL
379
379
  def authorize_application_object(argument, id, context, loaded_application_object)
380
380
  context.query.after_lazy(loaded_application_object) do |application_object|
381
381
  if application_object.nil?
382
- err = GraphQL::LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
383
- load_application_object_failed(err)
382
+ err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object)
383
+ application_object = load_application_object_failed(err)
384
384
  end
385
385
  # Double-check that the located object is actually of this type
386
386
  # (Don't want to allow arbitrary access to objects this way)
387
- maybe_lazy_resolve_type = context.schema.resolve_type(argument.loads, application_object, context)
388
- context.query.after_lazy(maybe_lazy_resolve_type) do |resolve_type_result|
389
- if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2
390
- application_object_type, application_object = resolve_type_result
391
- else
392
- application_object_type = resolve_type_result
393
- # application_object is already assigned
394
- end
387
+ if application_object.nil?
388
+ nil
389
+ else
390
+ maybe_lazy_resolve_type = context.schema.resolve_type(argument.loads, application_object, context)
391
+ context.query.after_lazy(maybe_lazy_resolve_type) do |resolve_type_result|
392
+ if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2
393
+ application_object_type, application_object = resolve_type_result
394
+ else
395
+ application_object_type = resolve_type_result
396
+ # application_object is already assigned
397
+ end
395
398
 
396
- if !(
397
- context.warden.possible_types(argument.loads).include?(application_object_type) ||
398
- context.warden.loadable?(argument.loads, context)
399
- )
400
- err = GraphQL::LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
401
- load_application_object_failed(err)
402
- else
403
- # This object was loaded successfully
404
- # and resolved to the right type,
405
- # now apply the `.authorized?` class method if there is one
406
- context.query.after_lazy(application_object_type.authorized?(application_object, context)) do |authed|
407
- if authed
408
- application_object
409
- else
410
- err = GraphQL::UnauthorizedError.new(
411
- object: application_object,
412
- type: application_object_type,
413
- context: context,
414
- )
415
- if self.respond_to?(:unauthorized_object)
416
- err.set_backtrace(caller)
417
- unauthorized_object(err)
399
+ if !(
400
+ context.warden.possible_types(argument.loads).include?(application_object_type) ||
401
+ context.warden.loadable?(argument.loads, context)
402
+ )
403
+ err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object)
404
+ application_object = load_application_object_failed(err)
405
+ end
406
+
407
+ if application_object.nil?
408
+ nil
409
+ else
410
+ # This object was loaded successfully
411
+ # and resolved to the right type,
412
+ # now apply the `.authorized?` class method if there is one
413
+ context.query.after_lazy(application_object_type.authorized?(application_object, context)) do |authed|
414
+ if authed
415
+ application_object
418
416
  else
419
- raise err
417
+ err = GraphQL::UnauthorizedError.new(
418
+ object: application_object,
419
+ type: application_object_type,
420
+ context: context,
421
+ )
422
+ if self.respond_to?(:unauthorized_object)
423
+ err.set_backtrace(caller)
424
+ unauthorized_object(err)
425
+ else
426
+ raise err
427
+ end
420
428
  end
421
429
  end
422
430
  end
@@ -97,7 +97,7 @@ module GraphQL
97
97
  end
98
98
 
99
99
  module InterfaceMethods
100
- def get_field(field_name, context = GraphQL::Query::NullContext)
100
+ def get_field(field_name, context = GraphQL::Query::NullContext.instance)
101
101
  warden = Warden.from_context(context)
102
102
  for ancestor in ancestors
103
103
  if ancestor.respond_to?(:own_fields) &&
@@ -110,7 +110,7 @@ module GraphQL
110
110
  end
111
111
 
112
112
  # @return [Hash<String => GraphQL::Schema::Field>] Fields on this object, keyed by name, including inherited fields
113
- def fields(context = GraphQL::Query::NullContext)
113
+ def fields(context = GraphQL::Query::NullContext.instance)
114
114
  warden = Warden.from_context(context)
115
115
  # Local overrides take precedence over inherited fields
116
116
  visible_fields = {}
@@ -130,7 +130,7 @@ module GraphQL
130
130
  end
131
131
 
132
132
  module ObjectMethods
133
- def get_field(field_name, context = GraphQL::Query::NullContext)
133
+ def get_field(field_name, context = GraphQL::Query::NullContext.instance)
134
134
  # Objects need to check that the interface implementation is visible, too
135
135
  warden = Warden.from_context(context)
136
136
  ancs = ancestors
@@ -148,7 +148,7 @@ module GraphQL
148
148
  end
149
149
 
150
150
  # @return [Hash<String => GraphQL::Schema::Field>] Fields on this object, keyed by name, including inherited fields
151
- def fields(context = GraphQL::Query::NullContext)
151
+ def fields(context = GraphQL::Query::NullContext.instance)
152
152
  # Objects need to check that the interface implementation is visible, too
153
153
  warden = Warden.from_context(context)
154
154
  # Local overrides take precedence over inherited fields
@@ -70,7 +70,7 @@ module GraphQL
70
70
  end
71
71
 
72
72
  module InheritedInterfaces
73
- def interfaces(context = GraphQL::Query::NullContext)
73
+ def interfaces(context = GraphQL::Query::NullContext.instance)
74
74
  visible_interfaces = super
75
75
  inherited_interfaces = superclass.interfaces(context)
76
76
  if visible_interfaces.any?
@@ -99,7 +99,7 @@ module GraphQL
99
99
  end
100
100
 
101
101
  # param context [Query::Context] If omitted, skip filtering.
102
- def interfaces(context = GraphQL::Query::NullContext)
102
+ def interfaces(context = GraphQL::Query::NullContext.instance)
103
103
  warden = Warden.from_context(context)
104
104
  visible_interfaces = nil
105
105
  own_interface_type_memberships.each do |type_membership|
@@ -17,15 +17,15 @@ module GraphQL
17
17
  end
18
18
 
19
19
  def valid_isolated_input?(v)
20
- valid_input?(v, GraphQL::Query::NullContext)
20
+ valid_input?(v, GraphQL::Query::NullContext.instance)
21
21
  end
22
22
 
23
23
  def coerce_isolated_input(v)
24
- coerce_input(v, GraphQL::Query::NullContext)
24
+ coerce_input(v, GraphQL::Query::NullContext.instance)
25
25
  end
26
26
 
27
27
  def coerce_isolated_result(v)
28
- coerce_result(v, GraphQL::Query::NullContext)
28
+ coerce_result(v, GraphQL::Query::NullContext.instance)
29
29
  end
30
30
  end
31
31
  end
@@ -205,12 +205,12 @@ module GraphQL
205
205
  end
206
206
  end
207
207
 
208
- def get_argument(name, context = GraphQL::Query::NullContext)
208
+ def get_argument(name, context = GraphQL::Query::NullContext.instance)
209
209
  self.class.get_argument(name, context)
210
210
  end
211
211
 
212
212
  class << self
213
- def field_arguments(context = GraphQL::Query::NullContext)
213
+ def field_arguments(context = GraphQL::Query::NullContext.instance)
214
214
  arguments(context)
215
215
  end
216
216
 
@@ -218,7 +218,7 @@ module GraphQL
218
218
  any_arguments?
219
219
  end
220
220
 
221
- def get_field_argument(name, context = GraphQL::Query::NullContext)
221
+ def get_field_argument(name, context = GraphQL::Query::NullContext.instance)
222
222
  get_argument(name, context)
223
223
  end
224
224
 
@@ -10,7 +10,7 @@ module GraphQL
10
10
  super
11
11
  end
12
12
 
13
- def possible_types(*types, context: GraphQL::Query::NullContext, **options)
13
+ def possible_types(*types, context: GraphQL::Query::NullContext.instance, **options)
14
14
  if types.any?
15
15
  types.each do |t|
16
16
  assert_valid_union_member(t)
@@ -106,7 +106,7 @@ module GraphQL
106
106
  @types = @visible_types = @reachable_types = @visible_parent_fields =
107
107
  @visible_possible_types = @visible_fields = @visible_arguments = @visible_enum_arrays =
108
108
  @visible_enum_values = @visible_interfaces = @type_visibility = @type_memberships =
109
- @visible_and_reachable_type = @unions = @unfiltered_interfaces = @references_to =
109
+ @visible_and_reachable_type = @unions = @unfiltered_interfaces =
110
110
  @reachable_type_set =
111
111
  nil
112
112
  end
@@ -291,7 +291,14 @@ module GraphQL
291
291
  if type_defn.kind.union?
292
292
  possible_types(type_defn).any? && (referenced?(type_defn) || orphan_type?(type_defn))
293
293
  elsif type_defn.kind.interface?
294
- possible_types(type_defn).any?
294
+ if possible_types(type_defn).any?
295
+ true
296
+ else
297
+ if @context.respond_to?(:logger) && (logger = @context.logger)
298
+ logger.debug { "Interface `#{type_defn.graphql_name}` hidden because it has no visible implementors" }
299
+ end
300
+ false
301
+ end
295
302
  else
296
303
  if referenced?(type_defn)
297
304
  true
@@ -356,14 +363,11 @@ module GraphQL
356
363
  end
357
364
 
358
365
  def referenced?(type_defn)
359
- @references_to ||= @schema.references_to
360
366
  graphql_name = type_defn.unwrap.graphql_name
361
- members = @references_to[graphql_name] || NO_REFERENCES
367
+ members = @schema.references_to(graphql_name)
362
368
  members.any? { |m| visible?(m) }
363
369
  end
364
370
 
365
- NO_REFERENCES = [].freeze
366
-
367
371
  def orphan_type?(type_defn)
368
372
  @schema.orphan_types.include?(type_defn)
369
373
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "logger"
2
3
  require "graphql/schema/addition"
3
4
  require "graphql/schema/always_visible"
4
5
  require "graphql/schema/base_64_encoder"
@@ -170,9 +171,9 @@ module GraphQL
170
171
  end
171
172
 
172
173
  # @return [Class] Return the trace class to use for this mode, looking one up on the superclass if this Schema doesn't have one defined.
173
- def trace_class_for(mode, build: false)
174
+ def trace_class_for(mode)
174
175
  own_trace_modes[mode] ||
175
- (superclass.respond_to?(:trace_class_for) ? superclass.trace_class_for(mode) : nil)
176
+ (superclass.respond_to?(:trace_class_for) ? superclass.trace_class_for(mode) : (own_trace_modes[mode] = build_trace_mode(mode)))
176
177
  end
177
178
 
178
179
  # Configure `trace_class` to be used whenever `context: { trace_mode: mode_name }` is requested.
@@ -325,7 +326,7 @@ module GraphQL
325
326
  # Build a map of `{ name => type }` and return it
326
327
  # @return [Hash<String => Class>] A dictionary of type classes by their GraphQL name
327
328
  # @see get_type Which is more efficient for finding _one type_ by name, because it doesn't merge hashes.
328
- def types(context = GraphQL::Query::NullContext)
329
+ def types(context = GraphQL::Query::NullContext.instance)
329
330
  all_types = non_introspection_types.merge(introspection_system.types)
330
331
  visible_types = {}
331
332
  all_types.each do |k, v|
@@ -352,7 +353,7 @@ module GraphQL
352
353
 
353
354
  # @param type_name [String]
354
355
  # @return [Module, nil] A type, or nil if there's no type called `type_name`
355
- def get_type(type_name, context = GraphQL::Query::NullContext)
356
+ def get_type(type_name, context = GraphQL::Query::NullContext.instance)
356
357
  local_entry = own_types[type_name]
357
358
  type_defn = case local_entry
358
359
  when nil
@@ -483,7 +484,7 @@ module GraphQL
483
484
  # @param type [Module] The type definition whose possible types you want to see
484
485
  # @return [Hash<String, Module>] All possible types, if no `type` is given.
485
486
  # @return [Array<Module>] Possible types for `type`, if it's given.
486
- def possible_types(type = nil, context = GraphQL::Query::NullContext)
487
+ def possible_types(type = nil, context = GraphQL::Query::NullContext.instance)
487
488
  if type
488
489
  # TODO duck-typing `.possible_types` would probably be nicer here
489
490
  if type.kind.union?
@@ -536,18 +537,17 @@ module GraphQL
536
537
  attr_writer :dataloader_class
537
538
 
538
539
  def references_to(to_type = nil, from: nil)
539
- @own_references_to ||= Hash.new { |h, k| h[k] = [] }
540
+ @own_references_to ||= {}
540
541
  if to_type
541
542
  if !to_type.is_a?(String)
542
543
  to_type = to_type.graphql_name
543
544
  end
544
545
 
545
546
  if from
546
- @own_references_to[to_type] << from
547
+ refs = @own_references_to[to_type] ||= []
548
+ refs << from
547
549
  else
548
- own_refs = @own_references_to[to_type]
549
- inherited_refs = find_inherited_value(:references_to, EMPTY_HASH)[to_type] || EMPTY_ARRAY
550
- own_refs + inherited_refs
550
+ get_references_to(to_type) || EMPTY_ARRAY
551
551
  end
552
552
  else
553
553
  # `@own_references_to` can be quite large for big schemas,
@@ -567,7 +567,7 @@ module GraphQL
567
567
  GraphQL::Schema::TypeExpression.build_type(type_owner, ast_node)
568
568
  end
569
569
 
570
- def get_field(type_or_name, field_name, context = GraphQL::Query::NullContext)
570
+ def get_field(type_or_name, field_name, context = GraphQL::Query::NullContext.instance)
571
571
  parent_type = case type_or_name
572
572
  when LateBoundType
573
573
  get_type(type_or_name.name, context)
@@ -590,7 +590,7 @@ module GraphQL
590
590
  end
591
591
  end
592
592
 
593
- def get_fields(type, context = GraphQL::Query::NullContext)
593
+ def get_fields(type, context = GraphQL::Query::NullContext.instance)
594
594
  type.fields(context)
595
595
  end
596
596
 
@@ -836,6 +836,26 @@ module GraphQL
836
836
  end
837
837
  end
838
838
 
839
+ def default_logger(new_default_logger = NOT_CONFIGURED)
840
+ if NOT_CONFIGURED.equal?(new_default_logger)
841
+ if defined?(@default_logger)
842
+ @default_logger
843
+ elsif superclass.respond_to?(:default_logger)
844
+ superclass.default_logger
845
+ elsif defined?(Rails)
846
+ Rails.logger
847
+ else
848
+ def_logger = Logger.new($stdout)
849
+ def_logger.info! # It doesn't output debug info by default
850
+ def_logger
851
+ end
852
+ elsif new_default_logger == nil
853
+ @default_logger = Logger.new(IO::NULL)
854
+ else
855
+ @default_logger = new_default_logger
856
+ end
857
+ end
858
+
839
859
  def context_class(new_context_class = nil)
840
860
  if new_context_class
841
861
  @context_class = new_context_class
@@ -921,8 +941,8 @@ module GraphQL
921
941
 
922
942
  def inherited(child_class)
923
943
  if self == GraphQL::Schema
924
- child_class.own_trace_modes[:default] = child_class.build_trace_mode(:default)
925
944
  child_class.directives(default_directives.values)
945
+ child_class.extend(SubclassGetReferencesTo)
926
946
  end
927
947
  # Make sure the child class has these built out, so that
928
948
  # subclasses can be modified by later calls to `trace_with`
@@ -1398,6 +1418,27 @@ module GraphQL
1398
1418
  def own_multiplex_analyzers
1399
1419
  @own_multiplex_analyzers ||= []
1400
1420
  end
1421
+
1422
+ # This is overridden in subclasses to check the inheritance chain
1423
+ def get_references_to(type_name)
1424
+ @own_references_to[type_name]
1425
+ end
1426
+ end
1427
+
1428
+ module SubclassGetReferencesTo
1429
+ def get_references_to(type_name)
1430
+ own_refs = @own_references_to[type_name]
1431
+ inherited_refs = superclass.references_to(type_name)
1432
+ if inherited_refs&.any?
1433
+ if own_refs&.any?
1434
+ own_refs + inherited_refs
1435
+ else
1436
+ inherited_refs
1437
+ end
1438
+ else
1439
+ own_refs
1440
+ end
1441
+ end
1401
1442
  end
1402
1443
 
1403
1444
  # Install these here so that subclasses will also install it.
@@ -37,7 +37,7 @@ module GraphQL
37
37
  end
38
38
 
39
39
  # @return [String] an identifier for this unit of subscription
40
- def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext)
40
+ def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext.instance)
41
41
  subscription = field.resolver || GraphQL::Schema::Subscription
42
42
  normalized_args = stringify_args(field, arguments.to_h, context)
43
43
  subscription.topic_for(arguments: normalized_args, field: field, scope: scope)
@@ -62,7 +62,7 @@ module GraphQL
62
62
  # @return [void]
63
63
  def trigger(event_name, args, object, scope: nil, context: {})
64
64
  # Make something as context-like as possible, even though there isn't a current query:
65
- dummy_query = GraphQL::Query.new(@schema, "", validate: false, context: context)
65
+ dummy_query = GraphQL::Query.new(@schema, "{ __typename }", validate: false, context: context)
66
66
  context = dummy_query.context
67
67
  event_name = event_name.to_s
68
68
 
@@ -83,7 +83,7 @@ module GraphQL
83
83
 
84
84
  # Normalize symbol-keyed args to strings, try camelizing them
85
85
  # Should this accept a real context somehow?
86
- normalized_args = normalize_arguments(normalized_event_name, field, args, GraphQL::Query::NullContext)
86
+ normalized_args = normalize_arguments(normalized_event_name, field, args, GraphQL::Query::NullContext.instance)
87
87
 
88
88
  event = Subscriptions::Event.new(
89
89
  name: normalized_event_name,
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.1.2"
3
+ VERSION = "2.1.4"
4
4
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.2
4
+ version: 2.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-11 00:00:00.000000000 Z
11
+ date: 2023-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: racc
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.4'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: benchmark-ips
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -94,20 +108,6 @@ dependencies:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '1.0'
97
- - !ruby/object:Gem::Dependency
98
- name: racc
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '1.4'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '1.4'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rake
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -626,7 +626,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
626
626
  - !ruby/object:Gem::Version
627
627
  version: '0'
628
628
  requirements: []
629
- rubygems_version: 3.5.0.dev
629
+ rubygems_version: 3.4.10
630
630
  signing_key:
631
631
  specification_version: 4
632
632
  summary: A GraphQL language and runtime for Ruby