graphql 1.9.16 → 1.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75a0bed671ca4979601036351265dfb7a0ce502b4f6937938084f2b9efaa7cd3
4
- data.tar.gz: c43fd98af6355bde57ca2e66b1c98831498f3ac71792176063d934f968c1355f
3
+ metadata.gz: '0486697e594e11affbd5f7d0a661023e2a649abfa654e8faa26b5849fce0b473'
4
+ data.tar.gz: 3943f0dcdeb83dc593ddf675cee9143180d61ea25f454ac1ba6e8f098f4bfaf3
5
5
  SHA512:
6
- metadata.gz: '096f8f6cfa0aa0465f13a54297e10e5564555e5f7937ab1ea2c10d4af49db84b3a4dcf2ca30692aac5b55a6df09e163db62e8e2d712ea3ef7763b20230b642e0'
7
- data.tar.gz: aa41d320f6ecf571c81d9243179fc5ffd7b53da4f12b741670caed08fb9405725a30dd56c1db5b6e6a20985e7e506a7ee2c1548097fb8a35e71e0094fbe307ff
6
+ metadata.gz: 5075b2a02ca5dc3d219736b64b768ef6bbb46f09cebb92aac5ab5d3249b4675b229cb058fe44a328543328497f91658078919e04019857802f42e219585d11fa
7
+ data.tar.gz: 8236278511482220a05461616be330b7c629241f2cb092b67392412d4710c6f5ab2c8e75d342c598bb64bec0fa02b003972d9b1af971562ccaf765daac444492
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GraphQL
4
4
  module Execution
5
- # A tracer that wraps query execution with error handling.
5
+ # A plugin that wraps query execution with error handling.
6
6
  # Supports class-based schemas and the new {Interpreter} runtime only.
7
7
  #
8
8
  # @example Handling ActiveRecord::NotFound
@@ -19,34 +19,35 @@ module GraphQL
19
19
  class Errors
20
20
  def self.use(schema)
21
21
  schema_class = schema.is_a?(Class) ? schema : schema.target.class
22
- schema.tracer(self.new(schema_class))
22
+ schema_class.error_handler = self.new(schema_class)
23
23
  end
24
24
 
25
25
  def initialize(schema)
26
26
  @schema = schema
27
27
  end
28
28
 
29
- def trace(event, data)
30
- case event
31
- when "execute_field", "execute_field_lazy"
32
- with_error_handling(data) { yield }
33
- else
29
+ class NullErrorHandler
30
+ def self.with_error_handling(_ctx)
34
31
  yield
35
32
  end
36
33
  end
37
34
 
38
- private
39
-
40
- def with_error_handling(trace_data)
35
+ # Call the given block with the schema's configured error handlers.
36
+ #
37
+ # If the block returns a lazy value, it's not wrapped with error handling. That area will have to be wrapped itself.
38
+ #
39
+ # @param ctx [GraphQL::Query::Context]
40
+ # @return [Object] Either the result of the given block, or some object to replace the result, in case of error handling.
41
+ def with_error_handling(ctx)
41
42
  yield
42
43
  rescue StandardError => err
43
44
  rescues = @schema.rescues
44
45
  _err_class, handler = rescues.find { |err_class, handler| err.is_a?(err_class) }
45
46
  if handler
46
- obj = trace_data[:object]
47
- args = trace_data[:arguments]
48
- ctx = trace_data[:query].context
49
- field = trace_data[:field]
47
+ runtime_info = ctx.namespace(:interpreter) || {}
48
+ obj = runtime_info[:current_object]
49
+ args = runtime_info[:current_arguments]
50
+ field = runtime_info[:current_field]
50
51
  if obj.is_a?(GraphQL::Schema::Object)
51
52
  obj = obj.object
52
53
  end
@@ -11,7 +11,7 @@ module GraphQL
11
11
  # @return [GraphQL::Query]
12
12
  attr_reader :query
13
13
 
14
- # @return [Class]
14
+ # @return [Class<GraphQL::Schema>]
15
15
  attr_reader :schema
16
16
 
17
17
  # @return [GraphQL::Query::Context]
@@ -43,19 +43,22 @@ module GraphQL
43
43
  # might be stored up in lazies.
44
44
  # @return [void]
45
45
  def run_eager
46
+
46
47
  root_operation = query.selected_operation
47
48
  root_op_type = root_operation.operation_type || "query"
48
49
  legacy_root_type = schema.root_type_for_operation(root_op_type)
