graphql 1.9.16 → 1.9.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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