graphql 2.5.18 → 2.5.20

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 (45) 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.rb +9 -74
  9. data/lib/graphql/dataloader/null_dataloader.rb +7 -3
  10. data/lib/graphql/execution/batching/field_compatibility.rb +150 -0
  11. data/lib/graphql/execution/batching/field_resolve_step.rb +408 -0
  12. data/lib/graphql/execution/batching/prepare_object_step.rb +112 -0
  13. data/lib/graphql/execution/batching/runner.rb +352 -0
  14. data/lib/graphql/execution/batching/selections_step.rb +37 -0
  15. data/lib/graphql/execution/batching.rb +62 -0
  16. data/lib/graphql/execution_error.rb +13 -10
  17. data/lib/graphql/introspection/directive_type.rb +7 -3
  18. data/lib/graphql/introspection/entry_points.rb +6 -2
  19. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  20. data/lib/graphql/introspection/field_type.rb +13 -5
  21. data/lib/graphql/introspection/input_value_type.rb +21 -13
  22. data/lib/graphql/introspection/type_type.rb +64 -28
  23. data/lib/graphql/invalid_null_error.rb +11 -5
  24. data/lib/graphql/query/context.rb +3 -2
  25. data/lib/graphql/query/null_context.rb +9 -3
  26. data/lib/graphql/schema/argument.rb +4 -0
  27. data/lib/graphql/schema/build_from_definition.rb +26 -6
  28. data/lib/graphql/schema/field.rb +76 -1
  29. data/lib/graphql/schema/member/has_dataloader.rb +9 -0
  30. data/lib/graphql/schema/member/has_fields.rb +3 -0
  31. data/lib/graphql/schema/resolver.rb +3 -1
  32. data/lib/graphql/schema/visibility.rb +1 -1
  33. data/lib/graphql/schema.rb +33 -7
  34. data/lib/graphql/subscriptions.rb +1 -1
  35. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  36. data/lib/graphql/tracing/detailed_trace.rb +20 -5
  37. data/lib/graphql/tracing/perfetto_trace.rb +48 -2
  38. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  39. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  40. data/lib/graphql/types/relay/has_node_field.rb +5 -8
  41. data/lib/graphql/types/relay/has_nodes_field.rb +5 -8
  42. data/lib/graphql/unauthorized_error.rb +5 -1
  43. data/lib/graphql/version.rb +1 -1
  44. data/lib/graphql.rb +3 -0
  45. metadata +14 -2
@@ -9,53 +9,61 @@ module GraphQL
9
9
  field :name, String, null: false
10
10
  field :description, String
11
11
  field :type, GraphQL::Schema::LateBoundType.new("__Type"), null: false
12
- field :default_value, String, "A GraphQL-formatted string representing the default value for this input value."
13
- field :is_deprecated, Boolean, null: false
12
+ field :default_value, String, "A GraphQL-formatted string representing the default value for this input value.", resolve_each: :resolve_default_value
13
+ field :is_deprecated, Boolean, null: false, resolve_each: :resolve_is_deprecated
14
14
  field :deprecation_reason, String
15
15
 
16
+ def self.resolve_is_deprecated(object, context)
17
+ !!object.deprecation_reason
18
+ end
19
+
16
20
  def is_deprecated
17
- !!@object.deprecation_reason
21
+ self.class.resolve_is_deprecated(object, context)
18
22
  end
19
23
 
20
- def default_value
21
- if @object.default_value?
22
- value = @object.default_value
24
+ def self.resolve_default_value(object, context)
25
+ if object.default_value?
26
+ value = object.default_value
23
27
  if value.nil?
24
28
  'null'
25
29
  else
26
- if (@object.type.kind.list? || (@object.type.kind.non_null? && @object.type.of_type.kind.list?)) && !value.respond_to?(:map)
30
+ if (object.type.kind.list? || (object.type.kind.non_null? && object.type.of_type.kind.list?)) && !value.respond_to?(:map)
27
31
  # This is a bit odd -- we expect the default value to be an application-style value, so we use coerce result below.