49
50
  root_type = legacy_root_type.metadata[:type_class] || raise("Invariant: type must be class-based: #{legacy_root_type}")
51
+ path = []
52
+ @interpreter_context[:current_object] = query.root_value
53
+ @interpreter_context[:current_path] = path
50
54
  object_proxy = root_type.authorized_new(query.root_value, context)
51
55
  object_proxy = schema.sync_lazy(object_proxy)
52
56
  if object_proxy.nil?
53
57
  # Root .authorized? returned false.
54
- write_in_response([], nil)
58
+ write_in_response(path, nil)
55
59
  nil
56
60
  else
57
- path = []
58
- evaluate_selections(path, object_proxy, root_type, root_operation.selections, root_operation_type: root_op_type)
61
+ evaluate_selections(path, context.scoped_context, object_proxy, root_type, root_operation.selections, root_operation_type: root_op_type)
59
62
  nil
60
63
  end
61
64
  end
@@ -115,7 +118,9 @@ module GraphQL
115
118
  end
116
119
  end
117
120
 
118
- def evaluate_selections(path, owner_object, owner_type, selections, root_operation_type: nil)
121
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, selections, root_operation_type: nil)
122
+ @interpreter_context[:current_object] = owner_object
123
+ @interpreter_context[:current_path] = path
119
124
  selections_by_name = {}
120
125
  gather_selections(owner_object, owner_type, selections, selections_by_name)
121
126
  selections_by_name.each do |result_name, field_ast_nodes_or_ast_node|
@@ -158,6 +163,7 @@ module GraphQL
158
163
  @interpreter_context[:current_path] = next_path
159
164
  @interpreter_context[:current_field] = field_defn
160
165
 
166
+ context.scoped_context = scoped_context
161
167
  object = owner_object
162
168
 
163
169
  if is_introspection
@@ -195,6 +201,8 @@ module GraphQL
195
201
  end
196
202
  end
197
203
 
204
+ @interpreter_context[:current_arguments] = kwarg_arguments
205
+
198
206
  # Optimize for the case that field is selected only once
199
207
  if field_ast_nodes.nil? || field_ast_nodes.size == 1
200
208
  next_selections = ast_node.selections
@@ -206,13 +214,15 @@ module GraphQL
206
214
  field_result = resolve_with_directives(object, ast_node) do
207
215
  # Actually call the field resolver and capture the result
208
216
  app_result = begin
209
- query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, query: query, object: object, arguments: kwarg_arguments}) do
210
- field_defn.resolve(object, kwarg_arguments, context)
217
+ query.with_error_handling do
218
+ query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, query: query, object: object, arguments: kwarg_arguments}) do
219
+ field_defn.resolve(object, kwarg_arguments, context)
220
+ end
211
221
  end
212
222
  rescue GraphQL::ExecutionError => err
213
223
  err
214
224
  end
215
- after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, owner_object: object, arguments: kwarg_arguments) do |inner_result|
225
+ after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
216
226
  continue_value = continue_value(next_path, inner_result, field_defn, return_type.non_null?, ast_node)
217
227
  if HALT != continue_value
