graphql 2.5.14 → 2.5.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  4. data/lib/graphql/dashboard/application_controller.rb +41 -0
  5. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  6. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  7. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  8. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  9. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  10. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  11. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  12. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  13. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  15. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  16. data/lib/graphql/dashboard.rb +11 -73
  17. data/lib/graphql/dataloader/null_dataloader.rb +7 -3
  18. data/lib/graphql/date_encoding_error.rb +1 -1
  19. data/lib/graphql/execution/interpreter.rb +0 -1
  20. data/lib/graphql/execution/multiplex.rb +1 -1
  21. data/lib/graphql/execution/next/field_resolve_step.rb +711 -0
  22. data/lib/graphql/execution/next/load_argument_step.rb +60 -0
  23. data/lib/graphql/execution/next/prepare_object_step.rb +129 -0
  24. data/lib/graphql/execution/next/runner.rb +389 -0
  25. data/lib/graphql/execution/next/selections_step.rb +37 -0
  26. data/lib/graphql/execution/next.rb +70 -0
  27. data/lib/graphql/execution.rb +1 -0
  28. data/lib/graphql/execution_error.rb +13 -10
  29. data/lib/graphql/introspection/directive_type.rb +7 -3
  30. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  31. data/lib/graphql/introspection/entry_points.rb +11 -3
  32. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  33. data/lib/graphql/introspection/field_type.rb +13 -5
  34. data/lib/graphql/introspection/input_value_type.rb +21 -13
  35. data/lib/graphql/introspection/type_type.rb +64 -28
  36. data/lib/graphql/invalid_null_error.rb +11 -5
  37. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  38. data/lib/graphql/language.rb +21 -12
  39. data/lib/graphql/pagination/connection.rb +2 -0
  40. data/lib/graphql/pagination/connections.rb +32 -0
  41. data/lib/graphql/query/context.rb +3 -2
  42. data/lib/graphql/query/null_context.rb +9 -3
  43. data/lib/graphql/schema/argument.rb +12 -0
  44. data/lib/graphql/schema/build_from_definition.rb +7 -0
  45. data/lib/graphql/schema/directive.rb +8 -1
  46. data/lib/graphql/schema/field/connection_extension.rb +15 -35
  47. data/lib/graphql/schema/field/scope_extension.rb +22 -13
  48. data/lib/graphql/schema/field.rb +80 -48
  49. data/lib/graphql/schema/field_extension.rb +33 -0
  50. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -1
  51. data/lib/graphql/schema/member/has_arguments.rb +37 -14
  52. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  53. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  54. data/lib/graphql/schema/member/has_fields.rb +81 -4
  55. data/lib/graphql/schema/member.rb +5 -0
  56. data/lib/graphql/schema/object.rb +1 -0
  57. data/lib/graphql/schema/resolver.rb +45 -1
  58. data/lib/graphql/schema/validator/required_validator.rb +33 -2
  59. data/lib/graphql/schema/visibility.rb +3 -3
  60. data/lib/graphql/schema.rb +53 -10
  61. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +2 -2
  62. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  63. data/lib/graphql/subscriptions.rb +1 -1
  64. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  65. data/lib/graphql/testing.rb +1 -0
  66. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  67. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  68. data/lib/graphql/tracing/perfetto_trace.rb +208 -78
  69. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  70. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  71. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  72. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  73. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  74. data/lib/graphql/unauthorized_error.rb +5 -1
  75. data/lib/graphql/version.rb +1 -1
  76. data/lib/graphql.rb +8 -2
  77. metadata +17 -3
@@ -5,11 +5,88 @@ module GraphQL
5
5
  class Member
6
6
  # Shared code for Objects, Interfaces, Mutations, Subscriptions
7
7
  module HasFields
8
+ include EmptyObjects
8
9
  # Add a field to this object or interface with the given definition