28
32
  # But coerce_result doesn't wrap single-item lists, which are valid inputs to list types.
29
33
  # So, apply that wrapper here if needed.
30
34
  value = [value]
31
35
  end
32
- coerced_default_value = @object.type.coerce_result(value, @context)
33
- serialize_default_value(coerced_default_value, @object.type)
36
+ coerced_default_value = object.type.coerce_result(value, context)
37
+ serialize_default_value(coerced_default_value, object.type, context)
34
38
  end
35
39
  else
36
40
  nil
37
41
  end
38
42
  end
39
43
 
44
+ def default_value
45
+ self.class.resolve_default_value(object, context)
46
+ end
47
+
40
48
 
41
49
  private
42
50
 
43
51
  # Recursively serialize, taking care not to add quotes to enum values
44
- def serialize_default_value(value, type)
52
+ def self.serialize_default_value(value, type, context)
45
53
  if value.nil?
46
54
  'null'
47
55
  elsif type.kind.list?
48
56
  inner_type = type.of_type
49
- "[" + value.map { |v| serialize_default_value(v, inner_type) }.join(", ") + "]"
57
+ "[" + value.map { |v| serialize_default_value(v, inner_type, context) }.join(", ") + "]"
50
58
  elsif type.kind.non_null?
51
- serialize_default_value(value, type.of_type)
59
+ serialize_default_value(value, type.of_type, context)
52
60
  elsif type.kind.enum?
53
61
  value
54
62
  elsif type.kind.input_object?
55
63
  "{" +
56
64
  value.map do |k, v|
57
65
  arg_defn = type.get_argument(k, context)
58
- "#{k}: #{serialize_default_value(v, arg_defn.type)}"
66
+ "#{k}: #{serialize_default_value(v, arg_defn.type, context)}"
59
67
  end.join(", ") +
60
68
  "}"
61
69
  else
@@ -11,32 +11,36 @@ module GraphQL
11
11
  "they describe. Abstract types, Union and Interface, provide the Object types "\
12
12
  "possible at runtime. List and NonNull types compose other types."
13
13
 
14
- field :kind, GraphQL::Schema::LateBoundType.new("__TypeKind"), null: false
14
+ field :kind, GraphQL::Schema::LateBoundType.new("__TypeKind"), null: false, resolve_each: :resolve_kind
15
15
  field :name, String, method: :graphql_name
16
16
  field :description, String
17
- field :fields, [GraphQL::Schema::LateBoundType.new("__Field")], scope: false do
17
+ field :fields, [GraphQL::Schema::LateBoundType.new("__Field")], scope: false, resolve_each: :resolve_fields do
18
18
  argument :include_deprecated, Boolean, required: false, default_value: false
19
19
  end
20
- field :interfaces, [GraphQL::Schema::LateBoundType.new("__Type")], scope: false
21
- field :possible_types, [GraphQL::Schema::LateBoundType.new("__Type")], scope: false
22
- field :enum_values, [GraphQL::Schema::LateBoundType.new("__EnumValue")], scope: false do
20
+ field :interfaces, [GraphQL::Schema::LateBoundType.new("__Type")], scope: false, resolve_each: :resolve_interfaces
21
+ field :possible_types, [GraphQL::Schema::LateBoundType.new("__Type")], scope: false, resolve_each: :resolve_possible_types
22
+ field :enum_values, [GraphQL::Schema::LateBoundType.new("__EnumValue")], scope: false, resolve_each: :resolve_enum_values do
23
23
  argument :include_deprecated, Boolean, required: false, default_value: false
24
24
  end
25
- field :input_fields, [GraphQL::Schema::LateBoundType.new("__InputValue")], scope: false do
25
+ field :input_fields, [GraphQL::Schema::LateBoundType.new("__InputValue")], scope: false, resolve_each: :resolve_input_fields do
26
26
  argument :include_deprecated, Boolean, required: false, default_value: false