218
228
  continue_field(next_path, continue_value, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
@@ -286,7 +296,7 @@ module GraphQL
286
296
  r
287
297
  when "UNION", "INTERFACE"
288
298
  resolved_type_or_lazy = query.resolve_type(type, value)
289
- after_lazy(resolved_type_or_lazy, owner: type, path: path, field: field, owner_object: owner_object, arguments: arguments) do |resolved_type|
299
+ after_lazy(resolved_type_or_lazy, owner: type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |resolved_type|
290
300
  possible_types = query.possible_types(type)
291
301
 
292
302
  if !possible_types.include?(resolved_type)
@@ -306,12 +316,12 @@ module GraphQL
306
316
  rescue GraphQL::ExecutionError => err
307
317
  err
308
318
  end
309
- after_lazy(object_proxy, owner: type, path: path, field: field, owner_object: owner_object, arguments: arguments) do |inner_object|
319
+ after_lazy(object_proxy, owner: type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_object|
310
320
  continue_value = continue_value(path, inner_object, field, is_non_null, ast_node)
311
321
  if HALT != continue_value
312
322
  response_hash = {}
313
323
  write_in_response(path, response_hash)
314
- evaluate_selections(path, continue_value, type, next_selections)
324
+ evaluate_selections(path, context.scoped_context, continue_value, type, next_selections)
315
325
  response_hash
316
326
  end
317
327
  end
@@ -320,6 +330,7 @@ module GraphQL
320
330
  write_in_response(path, response_list)
321
331
  inner_type = type.of_type
322
332
  idx = 0
333
+ scoped_context = context.scoped_context
323
334
  value.each do |inner_value|
324
335
  next_path = path.dup
325
336
  next_path << idx
@@ -327,7 +338,7 @@ module GraphQL
327
338
  idx += 1
328
339
  set_type_at_path(next_path, inner_type)
329
340
  # This will update `response_list` with the lazy
330
- after_lazy(inner_value, owner: inner_type, path: next_path, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
341
+ after_lazy(inner_value, owner: inner_type, path: next_path, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
331
342
  # reset `is_non_null` here and below, because the inner type will have its own nullability constraint
332
343
  continue_value = continue_value(next_path, inner_inner_value, field, false, ast_node)
333
344
  if HALT != continue_value
@@ -393,23 +404,30 @@ module GraphQL
393
404
  # @param field [GraphQL::Schema::Field]
394
405
  # @param eager [Boolean] Set to `true` for mutation root fields only
395
406
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
396
- def after_lazy(lazy_obj, owner:, field:, path:, owner_object:, arguments:, eager: false)
407
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, eager: false)
408
+ @interpreter_context[:current_object] = owner_object
409
+ @interpreter_context[:current_arguments] = arguments
397
410
  @interpreter_context[:current_path] = path
398
411
  @interpreter_context[:current_field] = field
399
412
  if schema.lazy?(lazy_obj)
400
413
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
401
414
  @interpreter_context[:current_path] = path
402
415
  @interpreter_context[:current_field] = field
416
+ @interpreter_context[:current_object] = owner_object
417
+ @interpreter_context[:current_arguments] = arguments
418
+ context.scoped_context = scoped_context
403
419
  # Wrap the execution of _this_ method with tracing,
404
420
  # but don't wrap the continuation below
405
421
  inner_obj = begin
406
- query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments}) do
407
- schema.sync_lazy(lazy_obj)
422
+ query.with_error_handling do
423
+ query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments}) do
424
+ schema.sync_lazy(lazy_obj)
425
+ end
408
426
  end
409
427
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
410
428
  yield(err)
411
429
  end
412
- after_lazy(inner_obj, owner: owner, field: field, path: path, owner_object: owner_object, arguments: arguments, eager: eager) do |really_inner_obj|
430
+ after_lazy(inner_obj, owner: owner, field: field, path: path, scoped_context: context.scoped_context, owner_object: owner_object, arguments: arguments, eager: eager) do |really_inner_obj|
413
431
  yield(really_inner_obj)
414
432
  end
415
433
  end
@@ -500,7 +518,11 @@ module GraphQL
500
518
  args = arguments(nil, arg_type, ast_value)
501
519
  # We're not tracking defaults_used, but for our purposes
502
520
  # we compare the value to the default value.
503
- return true, arg_type.new(ruby_kwargs: args, context: context, defaults_used: nil)
521
+
522
+ input_obj = query.with_error_handling do
523
+ arg_type.new(ruby_kwargs: args, context: context, defaults_used: nil)
524
+ end
525
+ return true, input_obj
504
526
  else
505
527
  flat_value = flatten_ast_value(ast_value)
506
528
  return true, arg_type.coerce_input(flat_value, context)
@@ -15,8 +15,9 @@ module GraphQL
15
15
  end
16
16
 
17
17
  def __type(name:)
18
- type = context.warden.get_type(name)
18
+ return unless context.warden.reachable_type?(name)
19
19
 
20
+ type = context.warden.get_type(name)
20
21
  if type && context.interpreter?
21
22
  type = type.metadata[:type_class] || raise("Invariant: interpreter requires class-based type for #{name}")
22
23
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Introspection
4
5
  class SchemaType < Introspection::BaseObject
@@ -14,7 +15,7 @@ module GraphQL
14
15
  field :directives, [GraphQL::Schema::LateBoundType.new("__Directive")], "A list of all directives supported by this server.", null: false
15
16
 
16
17
  def types
17
- types = @context.warden.types
18
+ types = @context.warden.reachable_types.sort_by(&:graphql_name)
18
19
  if context.interpreter?
19
20
  types.map { |t| t.metadata[:type_class] || raise("Invariant: can't introspect non-class-based type: #{t}") }
20
21
  else