9
- # @see {GraphQL::Schema::Field#initialize} for method signature
10
+ # @param name_positional [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); `name:` keyword is also accepted
11
+ # @param type_positional [Class, GraphQL::BaseType, Array] The return type of this field; `type:` keyword is also accepted
12
+ # @param desc_positional [String] Field description; `description:` keyword is also accepted
13
+ # @option kwargs [Symbol] :name The underscore-cased version of this field name (will be camelized for the GraphQL API); positional argument also accepted
14
+ # @option kwargs [Class, GraphQL::BaseType, Array] :type The return type of this field; positional argument is also accepted
15
+ # @option kwargs [Boolean] :null (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
16
+ # @option kwargs [String] :description Field description; positional argument also accepted
17
+ # @option kwargs [String] :comment Field comment
18
+ # @option kwargs [String] :deprecation_reason If present, the field is marked "deprecated" with this message
19
+ # @option kwargs [Symbol] :method The method to call on the underlying object to resolve this field (defaults to `name`)
20
+ # @option kwargs [String, Symbol] :hash_key The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
21
+ # @option kwargs [Array<String, Symbol>] :dig The nested hash keys to lookup on the underlying hash to resolve this field using dig
22
+ # @option kwargs [Symbol, true] :resolver_method The method on the type to call to resolve this field (defaults to `name`)
23
+ # @option kwargs [Symbol, true] :resolve_static Used by {Schema.execute_next} to produce a single value, shared by all objects which resolve this field. Called on the owner type class with `context, **arguments`
24
+ # @option kwargs [Symbol, true] :resolve_batch Used by {Schema.execute_next} map `objects` to a same-sized Array of results. Called on the owner type class with `objects, context, **arguments`.
25
+ # @option kwargs [Symbol, true] :resolve_each Used by {Schema.execute_next} to get a value value for each item. Called on the owner type class with `object, context, **arguments`.
26
+ # @option kwargs [Symbol, true] :resolve_legacy_instance_method Used by {Schema.execute_next} to get a value value for each item. Calls an instance method on the object type class.
27
+ # @option kwargs [Boolean] :connection `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
28
+ # @option kwargs [Class] :connection_extension The extension to add, to implement connections. If `nil`, no extension is added.
29
+ # @option kwargs [Integer, nil] :max_page_size For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
30
+ # @option kwargs [Integer, nil] :default_page_size For connections, the default number of items to return from this field, or `nil` to return unlimited results.
31
+ # @option kwargs [Boolean] :introspection If true, this field will be marked as `#introspection?` and the name may begin with `__`
32
+ # @option kwargs [{String=>GraphQL::Schema::Argument, Hash}] :arguments Arguments for this field (may be added in the block, also)
33
+ # @option kwargs [Boolean] :camelize If true, the field name will be camelized when building the schema
34
+ # @option kwargs [Numeric] :complexity When provided, set the complexity for this field
35
+ # @option kwargs [Boolean] :scope If true, the return type's `.scope_items` method will be called on the return value
36
+ # @option kwargs [Symbol, String] :subscription_scope A key in `context` which will be used to scope subscription payloads
37
+ # @option kwargs [Array<Class, Hash<Class => Object>>] :extensions Named extensions to apply to this field (see also {#extension})
38
+ # @option kwargs [Hash{Class => Hash}] :directives Directives to apply to this field
39
+ # @option kwargs [Boolean] :trace If true, a {GraphQL::Tracing} tracer will measure this scalar field
40
+ # @option kwargs [Boolean] :broadcastable Whether or not this field can be distributed in subscription broadcasts
41
+ # @option kwargs [Language::Nodes::FieldDefinition, nil] :ast_node If this schema was parsed from definition, this AST node defined the field
42
+ # @option kwargs [Boolean] :method_conflict_warning If false, skip the warning if this field's method conflicts with a built-in method
43
+ # @option kwargs [Array<Hash>] :validates Configurations for validating this field
44
+ # @option kwargs [Object] :fallback_value A fallback value if the method is not defined
45
+ # @option kwargs [Class<GraphQL::Schema::Mutation>] :mutation
46
+ # @option kwargs [Class<GraphQL::Schema::Resolver>] :resolver
47
+ # @option kwargs [Class<GraphQL::Schema::Subscription>] :subscription
48
+ # @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby)
49
+ # @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby)
50
+ # @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby)
51
+ # @option kwargs [Class, Hash] :dataload Shorthand for dataloader lookups
52
+ # @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field
53
+ # @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled.
54
+ # @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
55
+ # @yieldparam field [GraphQL::Schema::Field] The newly-created field instance
56
+ # @yieldreturn [void]
10
57
  # @return [GraphQL::Schema::Field]