27
27
  end
28
- field :of_type, GraphQL::Schema::LateBoundType.new("__Type")
28
+ field :of_type, GraphQL::Schema::LateBoundType.new("__Type"), resolve_each: :resolve_of_type
29
29
 
30
- field :specifiedByURL, String, resolver_method: :specified_by_url
30
+ field :specifiedByURL, String, resolve_each: :resolve_specified_by_url, resolver_method: :specified_by_url
31
31
 
32
- field :is_one_of, Boolean, null: false
32
+ field :is_one_of, Boolean, null: false, resolve_each: :resolve_is_one_of
33
33
 
34
- def is_one_of
34
+ def self.resolve_is_one_of(object, _ctx)
35
35
  object.kind.input_object? &&
36
36
  object.directives.any? { |d| d.graphql_name == "oneOf" }
37
37
  end
38
38
 
39
- def specified_by_url
39
+ def is_one_of
40
+ self.class.resolve_is_one_of(object, context)
41
+ end
42
+
43
+ def self.resolve_specified_by_url(object, _ctx)
40
44
  if object.kind.scalar?
41
45
  object.specified_by_url
42
46
  else
@@ -44,15 +48,23 @@ module GraphQL
44
48
  end
45
49
  end
46
50
 
51
+ def specified_by_url
52
+ self.class.resolve_specified_by_url(object, context)
53
+ end
54
+
55
+ def self.resolve_kind(object, context)
56
+ object.kind.name
57
+ end
58
+
47
59
  def kind
48
- @object.kind.name
60
+ self.class.resolve_kind(object, context)
49
61
  end
50
62
 
51
- def enum_values(include_deprecated:)
52
- if !@object.kind.enum?
63
+ def self.resolve_enum_values(object, context, include_deprecated:)
64
+ if !object.kind.enum?
53
65
  nil
54
66
  else
55
- enum_values = @context.types.enum_values(@object)
67
+ enum_values = context.types.enum_values(object)
56
68
 
57
69
  if !include_deprecated
58
70
  enum_values = enum_values.select {|f| !f.deprecation_reason }
@@ -62,17 +74,25 @@ module GraphQL
62
74
  end
63
75
  end
64
76
 
65
- def interfaces
66
- if @object.kind.object? || @object.kind.interface?
67
- @context.types.interfaces(@object).sort_by(&:graphql_name)
77
+ def enum_values(include_deprecated:)
78
+ self.class.resolve_enum_values(object, context, include_deprecated: include_deprecated)
79
+ end
80
+
81
+ def self.resolve_interfaces(object, context)
82
+ if object.kind.object? || object.kind.interface?
83
+ context.types.interfaces(object).sort_by(&:graphql_name)
68
84
  else
69
85
  nil
70
86
  end
71
87
  end
72
88
 
73
- def input_fields(include_deprecated:)
74
- if @object.kind.input_object?
75
- args = @context.types.arguments(@object)
89
+ def interfaces
90
+ self.class.resolve_interfaces(object, context)
91
+ end
92
+
93
+ def self.resolve_input_fields(object, context, include_deprecated:)
94
+ if object.kind.input_object?
95
+ args = context.types.arguments(object)
76
96
  args = args.reject(&:deprecation_reason) unless include_deprecated
77
97
  args
78
98
  else
@@ -80,19 +100,27 @@ module GraphQL
80
100
  end
81
101
  end
82
102
 
83
- def possible_types
84
- if @object.kind.abstract?
85
- @context.types.possible_types(@object).sort_by(&:graphql_name)
103
+ def input_fields(include_deprecated:)
104
+ self.class.resolve_input_fields(object, context, include_deprecated: include_deprecated)
105
+ end
106
+
107
+ def self.resolve_possible_types(object, context)
108
+ if object.kind.abstract?
109
+ context.types.possible_types(object).sort_by(&:graphql_name)
86
110
  else