data/lib/graphql/query.rb CHANGED
@@ -300,6 +300,13 @@ module GraphQL
300
300
  with_prepared_ast { @subscription }
301
301
  end
302
302
 
303
+ # @api private
304
+ def with_error_handling
305
+ schema.error_handler.with_error_handling(context) do
306
+ yield
307
+ end
308
+ end
309
+
303
310
  private
304
311
 
305
312
  def find_operation(operations, operation_name)
@@ -155,6 +155,7 @@ module GraphQL
155
155
  @path = []
156
156
  @value = nil
157
157
  @context = self # for SharedMethods
158
+ @scoped_context = {}
158
159
  end
159
160
 
160
161
  # @api private
@@ -163,15 +164,30 @@ module GraphQL
163
164
  # @api private
164
165
  attr_writer :value
165
166
 
166
- def_delegators :@provided_values, :[], :[]=, :to_h, :to_hash, :key?, :fetch, :dig
167
- def_delegators :@query, :trace, :interpreter?
167
+ # @api private
168
+ attr_accessor :scoped_context
168
169
 
169
- # @!method [](key)
170
- # Lookup `key` from the hash passed to {Schema#execute} as `context:`
170
+ def_delegators :@provided_values, :[]=
171
+ def_delegators :to_h, :fetch, :dig
172
+ def_delegators :@query, :trace, :interpreter?
171
173
 
172
174
  # @!method []=(key, value)
173
175
  # Reassign `key` to the hash passed to {Schema#execute} as `context:`
174
176
 
177
+ # Lookup `key` from the hash passed to {Schema#execute} as `context:`
178
+ def [](key)
179
+ return @scoped_context[key] if @scoped_context.key?(key)
180
+ @provided_values[key]
181
+ end
182
+
183
+ def to_h
184
+ @provided_values.merge(@scoped_context)
185
+ end
186
+ alias :to_hash :to_h
187
+
188
+ def key?(key)
189
+ @scoped_context.key?(key) || @provided_values.key?(key)
190
+ end
175
191
 
176
192
  # @return [GraphQL::Schema::Warden]
177
193
  def warden
@@ -195,6 +211,15 @@ module GraphQL
195
211
  @value = nil
196
212
  end
197
213
 
214
+ def scoped_merge!(hash)
215
+ @scoped_context = @scoped_context.merge(hash)
216
+ end
217
+
218
+ def scoped_set!(key, value)
219
+ scoped_merge!(key => value)
220
+ nil
221
+ end
222
+
198
223
  class FieldResolutionContext
199
224
  include SharedMethods
200
225
  include Tracing::Traceable
@@ -21,9 +21,13 @@ module GraphQL
21
21
  )
22
22
  end
23
23
 
24
+ def [](key); end
25
+
24
26
  class << self
25
27
  extend Forwardable
26
28
 
29
+ def [](key); end
30
+
27
31
  def instance
28
32
  @instance = self.new
29
33
  end
@@ -35,7 +35,9 @@ module GraphQL
35
35
  if validation_result.valid?
36
36
  if value_was_provided
37
37
  # Add the variable if a value was provided
38
- memo[variable_name] = variable_type.coerce_input(provided_value, ctx)
38
+ memo[variable_name] = schema.error_handler.with_error_handling(context) do
39
+ variable_type.coerce_input(provided_value, ctx)
40
+ end
39
41
  elsif default_value != nil
40
42
  # Add the variable if it wasn't provided but it has a default value (including `null`)
41
43
  memo[variable_name] = GraphQL::Query::LiteralInput.coerce(variable_type, default_value, self)
@@ -40,6 +40,7 @@ require "graphql/schema/directive/include"
40
40
  require "graphql/schema/directive/skip"
41
41
  require "graphql/schema/directive/feature"
42
42
  require "graphql/schema/directive/transform"
43
+ require "graphql/schema/type_membership"
43
44
 
44
45
  require "graphql/schema/resolver"
45
46
  require "graphql/schema/mutation"
@@ -448,10 +449,11 @@ module GraphQL
448
449
 
449
450
  # @see [GraphQL::Schema::Warden] Restricted access to members of a schema
450
451
  # @param type_defn [GraphQL::InterfaceType, GraphQL::UnionType] the type whose members you want to retrieve
452
+ # @param context [GraphQL::Query::Context] The context for the current query
451
453
  # @return [Array<GraphQL::ObjectType>] types which belong to `type_defn` in this schema