11
- def field(*args, **kwargs, &block)
12
- field_defn = field_class.from_options(*args, owner: self, **kwargs, &block)
58
+ def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &definition_block)
59
+ resolver = kwargs.delete(:resolver)
60
+ mutation = kwargs.delete(:mutation)
61
+ subscription = kwargs.delete(:subscription)
62
+ if (resolver_class = resolver || mutation || subscription)
63
+ # Add a reference to that parent class
64
+ kwargs[:resolver_class] = resolver_class
65
+ end
66
+
67
+ kwargs[:name] ||= name_positional
68
+ if !type_positional.nil?
69
+ if desc_positional
70
+ if kwargs[:description]
71
+ raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{kwargs[:description].inspect})"
72
+ end
73
+
74
+ kwargs[:description] = desc_positional
75
+ kwargs[:type] = type_positional
76
+ elsif (resolver || mutation) && type_positional.is_a?(String)
77
+ # The return type should be copied from the resolver, and the second positional argument is the description
78
+ kwargs[:description] = type_positional
79
+ else
80
+ kwargs[:type] = type_positional
81
+ end
82
+
83
+ if type_positional.is_a?(Class) && type_positional < GraphQL::Schema::Mutation
84
+ raise ArgumentError, "Use `field #{name_positional.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
85
+ end
86
+ end
87
+
88
+ kwargs[:owner] = self
89
+ field_defn = field_class.new(**kwargs, &definition_block)
13
90
  add_field(field_defn)
14
91
  field_defn
15
92
  end
@@ -232,7 +309,7 @@ module GraphQL
232
309
  end
233
310
  end
234
311
 
235
- # @param [GraphQL::Schema::Field]
312
+ # @param field_defn [GraphQL::Schema::Field]
236
313
  # @return [String] A warning to give when this field definition might conflict with a built-in method
237
314
  def conflict_field_name_warning(field_defn)
238
315
  "#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
@@ -2,6 +2,7 @@
2
2
  require 'graphql/schema/member/base_dsl_methods'
3
3
  require 'graphql/schema/member/graphql_type_names'
4
4
  require 'graphql/schema/member/has_ast_node'
5
+ require 'graphql/schema/member/has_authorization'
5
6
  require 'graphql/schema/member/has_dataloader'
6
7
  require 'graphql/schema/member/has_directives'
7
8
  require 'graphql/schema/member/has_deprecation_reason'
@@ -31,6 +32,10 @@ module GraphQL
31
32
  extend HasPath
32
33
  extend HasAstNode
33
34
  extend HasDirectives
35
+
36
+ def self.authorizes?(_ctx)
37
+ false
38
+ end
34
39
  end
35
40
  end
36
41
  end
@@ -5,6 +5,7 @@ require "graphql/query/null_context"
5
5
  module GraphQL
6
6
  class Schema
7
7
  class Object < GraphQL::Schema::Member
8
+ extend GraphQL::Schema::Member::HasAuthorization
8
9
  extend GraphQL::Schema::Member::HasFields
9
10
  extend GraphQL::Schema::Member::HasInterfaces
10
11
  include Member::HasDataloader
@@ -23,6 +23,7 @@ module GraphQL
23
23
  # Really we only need description & comment from here, but:
24
24
  extend Schema::Member::BaseDSLMethods
25
25
  extend GraphQL::Schema::Member::HasArguments
26
+ extend GraphQL::Schema::Member::HasAuthorization
26
27
  extend GraphQL::Schema::Member::HasValidators
27
28
  include Schema::Member::HasPath
28
29
  extend Schema::Member::HasPath
@@ -45,8 +46,10 @@ module GraphQL
45
46
  @prepared_arguments = nil
46
47
  end
47
48
 
49
+ attr_accessor :exec_result, :exec_index, :field_resolve_step
50
+
48
51
  # @return [Object] The application object this field is being resolved on
49
- attr_reader :object
52
+ attr_accessor :object
50
53
 