87
111
  nil
88
112
  end
89
113
  end
90
114
 
91
- def fields(include_deprecated:)
92
- if !@object.kind.fields?
115
+ def possible_types
116
+ self.class.resolve_possible_types(object, context)
117
+ end
118
+
119
+ def self.resolve_fields(object, context, include_deprecated:)
120
+ if !object.kind.fields?
93
121
  nil
94
122
  else
95
- fields = @context.types.fields(@object)
123
+ fields = context.types.fields(object)
96
124
  if !include_deprecated
97
125
  fields = fields.select {|f| !f.deprecation_reason }
98
126
  end
@@ -100,8 +128,16 @@ module GraphQL
100
128
  end
101
129
  end
102
130
 
131
+ def fields(include_deprecated:)
132
+ self.class.resolve_fields(object, context, include_deprecated: include_deprecated)
133
+ end
134
+
135
+ def self.resolve_of_type(object, _ctx)
136
+ object.kind.wraps? ? object.of_type : nil
137
+ end
138
+
103
139
  def of_type
104
- @object.kind.wraps? ? @object.of_type : nil
140
+ self.class.resolve_of_type(object, context)
105
141
  end
106
142
  end
107
143
  end
@@ -2,7 +2,7 @@
2
2
  module GraphQL
3
3
  # Raised automatically when a field's resolve function returns `nil`
4
4
  # for a non-null field.
5
- class InvalidNullError < GraphQL::Error
5
+ class InvalidNullError < GraphQL::RuntimeError
6
6
  # @return [GraphQL::BaseType] The owner of {#field}
7
7
  attr_reader :parent_type
8
8
 
@@ -10,17 +10,23 @@ module GraphQL
10
10
  attr_reader :field
11
11
 
12
12
  # @return [GraphQL::Language::Nodes::Field] the field where the error occurred
13
- attr_reader :ast_node
13
+ def ast_node
14
+ @ast_nodes.first
15
+ end
16
+
17
+ attr_reader :ast_nodes
14
18
 
15
19
  # @return [Boolean] indicates an array result caused the error
16
20
  attr_reader :is_from_array
17
21
 
18
- def initialize(parent_type, field, ast_node, is_from_array: false)
22
+ attr_accessor :path
23
+
24
+ def initialize(parent_type, field, ast_node_or_nodes, is_from_array: false, path: nil)
19
25
  @parent_type = parent_type
20
26
  @field = field
21
- @ast_node = ast_node
27
+ @ast_nodes = Array(ast_node_or_nodes)
22
28
  @is_from_array = is_from_array
23
-
29
+ @path = path
24
30
  # For List elements, identify the non-null error is for an
25
31
  # element and the required element type so it's not ambiguous
26
32
  # whether it was caused by a null instead of the list or a
@@ -29,6 +29,7 @@ module GraphQL
29
29
  end
30
30
 
31
31
  extend Forwardable
32
+ include Schema::Member::HasDataloader
32
33
 
33
34
  # @return [Array<GraphQL::ExecutionError>] errors returned during execution
34
35
  attr_reader :errors
@@ -118,8 +119,8 @@ module GraphQL
118
119
  # @param error [GraphQL::ExecutionError] an execution error
119
120
  # @return [void]
120
121
  def add_error(error)
121
- if !error.is_a?(ExecutionError)
122
- raise TypeError, "expected error to be a ExecutionError, but was #{error.class}"
122
+ if !error.is_a?(GraphQL::RuntimeError)
123
+ raise TypeError, "expected error to be a GraphQL::RuntimeError, but was #{error.class}"
123
124
  end
124
125
  errors << error
125
126
  nil
@@ -4,7 +4,13 @@ module GraphQL
4
4
  class Query
5
5
  # This object can be `ctx` in places where there is no query
6
6
  class NullContext < Context