452
- def possible_types(type_defn)
454
+ def possible_types(type_defn, context = GraphQL::Query::NullContext)
453
455
  @possible_types ||= GraphQL::Schema::PossibleTypes.new(self)
454
- @possible_types.possible_types(type_defn)
456
+ @possible_types.possible_types(type_defn, context)
455
457
  end
456
458
 
457
459
  # @see [GraphQL::Schema::Warden] Resticted access to root types
@@ -597,6 +599,7 @@ module GraphQL
597
599
  alias :_schema_class :class
598
600
  def_delegators :_schema_class, :visible?, :accessible?, :authorized?, :unauthorized_object, :unauthorized_field, :inaccessible_fields
599
601
  def_delegators :_schema_class, :directive
602
+ def_delegators :_schema_class, :error_handler
600
603
 
601
604
  # A function to call when {#execute} receives an invalid query string
602
605
  #
@@ -1027,6 +1030,13 @@ module GraphQL
1027
1030
  DefaultTypeError.call(type_err, ctx)
1028
1031
  end
1029
1032
 
1033
+ attr_writer :error_handler
1034
+
1035
+ # @return [GraphQL::Execution::Errors, Class<GraphQL::Execution::Errors::NullErrorHandler>]
1036
+ def error_handler
1037
+ @error_handler ||= GraphQL::Execution::Errors::NullErrorHandler
1038
+ end
1039
+
1030
1040
  def lazy_resolve(lazy_class, value_method)
1031
1041
  lazy_classes[lazy_class] = value_method
1032
1042
  end
@@ -121,6 +121,10 @@ module GraphQL
121
121
  # Used by the runtime.
122
122
  # @api private
123
123
  def prepare_value(obj, value)
124
+ if value.is_a?(GraphQL::Schema::InputObject)
125
+ value = value.prepare
126
+ end
127
+
124
128
  if @prepare.nil?
125
129
  value
126
130
  elsif @prepare.is_a?(String) || @prepare.is_a?(Symbol)
@@ -10,8 +10,9 @@ module Base64Bp
10
10
  module_function
11
11
 
12
12
  def urlsafe_encode64(bin, padding:)
13
- str = strict_encode64(bin).tr("+/", "-_")
14
- str = str.delete("=") unless padding
13
+ str = strict_encode64(bin)
14
+ str.tr!("+/", "-_")
15
+ str.delete!("=") unless padding
15
16
  str
16
17
  end
17
18
 
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'irb/ruby-token'
3
2
 
4
3
  module GraphQL
5
4
  class Schema
@@ -43,9 +42,7 @@ module GraphQL
43
42
  # A list of Ruby keywords.
44
43
  #
45
44
  # @api private
46
- RUBY_KEYWORDS = RubyToken::TokenDefinitions.select { |definition| definition[1] == RubyToken::TkId }
47
- .map { |definition| definition[2] }
48
- .compact
45
+ RUBY_KEYWORDS = [:class, :module, :def, :undef, :begin, :rescue, :ensure, :end, :if, :unless, :then, :elsif, :else, :case, :when, :while, :until, :for, :break, :next, :redo, :retry, :in, :do, :return, :yield, :super, :self, :nil, :true, :false, :and, :or, :not, :alias, :defined?, :BEGIN, :END, :__LINE__, :__FILE__]
49
46
 
50
47
  # A list of GraphQL-Ruby keywords.
51
48
  #
@@ -35,10 +35,12 @@ module GraphQL
35
35
  # @return [GraphQL::Schema::Object, GraphQL::Execution::Lazy]
36
36
  # @raise [GraphQL::UnauthorizedError] if the user-provided hook returns `false`
37
37
  def authorized_new(object, context)
38
- auth_val = begin
39
- authorized?(object, context)
40
- rescue GraphQL::UnauthorizedError => err
41
- context.schema.unauthorized_object(err)
38
+ auth_val = context.query.with_error_handling do
39
+ begin
40
+ authorized?(object, context)
41
+ rescue GraphQL::UnauthorizedError => err
42
+ context.schema.unauthorized_object(err)
43
+ end
42
44
  end
43
45
 
44
46
  context.schema.after_lazy(auth_val) do |is_authorized|
@@ -20,12 +20,12 @@ module GraphQL
20
20
  end
21
21
  end
22
22
 
23
- def possible_types(type_defn)
23
+ def possible_types(type_defn, ctx)
24
24
  case type_defn
25
25
  when Module
26
- possible_types(type_defn.graphql_definition)
26
+ possible_types(type_defn.graphql_definition, ctx)
27
27
  when GraphQL::UnionType