51
54
  # @return [GraphQL::Query::Context]
52
55
  attr_reader :context
@@ -54,6 +57,47 @@ module GraphQL
54
57
  # @return [GraphQL::Schema::Field]
55
58
  attr_reader :field
56
59
 
60
+ attr_writer :prepared_arguments
61
+
62
+ def call
63
+ if self.class < Schema::HasSingleInputArgument
64
+ @prepared_arguments = @prepared_arguments[:input]
65
+ end
66
+ q = context.query
67
+ trace_objs = [object]
68
+ q.current_trace.begin_execute_field(field, @prepared_arguments, trace_objs, q)
69
+ is_authed, new_return_value = authorized?(**@prepared_arguments)
70
+
71
+ if (runner = @field_resolve_step.runner).resolves_lazies && runner.schema.lazy?(is_authed)
72
+ is_authed, new_return_value = runner.schema.sync_lazy(is_authed)
73
+ end
74
+
75
+ result = if is_authed
76
+ Schema::Validator.validate!(self.class.validators, object, context, @prepared_arguments, as: @field)
77
+ call_resolve(@prepared_arguments)
78
+ else
79
+ new_return_value
80
+ end
81
+ q = context.query
82
+ q.current_trace.end_execute_field(field, @prepared_arguments, trace_objs, q, [result])
83
+
84
+ exec_result[exec_index] = result
85
+ rescue RuntimeError => err
86
+ exec_result[exec_index] = err
87
+ rescue StandardError => stderr
88
+ exec_result[exec_index] = begin
89
+ context.query.handle_or_reraise(stderr)
90
+ rescue GraphQL::ExecutionError => ex_err
91
+ ex_err
92
+ end
93
+ ensure
94
+ field_pending_steps = field_resolve_step.pending_steps
95
+ field_pending_steps.delete(self)
96
+ if field_pending_steps.size == 0 && field_resolve_step.field_results
97
+ field_resolve_step.runner.add_step(field_resolve_step)
98
+ end
99
+ end
100
+
57
101
  def arguments
58
102
  @prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
59
103
  end
@@ -8,6 +8,13 @@ module GraphQL
8
8
  #
9
9
  # (This is for specifying mutually exclusive sets of arguments.)
10
10
  #
11
+ # If you use {GraphQL::Schema::Visibility} to hide all the arguments in a `one_of: [..]` set,
12
+ # then a developer-facing {GraphQL::Error} will be raised during execution. Pass `allow_all_hidden: true` to
13
+ # skip validation in this case instead.
14
+ #
15
+ # This validator also implements `argument ... required: :nullable`. If an argument has `required: :nullable`
16
+ # but it's hidden with {GraphQL::Schema::Visibility}, then this validator doesn't run.
17
+ #
11
18
  # @example Require exactly one of these arguments
12
19
  #
13
20
  # field :update_amount, IngredientAmount, null: false do
@@ -37,15 +44,17 @@ module GraphQL
37
44
  class RequiredValidator < Validator
38
45
  # @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
39
46
  # @param argument [Symbol] An argument that is required for this field
47
+ # @param allow_all_hidden [Boolean] If `true`, then this validator won't run if all the `one_of: ...` arguments have been hidden
40
48
  # @param message [String]
41
- def initialize(one_of: nil, argument: nil, message: nil, **default_options)
49
+ def initialize(one_of: nil, argument: nil, allow_all_hidden: nil, message: nil, **default_options)
42
50
  @one_of = if one_of
43
51
  one_of
44
52
  elsif argument
45
- [argument]
53
+ [ argument ]
46
54
  else
47
55
  raise ArgumentError, "`one_of:` or `argument:` must be given in `validates required: {...}`"
48
56
  end
57
+ @allow_all_hidden = allow_all_hidden.nil? ? !!argument : allow_all_hidden
49
58
  @message = message
50
59
  super(**default_options)
51
60
  end
@@ -54,10 +63,17 @@ module GraphQL
54
63
  fully_matched_conditions = 0
55
64
  partially_matched_conditions = 0
56
65
 
66
+ visible_keywords = context.types.arguments(@validated).map(&:keyword)
67
+ no_visible_conditions = true
68
+
57
69
  if !value.nil?