7
- include Singleton
7
+ def self.instance
8
+ @instance ||= self.new
9
+ end
10
+
11
+ def self.instance=(new_inst)
12
+ @instance = new_inst
13
+ end
8
14
 
9
15
  class NullQuery
10
16
  def after_lazy(value)
@@ -20,10 +26,10 @@ module GraphQL
20
26
  attr_reader :schema, :query, :warden, :dataloader
21
27
  def_delegators GraphQL::EmptyObjects::EMPTY_HASH, :[], :fetch, :dig, :key?, :to_h
22
28
 
23
- def initialize
29
+ def initialize(schema: NullSchema)
24
30
  @query = NullQuery.new
25
31
  @dataloader = GraphQL::Dataloader::NullDataloader.new
26
- @schema = NullSchema
32
+ @schema = schema
27
33
  @warden = Schema::Warden::NullWarden.new(context: self, schema: @schema)
28
34
  @types = @warden.visibility_profile
29
35
  freeze
@@ -164,6 +164,10 @@ module GraphQL
164
164
  true
165
165
  end
166
166
 
167
+ def authorizes?(_context)
168
+ self.method(:authorized?).owner != GraphQL::Schema::Argument
169
+ end
170
+
167
171
  def authorized?(obj, value, ctx)
168
172
  authorized_as_type?(obj, value, ctx, as_type: type)
169
173
  end
@@ -512,22 +512,42 @@ module GraphQL
512
512
  camelize: false,
513
513
  directives: prepare_directives(field_definition, type_resolver),
514
514
  resolver_method: resolve_method_name,
515
+ resolve_batch: resolve_method_name,
515
516
  )
516
517
 
517
518
  builder.build_arguments(schema_field_defn, field_definition.arguments, type_resolver)
518
519
 
519
520
  # Don't do this for interfaces
520
521
  if default_resolve
521
- define_field_resolve_method(owner, resolve_method_name, field_definition.name)
522
+ define_field_resolve_method(owner, resolve_method_name, field_definition.name, field_definition.arguments.empty?)
522
523
  end
523
524
  end
524
525
  end
525
526
 
526
- def define_field_resolve_method(owner, method_name, field_name)
527
- owner.define_method(method_name) { |**args|
528
- field_instance = context.types.field(owner, field_name)
529
- context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context)
530
- }
527
+ def define_field_resolve_method(owner, method_name, field_name, empty_arguments)
528
+ if empty_arguments
529
+ owner.define_method(method_name) {
530
+ field_instance = context.types.field(owner, field_name)
531
+ context.schema.definition_default_resolve.call(self.class, field_instance, object, EmptyObjects::EMPTY_HASH, context)
532
+ }
533
+ owner.define_singleton_method(method_name) { |objects, context|
534
+ field_instance = context.types.field(owner, field_name)
535
+ objects.map do |object|
536
+ context.schema.definition_default_resolve.call(self, field_instance, object, EmptyObjects::EMPTY_HASH, context)
537
+ end
538
+ }
539
+ else
540
+ owner.define_method(method_name) { |**args|
541
+ field_instance = context.types.field(owner, field_name)
542
+ context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context)
543
+ }
544
+ owner.define_singleton_method(method_name) { |objects, context, **args|
545
+ field_instance = context.types.field(owner, field_name)
546
+ objects.map do |object|
547
+ context.schema.definition_default_resolve.call(self, field_instance, object, args, context)
548
+ end
549
+ }
550
+ end
531
551
  end
532
552
 
533
553
  def build_resolve_type(lookup_hash, directives, missing_type_handler)
@@ -192,6 +192,9 @@ module GraphQL
192
192
  # @param resolver_method [Symbol] The method on the type to call to resolve this field (defaults to `name`)
193
193
  # @param connection [Boolean] `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
194
194
  # @param connection_extension [Class] The extension to add, to implement connections. If `nil`, no extension is added.