28
- type_defn.possible_types
28
+ type_defn.possible_types(ctx)
29
29
  when GraphQL::InterfaceType
30
30
  @interface_implementers[type_defn]
31
31
  when GraphQL::BaseType
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ # This class joins an object type to an abstract type (interface or union) of which
6
+ # it is a member.
7
+ #
8
+ # TODO: Not yet implemented for interfaces.
9
+ class TypeMembership
10
+ # @return [Class<GraphQL::Schema::Object>]
11
+ attr_reader :object_type
12
+
13
+ # @return [Class<GraphQL::Schema::Union>, Module<GraphQL::Schema::Interface>]
14
+ attr_reader :abstract_type
15
+
16
+ # Called when an object is hooked up to an abstract type, such as {Schema::Union.possible_types}
17
+ # or {Schema::Object.implements} (for interfaces).
18
+ #
19
+ # @param abstract_type [Class<GraphQL::Schema::Union>, Module<GraphQL::Schema::Interface>]
20
+ # @param object_type [Class<GraphQL::Schema::Object>]
21
+ # @param options [Hash] Any options passed to `.possible_types` or `.implements`
22
+ def initialize(abstract_type, object_type, options)
23
+ @abstract_type = abstract_type
24
+ @object_type = object_type
25
+ @options = options
26
+ end
27
+
28
+ # @return [Boolean] if false, {#object_type} will be treated as _not_ a member of {#abstract_type}
29
+ def visible?(_ctx)
30
+ true
31
+ end
32
+ end
33
+ end
34
+ end
@@ -5,13 +5,19 @@ module GraphQL
5
5
  extend GraphQL::Schema::Member::AcceptsDefinition
6
6
 
7
7
  class << self
8
- def possible_types(*types)
8
+ def possible_types(*types, context: GraphQL::Query::NullContext, **options)
9
9
  if types.any?
10
- @possible_types = types
10
+ types.each do |t|
11
+ type_memberships << type_membership_class.new(self, t, options)
12
+ end
11
13
  else
12
- all_possible_types = @possible_types || []
13
- all_possible_types += super if defined?(super)
14
- all_possible_types.uniq
14
+ visible_types = []
15
+ type_memberships.each do |type_membership|
16
+ if type_membership.visible?(context)
17
+ visible_types << type_membership.object_type
18
+ end
19
+ end
20
+ visible_types
15
21
  end
16
22
  end
17
23
 
@@ -19,7 +25,7 @@ module GraphQL
19
25
  type_defn = GraphQL::UnionType.new
20
26
  type_defn.name = graphql_name
21
27
  type_defn.description = description
22
- type_defn.possible_types = possible_types
28
+ type_defn.type_memberships = type_memberships
23
29
  if respond_to?(:resolve_type)
24
30
  type_defn.resolve_type = method(:resolve_type)
25
31
  end
@@ -27,9 +33,23 @@ module GraphQL
27
33
  type_defn
28
34
  end
29
35
 
36
+ def type_membership_class(membership_class = nil)
37
+ if membership_class
38
+ @type_membership_class = membership_class
39
+ else
40
+ @type_membership_class || find_inherited_value(:type_membership_class, GraphQL::Schema::TypeMembership)
41
+ end
42
+ end
43
+
30
44
  def kind
31
45
  GraphQL::TypeKinds::UNION
32
46
  end
47
+
48
+ private
49
+
50
+ def type_memberships
51
+ @type_memberships ||= []
52
+ end
33
53
  end
34
54
  end
35
55
  end
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
2
5
  module GraphQL
3
6
  class Schema
4
7
  # Restrict access to a {GraphQL::Schema} with a user-defined filter.
@@ -40,7 +43,8 @@ module GraphQL
40
43
  # @param deep_check [Boolean]
41
44
  def initialize(filter, context:, schema:)
42
45
  @schema = schema
43
- @visibility_cache = read_through { |m| filter.call(m, context) }
46
+ @context = context
47
+ @visibility_cache = read_through { |m| filter.call(m, @context) }
44
48
  end
45
49
 
46
50
  # @return [Array<GraphQL::BaseType>] Visible types in the schema
@@ -62,6 +66,17 @@ module GraphQL
62
66
  @visible_types[type_name]
63
67
  end
64
68
 