58
70
  @one_of.each do |one_of_condition|
59
71
  case one_of_condition
60
72
  when Symbol
73
+ if no_visible_conditions && visible_keywords.include?(one_of_condition)
74
+ no_visible_conditions = false
75
+ end
76
+
61
77
  if value.key?(one_of_condition)
62
78
  fully_matched_conditions += 1
63
79
  end
@@ -66,6 +82,9 @@ module GraphQL
66
82
  full_match = true
67
83
 
68
84
  one_of_condition.each do |k|
85
+ if no_visible_conditions && visible_keywords.include?(k)
86
+ no_visible_conditions = false
87
+ end
69
88
  if value.key?(k)
70
89
  any_match = true
71
90
  else
@@ -88,6 +107,18 @@ module GraphQL
88
107
  end
89
108
  end
90
109
 
110
+ if no_visible_conditions
111
+ if @allow_all_hidden
112
+ return nil
113
+ else
114
+ raise GraphQL::Error, <<~ERR
115
+ #{@validated.path} validates `required: ...` but all required arguments were hidden.
116
+
117
+ Update your schema definition to allow the client to see some fields or skip validation by adding `required: { ..., allow_all_hidden: true }`
118
+ ERR
119
+ end
120
+ end
121
+
91
122
  if fully_matched_conditions == 1 && partially_matched_conditions == 0
92
123
  nil # OK
93
124
  else
@@ -10,9 +10,9 @@ module GraphQL
10
10
  class Visibility
11
11
  # @param schema [Class<GraphQL::Schema>]
12
12
  # @param profiles [Hash<Symbol => Hash>] A hash of `name => context` pairs for preloading visibility profiles
13
- # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?`)
13
+ # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?` and `Rails.env.staging?`)
14
14
  # @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