195
+ # @param resolve_static [Symbol, nil] Used by {Schema.execute_batching} to produce a single value, shared by all objects which resolve this field. Called on the owner type class with `context, **arguments`
196
+ # @param resolve_batch [Symbol, nil] Used by {Schema.execute_batching} map `objects` to a same-sized Array of results. Called on the owner type class with `objects, context, **arguments`.
197
+ # @param resolve_each [Symbol, nil] Used by {Schema.execute_batching} to get a value value for each item. Called on the owner type class with `object, context, **arguments`.
195
198
  # @param max_page_size [Integer, nil] For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
196
199
  # @param default_page_size [Integer, nil] For connections, the default number of items to return from this field, or `nil` to return unlimited results.
197
200
  # @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__`
@@ -214,7 +217,7 @@ module GraphQL
214
217
  # @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby)
215
218
  # @param extras [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] Extra arguments to be injected into the resolver for this field
216
219
  # @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}.
217
- def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
220
+ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, resolve_static: nil, resolve_each: nil, resolve_batch: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
218
221
  if name.nil?
219
222
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
220
223
  end
@@ -262,6 +265,27 @@ module GraphQL
262
265
  @method_str = -method_name.to_s
263
266
  @method_sym = method_name.to_sym
264
267
  @resolver_method = (resolver_method || name_s).to_sym
268
+
269
+ if resolve_static
270
+ @batch_mode = :resolve_static
271
+ @batch_mode_key = resolve_static == true ? @method_sym : resolve_static
272
+ elsif resolve_batch
273
+ @batch_mode = :resolve_batch
274
+ @batch_mode_key = resolve_batch == true ? @method_sym : resolve_batch
275
+ elsif resolve_each
276
+ @batch_mode = :resolve_each
277
+ @batch_mode_key = resolve_each == true ? @method_sym : resolve_each
278
+ elsif hash_key
279
+ @batch_mode = :hash_key
280
+ @batch_mode_key = hash_key
281
+ elsif dig
282
+ @batch_mode = :dig
283
+ @batch_mode_key = dig
284
+ else
285
+ @batch_mode = :direct_send
286
+ @batch_mode_key = @method_sym
287
+ end
288
+
265
289
  @complexity = complexity
266
290
  @dynamic_introspection = dynamic_introspection
267
291
  @return_type_expr = type
@@ -332,6 +356,50 @@ module GraphQL
332
356
  end
333
357
  end
334
358
 
359
+ # Called by {Execution::Batching} to resolve this field for each of `objects`
360
+ # @param field_resolve_step [Execution::Batching::FieldResolveStep] an internal metadata object from execution code
361
+ # @param objects [Array<Object>] Objects returned from previously-executed fields
362
+ # @param context [GraphQL::Query::Context]
363
+ # @param args_hash [Hash<Symbol => Object>] Ruby-style arguments for this field
364
+ # @return [Array<Object>] One field result for each of `objects`; must have the same length as `objects`
365
+ # @see #initialize Use `resolve_static:`, `resolve_batch:`, `resolve_each:`, `hash_key:`, or `method:`
366
+ # @api private
367
+ def resolve_batch(field_resolve_step, objects, context, args_hash)
368
+ case @batch_mode
369
+ when :resolve_batch
370
+ if args_hash.empty?
371
+ @owner.public_send(@batch_mode_key, objects, context)
372
+ else
373
+ @owner.public_send(@batch_mode_key, objects, context, **args_hash)
374
+ end
375
+ when :resolve_static
376
+ result = if args_hash.empty?
377
+ @owner.public_send(@batch_mode_key, context)
378
+ else
379
+ @owner.public_send(@batch_mode_key, context, **args_hash)
380
+ end
381
+ Array.new(objects.size, result)
382
+ when :resolve_each
383
+ if args_hash.empty?
384
+ objects.map { |o| @owner.public_send(@batch_mode_key, o, context) }
385
+ else
386
+ objects.map { |o| @owner.public_send(@batch_mode_key, o, context, **args_hash) }
387
+ end
388
+ when :hash_key
389
+ objects.map { |o| o[@batch_mode_key] }
390
+ when :direct_send
391
+ if args_hash.empty?
392
+ objects.map { |o| o.public_send(@batch_mode_key) }
393
+ else
394
+ objects.map { |o| o.public_send(@batch_mode_key, **args_hash) }
395
+ end
396
+ when :dig
397
+ objects.map { |o| o.dig(*@batch_mode_key) }
398
+ else
399
+ raise "Batching execution for #{path} not implemented; provide `resolve_static:`, `resolve_batch:`, `hash_key:`, `method:`, or use a compatibility plug-in"
400
+ end
401
+ end
402
+
335
403
  # Calls the definition block, if one was given.