69
+ # @return [Array<GraphQL::BaseType>] Visible and reachable types in the schema
70
+ def reachable_types
71
+ @reachable_types ||= reachable_type_set.to_a
72
+ end
73
+
74
+ # @return Boolean True if the type is visible and reachable in the schema
75
+ def reachable_type?(type_name)
76
+ type = get_type(type_name)
77
+ type && reachable_type_set.include?(type)
78
+ end
79
+
65
80
  # @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists
66
81
  def get_field(parent_type, field_name)
67
82
 
@@ -81,7 +96,7 @@ module GraphQL
81
96
 
82
97
  # @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn`
83
98
  def possible_types(type_defn)
84
- @visible_possible_types ||= read_through { |type_defn| @schema.possible_types(type_defn).select { |t| visible_type?(t) } }
99
+ @visible_possible_types ||= read_through { |type_defn| @schema.possible_types(type_defn, @context).select { |t| visible_type?(t) } }
85
100
  @visible_possible_types[type_defn]
86
101
  end
87
102
 
@@ -170,7 +185,7 @@ module GraphQL
170
185
  end
171
186
 
172
187
  def visible_possible_types?(type_defn)
173
- @schema.possible_types(type_defn).any? { |t| visible_type?(t) }
188
+ @schema.possible_types(type_defn, @context).any? { |t| visible_type?(t) }
174
189
  end
175
190
 
176
191
  def visible?(member)
@@ -180,6 +195,65 @@ module GraphQL
180
195
  def read_through
181
196
  Hash.new { |h, k| h[k] = yield(k) }
182
197
  end
198
+
199
+ def reachable_type_set
200
+ return @reachable_type_set if defined?(@reachable_type_set)
201
+
202
+ @reachable_type_set = Set.new
203
+
204
+ unvisited_types = []
205
+ ['query', 'mutation', 'subscription'].each do |op_name|
206
+ root_type = root_type_for_operation(op_name)
207
+ unvisited_types << root_type if root_type
208
+ end
209
+ unvisited_types.concat(@schema.introspection_system.object_types)
210
+ @schema.orphan_types.each do |orphan_type|
211
+ unvisited_types << orphan_type.graphql_definition if get_type(orphan_type.graphql_name)
212
+ end
213
+
214
+ until unvisited_types.empty?
215
+ type = unvisited_types.pop
216
+ if @reachable_type_set.add?(type)
217
+ if type.kind.input_object?
218
+ # recurse into visible arguments
219
+ arguments(type).each do |argument|
220
+ argument_type = argument.type.unwrap
221
+ unvisited_types << argument_type
222
+ end
223
+ elsif type.kind.union?
224
+ # recurse into visible possible types
225
+ possible_types(type).each do |possible_type|
226
+ unvisited_types << possible_type
227
+ end
228
+ elsif type.kind.fields?
229
+ if type.kind.interface?
230
+ # recurse into visible possible types
231
+ possible_types(type).each do |possible_type|
232
+ unvisited_types << possible_type
233
+ end
234
+ elsif type.kind.object?
235
+ # recurse into visible implemented interfaces
236
+ interfaces(type).each do |interface|
237
+ unvisited_types << interface
238
+ end
239
+ end
240
+
241
+ # recurse into visible fields
242
+ fields(type).each do |field|
243
+ field_type = field.type.unwrap
244
+ unvisited_types << field_type
245
+ # recurse into visible arguments
246
+ arguments(field).each do |argument|
247
+ argument_type = argument.type.unwrap
248
+ unvisited_types << argument_type
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+
255
+ @reachable_type_set
256
+ end
183
257
  end
184
258
  end
185
259
  end
@@ -24,22 +24,27 @@ module GraphQL
24
24
  # }
25
25
  #
26
26
  class UnionType < GraphQL::BaseType
27
- accepts_definitions :possible_types, :resolve_type
28
- ensure_defined :possible_types, :resolve_type, :resolve_type_proc
27
+ accepts_definitions :resolve_type, :type_membership_class,
28
+ possible_types: ->(target, possible_types, options = {}) { target.add_possible_types(possible_types, options) }
29
+ ensure_defined :possible_types, :resolve_type, :resolve_type_proc, :type_membership_class
29
30
 
30
31
  attr_accessor :resolve_type_proc
32
+ attr_reader :type_memberships
33
+ attr_accessor :type_membership_class
31
34
 
32
35
  def initialize
33
36
  super
34
- @dirty_possible_types = []
35
- @clean_possible_types = nil
37
+ @type_membership_class = GraphQL::Schema::TypeMembership
38
+ @type_memberships = []
39
+ @cached_possible_types = nil
36
40
  @resolve_type_proc = nil