15
- def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? Rails.env.production? : nil), migration_errors: false)
15
+ def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? (Rails.env.production? || Rails.env.staging?) : nil), migration_errors: false)
16
16
  profiles&.each { |name, ctx|
17
17
  ctx[:visibility_profile] = name
18
18
  ctx.freeze
@@ -191,7 +191,7 @@ module GraphQL
191
191
  if refresh
192
192
  @top_level_profile = nil
193
193
  end
194
- @top_level_profile ||= @schema.visibility_profile_class.new(context: Query::NullContext.instance, schema: @schema, visibility: self)
194
+ @top_level_profile ||= @schema.visibility_profile_class.new(context: @schema.null_context, schema: @schema, visibility: self)
195
195
  end
196
196
 
197
197
  private
@@ -330,10 +330,16 @@ module GraphQL
330
330
  find_inherited_value(:plugins, EMPTY_ARRAY) + own_plugins
331
331
  end
332
332
 
333
+ attr_writer :null_context
334
+
335
+ def null_context
336
+ @null_context || GraphQL::Query::NullContext.instance
337
+ end
338
+
333
339
  # Build a map of `{ name => type }` and return it
334
340
  # @return [Hash<String => Class>] A dictionary of type classes by their GraphQL name
335
341
  # @see get_type Which is more efficient for finding _one type_ by name, because it doesn't merge hashes.
336
- def types(context = GraphQL::Query::NullContext.instance)
342
+ def types(context = null_context)
337
343
  if use_visibility_profile?
338
344
  types = Visibility::Profile.from_context(context, self)
339
345
  return types.all_types_h
@@ -366,7 +372,7 @@ module GraphQL
366
372
  # @param context [GraphQL::Query::Context] Used for filtering definitions at query-time
367
373
  # @param use_visibility_profile Private, for migration to {Schema::Visibility}
368
374
  # @return [Module, nil] A type, or nil if there's no type called `type_name`
369
- def get_type(type_name, context = GraphQL::Query::NullContext.instance, use_visibility_profile = use_visibility_profile?)
375
+ def get_type(type_name, context = null_context, use_visibility_profile = use_visibility_profile?)
370
376
  if use_visibility_profile
371
377
  profile = Visibility::Profile.from_context(context, self)
372
378
  return profile.type(type_name)
@@ -617,7 +623,7 @@ module GraphQL
617
623
  # @param use_visibility_profile Private, for migration to {Schema::Visibility}
618
624
  # @return [Hash<String, Module>] All possible types, if no `type` is given.
619
625
  # @return [Array<Module>] Possible types for `type`, if it's given.
620
- def possible_types(type = nil, context = GraphQL::Query::NullContext.instance, use_visibility_profile = use_visibility_profile?)
626
+ def possible_types(type = nil, context = null_context, use_visibility_profile = use_visibility_profile?)
621
627
  if use_visibility_profile
622
628
  if type
623
629
  return Visibility::Profile.from_context(context, self).possible_types(type)
@@ -701,7 +707,7 @@ module GraphQL
701
707
  GraphQL::Schema::TypeExpression.build_type(context.query.types, ast_node)
702
708
  end
703
709
 
704
- def get_field(type_or_name, field_name, context = GraphQL::Query::NullContext.instance, use_visibility_profile = use_visibility_profile?)
710
+ def get_field(type_or_name, field_name, context = null_context, use_visibility_profile = use_visibility_profile?)
705
711
  if use_visibility_profile
706
712
  profile = Visibility::Profile.from_context(context, self)
707
713
  parent_type = case type_or_name
@@ -738,7 +744,7 @@ module GraphQL
738
744
  end
739
745
  end
740
746
 
741
- def get_fields(type, context = GraphQL::Query::NullContext.instance)
747
+ def get_fields(type, context = null_context)
742
748
  type.fields(context)
743
749
  end
744
750
 
@@ -1228,6 +1234,7 @@ module GraphQL
1228
1234
  vis = self.visibility
1229
1235
  child_class.visibility = vis.dup_for(child_class)
1230
1236
  end
1237
+ child_class.null_context = Query::NullContext.new(schema: child_class)
1231
1238
  super
1232
1239
  end
1233
1240
 
@@ -1329,10 +1336,11 @@ module GraphQL
1329
1336
  def type_error(type_error, context)
1330
1337
  case type_error
1331
1338
  when GraphQL::InvalidNullError
1332
- execution_error = GraphQL::ExecutionError.new(type_error.message, ast_node: type_error.ast_node)
1333
- execution_error.path = context[:current_path]
1339
+ execution_error = GraphQL::ExecutionError.new(type_error.message, ast_nodes: type_error.ast_nodes)
1340
+ execution_error.path = type_error.path || context[:current_path]
1334
1341
 
1335
1342
  context.errors << execution_error
1343
+ execution_error
1336
1344
  when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
1337
1345
  raise type_error
1338
1346
  when GraphQL::IntegerDecodingError
@@ -1354,6 +1362,24 @@ module GraphQL
1354
1362
  lazy_methods.set(lazy_class, value_method)
1355
1363
  end
1356
1364
 
1365
+ def uses_raw_value?
1366
+ !!@uses_raw_value
1367
+ end
1368
+
1369
+ def uses_raw_value(new_val)
1370
+ @uses_raw_value = new_val
1371
+ end
1372
+
1373
+ def resolves_lazies?
1374
+ lazy_method_count = 0
1375
+ lazy_methods.each do |k, v|
1376
+ if !v.nil?
1377
+ lazy_method_count += 1
1378
+ end
1379
+ end
1380
+ lazy_method_count > 2
1381
+ end
1382
+
1357
1383
  def instrument(instrument_step, instrumenter, options = {})
1358
1384
  warn <<~WARN
1359
1385
  Schema.instrument is deprecated, use `trace_with` instead: https://graphql-ruby.org/queries/tracing.html"
@@ -1708,7 +1734,7 @@ module GraphQL
1708
1734
  # If you need to support previous, non-spec behavior which allowed selecting union fields
1709
1735
  # but *not* selecting any fields on that union, set this to `true` to continue allowing that behavior.
1710
1736
  #
1711
- # If this is `true`, then {.legacy_invalid_empty_selections_on_union} will be called with {Query} objects
1737
+ # If this is `true`, then {.legacy_invalid_empty_selections_on_union_with_type} will be called with {Query} objects
1712
1738
  # with that kind of selections. You must implement that method
1713
1739
  # @param new_value [Boolean]
1714
1740
  # @return [true, false, nil]
@@ -1724,6 +1750,22 @@ module GraphQL
1724
1750
  end
1725
1751
  end
1726
1752
 
1753
+ # This method is called during validation when a previously-allowed, but non-spec
1754
+ # query is encountered where a union field has no child selections on it.
1755
+ #
1756
+ # If `legacy_invalid_empty_selections_on_union_with_type` is overridden, this method will not be called.
1757
+ #
1758
+ # You should implement this method or `legacy_invalid_empty_selections_on_union_with_type`
1759
+ # to log the violation so that you can contact clients and notify them about changing their queries.
1760
+ # Then return a suitable value to tell GraphQL-Ruby how to continue.
1761
+ # @param query [GraphQL::Query]
1762
+ # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
1763
+ # @return [String] A validation error to return for this query
1764
+ # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
1765
+ def legacy_invalid_empty_selections_on_union(query)
1766
+ raise "Implement `def self.legacy_invalid_empty_selections_on_union_with_type(query, type)` or `def self.legacy_invalid_empty_selections_on_union(query)` to handle this scenario"
1767
+ end
1768
+
1727
1769
  # This method is called during validation when a previously-allowed, but non-spec
1728
1770
  # query is encountered where a union field has no child selections on it.
1729
1771
  #
@@ -1731,11 +1773,12 @@ module GraphQL
1731
1773
  # and notify them about changing their queries. Then return a suitable value to
1732
1774
  # tell GraphQL-Ruby how to continue.
1733
1775
  # @param query [GraphQL::Query]
1776
+ # @param type [Module] A GraphQL type definition
1734
1777
  # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
1735
1778
  # @return [String] A validation error to return for this query
1736
1779
  # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
1737
- def legacy_invalid_empty_selections_on_union(query)
1738
- raise "Implement `def self.legacy_invalid_empty_selections_on_union(query)` to handle this scenario"
1780
+ def legacy_invalid_empty_selections_on_union_with_type(query, type)
1781
+ legacy_invalid_empty_selections_on_union(query)
1739
1782
  end
1740
1783
 
1741
1784
  # This setting controls how GraphQL-Ruby handles overlapping selections on scalar types when the types
@@ -49,7 +49,7 @@ module GraphQL
49
49
  if !resolved_type.kind.fields?
50
50
  case @schema.allow_legacy_invalid_empty_selections_on_union
51
51
  when true
52
- legacy_invalid_empty_selection_result = @schema.legacy_invalid_empty_selections_on_union(@context.query)
52
+ legacy_invalid_empty_selection_result = @schema.legacy_invalid_empty_selections_on_union_with_type(@context.query, resolved_type)
53
53
  case legacy_invalid_empty_selection_result
54
54
  when :return_validation_error
55
55
  # keep `return_validation_error = true`
@@ -61,7 +61,7 @@ module GraphQL
61
61
  return_validation_error = false
62
62
  legacy_invalid_empty_selection_result = nil
63
63
  else
64
- raise GraphQL::InvariantError, "Unexpected return value from legacy_invalid_empty_selections_on_union, must be `:return_validation_error`, String, or nil (got: #{legacy_invalid_empty_selection_result.inspect})"
64
+ raise GraphQL::InvariantError, "Unexpected return value from legacy_invalid_empty_selections_on_union_with_type, must be `:return_validation_error`, String, or nil (got: #{legacy_invalid_empty_selection_result.inspect})"
65
65
  end
66
66
  when false
67
67
  # pass -- error below
@@ -81,6 +81,7 @@ module GraphQL
81
81
  # end
82
82
  # end
83
83
  #
84
+ # @see GraphQL::Testing::MockActionCable for test helpers
84
85
  class ActionCableSubscriptions < GraphQL::Subscriptions
85
86
  SUBSCRIPTION_PREFIX = "graphql-subscription:"
86
87
  EVENT_PREFIX = "graphql-event:"
@@ -80,7 +80,7 @@ module GraphQL
80
80
 
81
81
  # Normalize symbol-keyed args to strings, try camelizing them
82
82
  # Should this accept a real context somehow?
83
- normalized_args = normalize_arguments(normalized_event_name, field, args, GraphQL::Query::NullContext.instance)
83
+ normalized_args = normalize_arguments(normalized_event_name, field, args, @schema.null_context)
84
84
 
85
85
  event = Subscriptions::Event.new(
86
86
  name: normalized_event_name,
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Testing
4
+ # A stub implementation of ActionCable.
5
+ # Any methods to support the mock backend have `mock` in the name.
6
+ #
7
+ # @example Configuring your schema to use MockActionCable in the test environment
8
+ # class MySchema < GraphQL::Schema
9
+ # # Use MockActionCable in test:
10
+ # use GraphQL::Subscriptions::ActionCableSubscriptions,
11
+ # action_cable: Rails.env.test? ? GraphQL::Testing::MockActionCable : ActionCable
12
+ # end
13
+ #
14
+ # @example Clearing old data before each test
15
+ # setup do
16
+ # GraphQL::Testing::MockActionCable.clear_mocks
17
+ # end
18
+ #
19
+ # @example Using MockActionCable in a test case
20
+ # # Create a channel to use in the test, pass it to GraphQL
21
+ # mock_channel = GraphQL::Testing::MockActionCable.get_mock_channel
22
+ # ActionCableTestSchema.execute("subscription { newsFlash { text } }", context: { channel: mock_channel })
23
+ #
24
+ # # Trigger a subscription update
25
+ # ActionCableTestSchema.subscriptions.trigger(:news_flash, {}, {text: "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"})
26
+ #
27
+ # # Check messages on the channel
28
+ # expected_msg = {
29
+ # result: {
30
+ # "data" => {
31
+ # "newsFlash" => {
32
+ # "text" => "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"
33
+ # }
34
+ # }
35
+ # },
36
+ # more: true,
37
+ # }
38
+ # assert_equal [expected_msg], mock_channel.mock_broadcasted_messages
39
+ #
40
+ class MockActionCable
41
+ class MockChannel
42
+ def initialize
43
+ @mock_broadcasted_messages = []
44
+ end
45
+
46
+ # @return [Array<Hash>] Payloads "sent" to this channel by GraphQL-Ruby
47
+ attr_reader :mock_broadcasted_messages
48
+
49
+ # Called by ActionCableSubscriptions. Implements a Rails API.
50
+ def stream_from(stream_name, coder: nil, &block)
51
+ # Rails uses `coder`, we don't
52
+ block ||= ->(msg) { @mock_broadcasted_messages << msg }
53
+ MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block)
54
+ end
55
+ end
56
+
57
+ # Used by mock code
58
+ # @api private
59
+ class MockStream
60
+ def initialize
61
+ @mock_channels = {}
62
+ end
63
+
64
+ def add_mock_channel(channel, handler)
65
+ @mock_channels[channel] = handler
66
+ end
67
+
68
+ def mock_broadcast(message)
69
+ @mock_channels.each do |channel, handler|
70
+ handler && handler.call(message)
71
+ end
72
+ end
73
+ end
74
+
75
+ class << self
76
+ # Call this before each test run to make sure that MockActionCable's data is empty
77
+ def clear_mocks
78
+ @mock_streams = {}
79
+ end
80
+
81
+ # Implements Rails API
82
+ def server
83
+ self
84
+ end
85
+
86
+ # Implements Rails API
87
+ def broadcast(stream_name, message)
88
+ stream = @mock_streams[stream_name]
89
+ stream && stream.mock_broadcast(message)
90
+ end
91
+
92
+ # Used by mock code
93
+ def mock_stream_for(stream_name)
94
+ @mock_streams[stream_name] ||= MockStream.new
95
+ end
96
+
97
+ # Use this as `context[:channel]` to simulate an ActionCable channel
98
+ #
99
+ # @return [GraphQL::Testing::MockActionCable::MockChannel]
100
+ def get_mock_channel
101
+ MockChannel.new
102
+ end
103
+
104
+ # @return [Array<String>] Streams that currently have subscribers
105
+ def mock_stream_names
106
+ @mock_streams.keys
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -1,2 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/testing/helpers"
3
+ require "graphql/testing/mock_action_cable"