336
404
  # This is deferred so that references to the return type
337
405
  # can be lazily evaluated, reducing Rails boot time.
@@ -625,6 +693,13 @@ module GraphQL
625
693
  end
626
694
  end
627
695
 
696
+ def authorizes?(context)
697
+ method(:authorized?).owner != GraphQL::Schema::Field || (
698
+ (args = context.types.arguments(self)) &&
699
+ (args.any? { |a| a.authorizes?(context) })
700
+ )
701
+ end
702
+
628
703
  def authorized?(object, args, context)
629
704
  if @resolver_class
630
705
  # The resolver _instance_ will check itself during `resolve()`
@@ -20,6 +20,15 @@ module GraphQL
20
20
  dataloader.with(source_class, *source_args).load(load_key)
21
21
  end
22
22
 
23
+ # A shortcut method for loading many keys from a source.
24
+ # Identical to `dataloader.with(source_class, *source_args).load_all(load_keys)`
25
+ # @param source_class [Class<GraphQL::Dataloader::Source>]
26
+ # @param source_args [Array<Object>] Any extra parameters defined in `source_class`'s `initialize` method
27
+ # @param load_keys [Array<Object>] The keys to look up using `def fetch`
28
+ def dataload_all(source_class, *source_args, load_keys)
29
+ dataloader.with(source_class, *source_args).load_all(load_keys)
30
+ end
31
+
23
32
  # Find an object with ActiveRecord via {Dataloader::ActiveRecordSource}.
24
33
  # @param model [Class<ActiveRecord::Base>]
25
34
  # @param find_by_value [Object] Usually an `id`, might be another value if `find_by:` is also provided
@@ -20,6 +20,9 @@ module GraphQL
20
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
21
  # @option kwargs [Array<String, Symbol>] :dig The nested hash keys to lookup on the underlying hash to resolve this field using dig
22
22
  # @option kwargs [Symbol] :resolver_method The method on the type to call to resolve this field (defaults to `name`)
23
+ # @option kwargs [Symbol] :resolve_static Used by {Schema.execute_batching} 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] :resolve_batch Used by {Schema.execute_batching} map `objects` to a same-sized Array of results. Called on the owner type class with `objects, context, **arguments`.
25
+ # @option kwargs [Symbol] :resolve_each Used by {Schema.execute_batching} to get a value value for each item. Called on the owner type class with `object, context, **arguments`.
23
26
  # @option kwargs [Boolean] :connection `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
24
27
  # @option kwargs [Class] :connection_extension The extension to add, to implement connections. If `nil`, no extension is added.
25
28
  # @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.
@@ -46,7 +46,7 @@ module GraphQL
46
46
  end
47
47
 
48
48
  # @return [Object] The application object this field is being resolved on
49
- attr_reader :object
49
+ attr_accessor :object
50
50
 
51
51
  # @return [GraphQL::Query::Context]
52
52
  attr_reader :context
@@ -54,6 +54,8 @@ module GraphQL
54
54
  # @return [GraphQL::Schema::Field]
55
55
  attr_reader :field
56
56
 
57
+ attr_writer :prepared_arguments
58
+
57
59
  def arguments
58
60
  @prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
59
61
  end
@@ -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"
@@ -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,