37
41
  end
38
42
 
39
43
  def initialize_copy(other)
40
44
  super
41
- @clean_possible_types = nil
42
- @dirty_possible_types = other.dirty_possible_types.dup
45
+ @type_membership_class = other.type_membership_class
46
+ @type_memberships = other.type_memberships.dup
47
+ @cached_possible_types = nil
43
48
  end
44
49
 
45
50
  def kind
@@ -47,33 +52,44 @@ module GraphQL
47
52
  end
48
53
 
49
54
  # @return [Boolean] True if `child_type_defn` is a member of this {UnionType}
50
- def include?(child_type_defn)
51
- possible_types.include?(child_type_defn)
52
- end
53
-
54
- def possible_types=(new_possible_types)
55
- @clean_possible_types = nil
56
- @dirty_possible_types = new_possible_types
55
+ def include?(child_type_defn, ctx = GraphQL::Query::NullContext)
56
+ possible_types(ctx).include?(child_type_defn)
57
57
  end
58
58
 
59
59
  # @return [Array<GraphQL::ObjectType>] Types which may be found in this union
60
- def possible_types
61
- @clean_possible_types ||= begin
62
- if @dirty_possible_types.respond_to?(:map)
63
- @dirty_possible_types.map { |type| GraphQL::BaseType.resolve_related_type(type) }
64
- else
65
- @dirty_possible_types
66
- end
60
+ def possible_types(ctx = GraphQL::Query::NullContext)
61
+ if ctx == GraphQL::Query::NullContext
62
+ # Only cache the default case; if we cached for every `ctx`, it would be a memory leak
63
+ # (The warden should cache calls to this method, so it's called only once per query,
64
+ # unless user code calls it directly.)
65
+ @cached_possible_types ||= possible_types_for_context(ctx)
66
+ else
67
+ possible_types_for_context(ctx)
67
68
  end
68
69
  end
69
70
 
71
+ def possible_types=(types)
72
+ # This is a re-assignment, so clear the previous values
73
+ @type_memberships = []
74
+ @cached_possible_types = nil
75
+ add_possible_types(types, {})
76
+ end
77
+
78
+ def add_possible_types(types, options)
79
+ @type_memberships ||= []
80
+ Array(types).each { |t|
81
+ @type_memberships << self.type_membership_class.new(self, t, options)
82
+ }
83
+ nil
84
+ end
85
+
70
86
  # Get a possible type of this {UnionType} by type name
71
87
  # @param type_name [String]
72
88
  # @param ctx [GraphQL::Query::Context] The context for the current query
73
89
  # @return [GraphQL::ObjectType, nil] The type named `type_name` if it exists and is a member of this {UnionType}, (else `nil`)
74
90
  def get_possible_type(type_name, ctx)
75
91
  type = ctx.query.get_type(type_name)
76
- type if type && ctx.query.schema.possible_types(self).include?(type)
92
+ type if type && ctx.query.schema.possible_types(self, ctx).include?(type)
77
93
  end
78
94
 
79
95
  # Check if a type is a possible type of this {UnionType}
@@ -93,8 +109,20 @@ module GraphQL
93
109
  @resolve_type_proc = new_resolve_type_proc
94
110
  end
95
111
 
96
- protected
112
+ def type_memberships=(type_memberships)
113
+ @type_memberships = type_memberships
114
+ end
115
+
116
+ private
97
117
 
98
- attr_reader :dirty_possible_types
118
+ def possible_types_for_context(ctx)
119
+ visible_types = []
120
+ @type_memberships.each do |type_membership|
121
+ if type_membership.visible?(ctx)
122
+ visible_types << BaseType.resolve_related_type(type_membership.object_type)
123
+ end
124
+ end
125
+ visible_types
126
+ end
99
127
  end
100
128
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.9.16"
3
+ VERSION = "1.9.17"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.16
4
+ version: 1.9.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-02 00:00:00.000000000 Z
11
+ date: 2019-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -588,6 +588,7 @@ files:
588
588
  - lib/graphql/schema/timeout_middleware.rb
589
589
  - lib/graphql/schema/traversal.rb
590
590
  - lib/graphql/schema/type_expression.rb
591
+ - lib/graphql/schema/type_membership.rb
591
592
  - lib/graphql/schema/union.rb
592
593
  - lib/graphql/schema/unique_within_type.rb
593
594
  - lib/graphql/schema/validation.rb