graphql 2.5.9 → 2.5.26

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 (127) 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/analysis.rb +20 -13
  5. data/lib/graphql/dashboard/application_controller.rb +41 -0
  6. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  7. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  8. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  9. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  10. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  11. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  12. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  13. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  15. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  16. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  17. data/lib/graphql/dashboard.rb +11 -73
  18. data/lib/graphql/dataloader/active_record_association_source.rb +14 -2
  19. data/lib/graphql/dataloader/async_dataloader.rb +22 -11
  20. data/lib/graphql/dataloader/null_dataloader.rb +54 -9
  21. data/lib/graphql/dataloader.rb +75 -23
  22. data/lib/graphql/date_encoding_error.rb +1 -1
  23. data/lib/graphql/execution/field_resolve_step.rb +631 -0
  24. data/lib/graphql/execution/finalize.rb +217 -0
  25. data/lib/graphql/execution/input_values.rb +261 -0
  26. data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
  27. data/lib/graphql/execution/interpreter/resolve.rb +10 -16
  28. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  29. data/lib/graphql/execution/interpreter/runtime.rb +28 -33
  30. data/lib/graphql/execution/interpreter.rb +8 -22
  31. data/lib/graphql/execution/lazy.rb +1 -1
  32. data/lib/graphql/execution/load_argument_step.rb +64 -0
  33. data/lib/graphql/execution/multiplex.rb +1 -1
  34. data/lib/graphql/execution/next.rb +90 -0
  35. data/lib/graphql/execution/prepare_object_step.rb +128 -0
  36. data/lib/graphql/execution/runner.rb +410 -0
  37. data/lib/graphql/execution/selections_step.rb +91 -0
  38. data/lib/graphql/execution.rb +8 -4
  39. data/lib/graphql/execution_error.rb +17 -10
  40. data/lib/graphql/introspection/directive_type.rb +7 -3
  41. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  42. data/lib/graphql/introspection/entry_points.rb +11 -3
  43. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  44. data/lib/graphql/introspection/field_type.rb +13 -5
  45. data/lib/graphql/introspection/input_value_type.rb +21 -13
  46. data/lib/graphql/introspection/type_type.rb +64 -28
  47. data/lib/graphql/invalid_null_error.rb +11 -5
  48. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  49. data/lib/graphql/language/lexer.rb +20 -9
  50. data/lib/graphql/language/nodes.rb +5 -1
  51. data/lib/graphql/language/parser.rb +1 -0
  52. data/lib/graphql/language.rb +21 -12
  53. data/lib/graphql/pagination/connection.rb +2 -0
  54. data/lib/graphql/pagination/connections.rb +32 -0
  55. data/lib/graphql/query/context.rb +11 -4
  56. data/lib/graphql/query/null_context.rb +9 -3
  57. data/lib/graphql/query/partial.rb +18 -3
  58. data/lib/graphql/query.rb +10 -1
  59. data/lib/graphql/runtime_error.rb +6 -0
  60. data/lib/graphql/schema/addition.rb +3 -1
  61. data/lib/graphql/schema/argument.rb +17 -0
  62. data/lib/graphql/schema/build_from_definition.rb +15 -2
  63. data/lib/graphql/schema/directive.rb +45 -13
  64. data/lib/graphql/schema/field/connection_extension.rb +4 -37
  65. data/lib/graphql/schema/field/scope_extension.rb +18 -13
  66. data/lib/graphql/schema/field.rb +87 -48
  67. data/lib/graphql/schema/field_extension.rb +11 -8
  68. data/lib/graphql/schema/interface.rb +26 -0
  69. data/lib/graphql/schema/list.rb +5 -1
  70. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -11
  71. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  72. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  73. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  74. data/lib/graphql/schema/member/has_fields.rb +86 -5
  75. data/lib/graphql/schema/member/has_interfaces.rb +2 -2
  76. data/lib/graphql/schema/member/type_system_helpers.rb +16 -2
  77. data/lib/graphql/schema/member.rb +5 -0
  78. data/lib/graphql/schema/non_null.rb +1 -1
  79. data/lib/graphql/schema/object.rb +1 -0
  80. data/lib/graphql/schema/ractor_shareable.rb +79 -0
  81. data/lib/graphql/schema/resolver.rb +60 -1
  82. data/lib/graphql/schema/subscription.rb +0 -2
  83. data/lib/graphql/schema/validator/required_validator.rb +45 -5
  84. data/lib/graphql/schema/visibility/migration.rb +2 -2
  85. data/lib/graphql/schema/visibility/profile.rb +140 -56
  86. data/lib/graphql/schema/visibility.rb +31 -18
  87. data/lib/graphql/schema/wrapper.rb +7 -1
  88. data/lib/graphql/schema.rb +108 -32
  89. data/lib/graphql/static_validation/all_rules.rb +1 -1
  90. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  91. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  92. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  93. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  94. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  95. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  96. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  97. data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
  98. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  99. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  100. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  101. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  102. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
  103. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  104. data/lib/graphql/static_validation/validation_context.rb +1 -1
  105. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  106. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +34 -10
  107. data/lib/graphql/subscriptions/event.rb +1 -0
  108. data/lib/graphql/subscriptions.rb +36 -1
  109. data/lib/graphql/testing/helpers.rb +12 -9
  110. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  111. data/lib/graphql/testing.rb +1 -0
  112. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  113. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  114. data/lib/graphql/tracing/null_trace.rb +1 -1
  115. data/lib/graphql/tracing/perfetto_trace.rb +209 -79
  116. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  117. data/lib/graphql/tracing/trace.rb +6 -0
  118. data/lib/graphql/type_kinds.rb +1 -0
  119. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  120. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  121. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  122. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  123. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  124. data/lib/graphql/unauthorized_error.rb +9 -1
  125. data/lib/graphql/version.rb +1 -1
  126. data/lib/graphql.rb +7 -3
  127. metadata +21 -3
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class RuntimeError < Error
4
+ include GraphQL::Execution::Finalizer
5
+ end
6
+ end
@@ -258,7 +258,9 @@ module GraphQL
258
258
  # We can get these now; we'll have to get late-bound types later
259
259
  if interface_type.is_a?(Module) && type.is_a?(Class)
260
260
  implementers = @possible_types[interface_type] ||= []
261
- implementers << type
261
+ if !implementers.include?(type)
262
+ implementers << type
263
+ end
262
264
  end
263
265
  when String, Schema::LateBoundType
264
266
  interface_type = interface_type_membership
@@ -4,6 +4,7 @@ module GraphQL
4
4
  class Argument
5
5
  include GraphQL::Schema::Member::HasPath
6
6
  include GraphQL::Schema::Member::HasAstNode
7
+ include GraphQL::Schema::Member::HasAuthorization
7
8
  include GraphQL::Schema::Member::HasDirectives
8
9
  include GraphQL::Schema::Member::HasDeprecationReason
9
10
  include GraphQL::Schema::Member::HasValidators
@@ -39,9 +40,14 @@ module GraphQL
39
40
  # @param arg_name [Symbol]
40
41
  # @param type_expr
41
42
  # @param desc [String]
43
+ # @param type [Class, Array<Class>] Input type; positional argument also accepted
44
+ # @param name [Symbol] positional argument also accepted # @param loads [Class, Array<Class>] A GraphQL type to load for the given ID when one is present
45
+ # @param definition_block [Proc] Called with the newly-created {Argument}
46
+ # @param owner [Class] Private, used by GraphQL-Ruby during schema definition
42
47
  # @param required [Boolean, :nullable] if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`.
43
48
  # @param description [String]
44
49
  # @param default_value [Object]
50
+ # @param loads [Class, Array<Class>] A GraphQL type to load for the given ID when one is present
45
51
  # @param as [Symbol] Override the keyword name when passed to a method
46
52
  # @param prepare [Symbol] A method to call to transform this argument's valuebefore sending it to field resolution
47
53
  # @param camelize [Boolean] if true, the name will be camelized when building the schema
@@ -50,6 +56,8 @@ module GraphQL
50
56
  # @param deprecation_reason [String]
51
57
  # @param validates [Hash, nil] Options for building validators, if any should be applied
52
58
  # @param replace_null_with_default [Boolean] if `true`, incoming values of `null` will be replaced with the configured `default_value`
59
+ # @param comment [String] Private, used by GraphQL-Ruby when parsing GraphQL schema files
60
+ # @param ast_node [GraphQL::Language::Nodes::InputValueDefinition] Private, used by GraphQL-Ruby when parsing schema files
53
61
  def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, comment: nil, ast_node: nil, default_value: NOT_CONFIGURED, as: nil, from_resolver: false, camelize: true, prepare: nil, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block)
54
62
  arg_name ||= name
55
63
  @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
@@ -157,6 +165,10 @@ module GraphQL
157
165
  true
158
166
  end
159
167
 
168
+ def authorizes?(_context)
169
+ self.method(:authorized?).owner != GraphQL::Schema::Argument
170
+ end
171
+
160
172
  def authorized?(obj, value, ctx)
161
173
  authorized_as_type?(obj, value, ctx, as_type: type)
162
174
  end
@@ -212,6 +224,11 @@ module GraphQL
212
224
  @statically_coercible = !requires_parent_object
213
225
  end
214
226
 
227
+ def freeze
228
+ statically_coercible?
229
+ super
230
+ end
231
+
215
232
  # Apply the {prepare} configuration to `value`, using methods from `obj`.
216
233
  # Used by the runtime.
217
234
  # @api private
@@ -187,6 +187,10 @@ module GraphQL
187
187
 
188
188
  object_types.each do |t|
189
189
  t.interfaces.each do |int_t|
190
+ if int_t.is_a?(LateBoundType)
191
+ int_t = types[int_t.graphql_name]
192
+ t.implements(int_t)
193
+ end
190
194
  int_t.orphan_types(t)
191
195
  end
192
196
  end
@@ -262,6 +266,8 @@ module GraphQL
262
266
  build_scalar_type(definition, type_resolver, base_types[:scalar], default_resolve: default_resolve)
263
267
  when GraphQL::Language::Nodes::InputObjectTypeDefinition
264
268
  build_input_object_type(definition, type_resolver, base_types[:input_object])
269
+ when GraphQL::Language::Nodes::DirectiveDefinition
270
+ build_directive(definition, type_resolver)
265
271
  end
266
272
  end
267
273
 
@@ -506,6 +512,7 @@ module GraphQL
506
512
  camelize: false,
507
513
  directives: prepare_directives(field_definition, type_resolver),
508
514
  resolver_method: resolve_method_name,
515
+ resolve_batch: resolve_method_name,
509
516
  )
510
517
 
511
518
  builder.build_arguments(schema_field_defn, field_definition.arguments, type_resolver)
@@ -519,9 +526,15 @@ module GraphQL
519
526
 
520
527
  def define_field_resolve_method(owner, method_name, field_name)
521
528
  owner.define_method(method_name) { |**args|
522
- field_instance = self.class.get_field(field_name)
529
+ field_instance = context.types.field(owner, field_name)
523
530
  context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context)
524
531
  }
532
+ owner.define_singleton_method(method_name) { |objects, context, **args|
533
+ field_instance = context.types.field(owner, field_name)
534
+ objects.map do |object|
535
+ context.schema.definition_default_resolve.call(self, field_instance, object, args, context)
536
+ end
537
+ }
525
538
  end
526
539
 
527
540
  def build_resolve_type(lookup_hash, directives, missing_type_handler)
@@ -540,7 +553,7 @@ module GraphQL
540
553
  when GraphQL::Language::Nodes::ListType
541
554
  resolve_type_proc.call(ast_node.of_type).to_list_type
542
555
  when String
543
- directives[ast_node]
556
+ directives[ast_node] ||= missing_type_handler.call(ast_node)
544
557
  else
545
558
  raise "Unexpected ast_node: #{ast_node.inspect}"
546
559
  end
@@ -31,17 +31,25 @@ module GraphQL
31
31
 
32
32
  def locations(*new_locations)
33
33
  if !new_locations.empty?
34
+ is_runtime = false
34
35
  new_locations.each do |new_loc|
35
- if !LOCATIONS.include?(new_loc.to_sym)
36
+ loc_sym = new_loc.to_sym
37
+ if !LOCATIONS.include?(loc_sym)
36
38
  raise ArgumentError, "#{self} (#{self.graphql_name}) has an invalid directive location: `locations #{new_loc}` "
37
39
  end
40
+ is_runtime ||= RUNTIME_LOCATIONS.include?(loc_sym)
38
41
  end
39
42
  @locations = new_locations
43
+ @is_runtime = is_runtime
40
44
  else
41
45
  @locations ||= (superclass.respond_to?(:locations) ? superclass.locations : [])
42
46
  end
43
47
  end
44
48
 
49
+ def runtime?
50
+ @is_runtime
51
+ end
52
+
45
53
  def default_directive(new_default_directive = nil)
46
54
  if new_default_directive != nil
47
55
  @default_directive = new_default_directive
@@ -104,8 +112,12 @@ module GraphQL
104
112
 
105
113
  def inherited(subclass)
106
114
  super
115
+ parent_class = self
107
116
  subclass.class_exec do
108
117
  @default_graphql_name ||= nil
118
+ @locations = parent_class.locations
119
+ @is_runtime = parent_class.runtime?
120
+ @repeatable = false
109
121
  end
110
122
  end
111
123
  end
@@ -129,11 +141,29 @@ module GraphQL
129
141
  # not runtime arguments.
130
142
  context = Query::NullContext.instance
131
143
  self.class.all_argument_definitions.each do |arg_defn|
132
- if arguments.key?(arg_defn.keyword)
133
- value = arguments[arg_defn.keyword]
144
+ keyword = arg_defn.keyword
145
+ arg_type = arg_defn.type
146
+ if arguments.key?(keyword)
147
+ value = arguments[keyword]
134
148
  # This is a Ruby-land value; convert it to graphql for validation
135
149
  graphql_value = begin
136
- arg_defn.type.unwrap.coerce_isolated_result(value)
150
+ coerce_value = value
151
+ if arg_type.list? && (!coerce_value.nil?) && (!coerce_value.is_a?(Array))
152
+ # When validating inputs, GraphQL accepts a single item
153
+ # and implicitly converts it to a one-item list.
154
+ # However, we're using result coercion here to go from Ruby value
155
+ # to GraphQL value, so it doesn't have that feature.
156
+ # Keep the GraphQL-type behavior but implement it manually:
157
+ wrap_type = arg_type
158
+ while wrap_type.list?
159
+ if wrap_type.non_null?
160
+ wrap_type = wrap_type.of_type
161
+ end
162
+ wrap_type = wrap_type.of_type
163
+ coerce_value = [coerce_value]
164
+ end
165
+ end
166
+ arg_type.coerce_isolated_result(coerce_value)
137
167
  rescue GraphQL::Schema::Enum::UnresolvedValueError
138
168
  # Let validation handle this
139
169
  value
@@ -142,7 +172,7 @@ module GraphQL
142
172
  value = graphql_value = nil
143
173
  end
144
174
 
145
- result = arg_defn.type.validate_input(graphql_value, context)
175
+ result = arg_type.validate_input(graphql_value, context)
146
176
  if !result.valid?
147
177
  raise InvalidArgumentError, "@#{graphql_name}.#{arg_defn.graphql_name} on #{owner.path} is invalid (#{value.inspect}): #{result.problems.first["explanation"]}"
148
178
  end
@@ -159,13 +189,16 @@ module GraphQL
159
189
  end
160
190
 
161
191
  LOCATIONS = [
162
- QUERY = :QUERY,
163
- MUTATION = :MUTATION,
164
- SUBSCRIPTION = :SUBSCRIPTION,
165
- FIELD = :FIELD,
166
- FRAGMENT_DEFINITION = :FRAGMENT_DEFINITION,
167
- FRAGMENT_SPREAD = :FRAGMENT_SPREAD,
168
- INLINE_FRAGMENT = :INLINE_FRAGMENT,
192
+ *(RUNTIME_LOCATIONS = [
193
+ QUERY = :QUERY,
194
+ MUTATION = :MUTATION,
195
+ SUBSCRIPTION = :SUBSCRIPTION,
196
+ FIELD = :FIELD,
197
+ FRAGMENT_DEFINITION = :FRAGMENT_DEFINITION,
198
+ FRAGMENT_SPREAD = :FRAGMENT_SPREAD,
199
+ INLINE_FRAGMENT = :INLINE_FRAGMENT,
200
+ VARIABLE_DEFINITION = :VARIABLE_DEFINITION,
201
+ ]),
169
202
  SCHEMA = :SCHEMA,
170
203
  SCALAR = :SCALAR,
171
204
  OBJECT = :OBJECT,
@@ -177,7 +210,6 @@ module GraphQL
177
210
  ENUM_VALUE = :ENUM_VALUE,
178
211
  INPUT_OBJECT = :INPUT_OBJECT,
179
212
  INPUT_FIELD_DEFINITION = :INPUT_FIELD_DEFINITION,
180
- VARIABLE_DEFINITION = :VARIABLE_DEFINITION,
181
213
  ]
182
214
 
183
215
  DEFAULT_DEPRECATION_REASON = 'No longer supported'
@@ -12,52 +12,19 @@ module GraphQL
12
12
  end
13
13
 
14
14
  # Remove pagination args before passing it to a user method
15
- def resolve(object:, arguments:, context:)
15
+ def resolve(object: nil, objects: nil, arguments:, context:)
16
16
  next_args = arguments.dup
17
17
  next_args.delete(:first)
18
18
  next_args.delete(:last)
19
19
  next_args.delete(:before)
20
20
  next_args.delete(:after)
21
- yield(object, next_args, arguments)
21
+ yield(object || objects, next_args, arguments)
22
22
  end
23
23
 
24
24
  def after_resolve(value:, object:, arguments:, context:, memo:)
25
25
  original_arguments = memo
26
- # rename some inputs to avoid conflicts inside the block
27
- maybe_lazy = value
28
- value = nil
29
- context.query.after_lazy(maybe_lazy) do |resolved_value|
30
- value = resolved_value
31
- if value.is_a? GraphQL::ExecutionError
32
- # This isn't even going to work because context doesn't have ast_node anymore
33
- context.add_error(value)
34
- nil
35
- elsif value.nil?
36
- nil
37
- elsif value.is_a?(GraphQL::Pagination::Connection)
38
- # update the connection with some things that may not have been provided
39
- value.context ||= context
40
- value.parent ||= object.object
41
- value.first_value ||= original_arguments[:first]
42
- value.after_value ||= original_arguments[:after]
43
- value.last_value ||= original_arguments[:last]
44
- value.before_value ||= original_arguments[:before]
45
- value.arguments ||= original_arguments # rubocop:disable Development/ContextIsPassedCop -- unrelated .arguments method
46
- value.field ||= field
47
- if field.has_max_page_size? && !value.has_max_page_size_override?
48
- value.max_page_size = field.max_page_size
49
- end
50
- if field.has_default_page_size? && !value.has_default_page_size_override?
51
- value.default_page_size = field.default_page_size
52
- end
53
- if (custom_t = context.schema.connections.edge_class_for_field(@field))
54
- value.edge_class = custom_t
55
- end
56
- value
57
- else
58
- context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers
59
- context.schema.connections.wrap(field, object.object, value, original_arguments, context)
60
- end
26
+ context.query.after_lazy(value) do |resolved_value|
27
+ context.schema.connections.populate_connection(field, object.object, resolved_value, original_arguments, context)
61
28
  end
62
29
  end
63
30
  end
@@ -5,22 +5,27 @@ module GraphQL
5
5
  class Field
6
6
  class ScopeExtension < GraphQL::Schema::FieldExtension
7
7
  def after_resolve(object:, arguments:, context:, value:, memo:)
8
- if value.nil?
9
- value
10
- else
11
- ret_type = @field.type.unwrap
12
- if ret_type.respond_to?(:scope_items)
13
- scoped_items = ret_type.scope_items(value, context)
14
- if !scoped_items.equal?(value) && !ret_type.reauthorize_scoped_objects
15
- if (current_runtime_state = Fiber[:__graphql_runtime_info]) &&
16
- (query_runtime_state = current_runtime_state[context.query])
17
- query_runtime_state.was_authorized_by_scope_items = true
8
+ if object.is_a?(GraphQL::Schema::Object)
9
+ if value.nil?
10
+ value
11
+ else
12
+ return_type = field.type.unwrap
13
+ if return_type.respond_to?(:scope_items)
14
+ scoped_items = return_type.scope_items(value, context)
15
+ if !scoped_items.equal?(value) && !return_type.reauthorize_scoped_objects
16
+ if (current_runtime_state = Fiber[:__graphql_runtime_info]) &&
17
+ (query_runtime_state = current_runtime_state[context.query])
18
+ query_runtime_state.was_authorized_by_scope_items = true
19
+ end
18
20
  end
21
+ scoped_items
22
+ else
23
+ value
19
24
  end
20
- scoped_items
21
- else
22
- value
23
25
  end
26
+ else
27
+ # TODO skip this entirely?
28
+ value
24
29
  end
25
30
  end
26
31
  end
@@ -8,6 +8,7 @@ module GraphQL
8
8
  include GraphQL::Schema::Member::HasArguments
9
9
  include GraphQL::Schema::Member::HasArguments::FieldConfigured
10
10
  include GraphQL::Schema::Member::HasAstNode
11
+ include GraphQL::Schema::Member::HasAuthorization
11
12
  include GraphQL::Schema::Member::HasPath
12
13
  include GraphQL::Schema::Member::HasValidators
13
14
  extend GraphQL::Schema::FindInheritedValue
@@ -109,52 +110,6 @@ module GraphQL
109
110
  end
110
111
  attr_writer :subscription_scope
111
112
 
112
- # Create a field instance from a list of arguments, keyword arguments, and a block.
113
- #
114
- # This method implements prioritization between the `resolver` or `mutation` defaults
115
- # and the local overrides via other keywords.
116
- #
117
- # It also normalizes positional arguments into keywords for {Schema::Field#initialize}.
118
- # @param resolver [Class] A {GraphQL::Schema::Resolver} class to use for field configuration
119
- # @param mutation [Class] A {GraphQL::Schema::Mutation} class to use for field configuration
120
- # @param subscription [Class] A {GraphQL::Schema::Subscription} class to use for field configuration
121
- # @return [GraphQL::Schema:Field] an instance of `self`
122
- # @see {.initialize} for other options
123
- def self.from_options(name = nil, type = nil, desc = nil, comment: nil, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block)
124
- if (resolver_class = resolver || mutation || subscription)
125
- # Add a reference to that parent class
126
- kwargs[:resolver_class] = resolver_class
127
- end
128
-
129
- if name
130
- kwargs[:name] = name
131
- end
132
-
133
- if comment
134
- kwargs[:comment] = comment
135
- end
136
-
137
- if !type.nil?
138
- if desc
139
- if kwargs[:description]
140
- raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc.inspect}, #{kwargs[:description].inspect})"
141
- end
142
-
143
- kwargs[:description] = desc
144
- kwargs[:type] = type
145
- elsif (resolver || mutation) && type.is_a?(String)
146
- # The return type should be copied from the resolver, and the second positional argument is the description
147
- kwargs[:description] = type
148
- else
149
- kwargs[:type] = type
150
- end
151
- if type.is_a?(Class) && type < GraphQL::Schema::Mutation
152
- raise ArgumentError, "Use `field #{name.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
153
- end
154
- end
155
- new(**kwargs, &block)
156
- end
157
-
158
113
  # Can be set with `connection: true|false` or inferred from a type name ending in `*Connection`
159
114
  # @return [Boolean] if true, this field will be wrapped with Relay connection behavior
160
115
  def connection?
@@ -238,6 +193,11 @@ module GraphQL
238
193
  # @param resolver_method [Symbol] The method on the type to call to resolve this field (defaults to `name`)
239
194
  # @param connection [Boolean] `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
240
195
  # @param connection_extension [Class] The extension to add, to implement connections. If `nil`, no extension is added.
196
+ # @param resolve_static [Symbol, true, nil] 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`
197
+ # @param resolve_batch [Symbol, true, nil] Used by {Schema.execute_next} map `objects` to a same-sized Array of results. Called on the owner type class with `objects, context, **arguments`.
198
+ # @param resolve_each [Symbol, true, nil] Used by {Schema.execute_next} to get a value value for each item. Called on the owner type class with `object, context, **arguments`.
199
+ # @param resolve_legacy_instance_method [Symbol, true, nil] Used by {Schema.execute_next} to get a value value for each item. Calls an instance method on the object type class.
200
+ # @param dataload [Class, Hash] Shorthand for making dataloader calls
241
201
  # @param max_page_size [Integer, nil] For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
242
202
  # @param default_page_size [Integer, nil] For connections, the default number of items to return from this field, or `nil` to return unlimited results.
243
203
  # @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__`
@@ -255,7 +215,12 @@ module GraphQL
255
215
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
256
216
  # @param validates [Array<Hash>] Configurations for validating this field
257
217
  # @param fallback_value [Object] A fallback value if the method is not defined
258
- 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)
218
+ # @param dynamic_introspection [Boolean] (Private, used by GraphQL-Ruby)
219
+ # @param relay_node_field [Boolean] (Private, used by GraphQL-Ruby)
220
+ # @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby)
221
+ # @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
222
+ # @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}.
223
+ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, resolve_legacy_instance_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, dataload: 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)
259
224
  if name.nil?
260
225
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
261
226
  end
@@ -303,6 +268,36 @@ module GraphQL
303
268
  @method_str = -method_name.to_s
304
269
  @method_sym = method_name.to_sym
305
270
  @resolver_method = (resolver_method || name_s).to_sym
271
+
272
+ if resolve_static
273
+ @execution_mode = :resolve_static
274
+ @execution_mode_key = resolve_static == true ? @method_sym : resolve_static
275
+ elsif resolve_batch
276
+ @execution_mode = :resolve_batch
277
+ @execution_mode_key = resolve_batch == true ? @method_sym : resolve_batch
278
+ elsif resolve_each
279
+ @execution_mode = :resolve_each
280
+ @execution_mode_key = resolve_each == true ? @method_sym : resolve_each
281
+ elsif hash_key
282
+ @execution_mode = :hash_key
283
+ @execution_mode_key = hash_key
284
+ elsif dig
285
+ @execution_mode = :dig
286
+ @execution_mode_key = dig
287
+ elsif resolver_class
288
+ @execution_mode = :resolver_class
289
+ @execution_mode_key = resolver_class
290
+ elsif resolve_legacy_instance_method
291
+ @execution_mode = :resolve_legacy_instance_method
292
+ @execution_mode_key = resolve_legacy_instance_method == true ? @method_sym : resolve_legacy_instance_method
293
+ elsif dataload
294
+ @execution_mode = :dataload
295
+ @execution_mode_key = dataload
296
+ else
297
+ @execution_mode = :direct_send
298
+ @execution_mode_key = @method_sym
299
+ end
300
+
306
301
  @complexity = complexity
307
302
  @dynamic_introspection = dynamic_introspection
308
303
  @return_type_expr = type
@@ -347,7 +342,7 @@ module GraphQL
347
342
 
348
343
  @extensions = EMPTY_ARRAY
349
344
  @call_after_define = false
350
- set_pagination_extensions(connection_extension: connection_extension)
345
+ set_pagination_extensions(connection_extension: NOT_CONFIGURED.equal?(connection_extension) ? self.class.connection_extension : connection_extension)
351
346
  # Do this last so we have as much context as possible when initializing them:
352
347
  if !extensions.empty?
353
348
  self.extensions(extensions)
@@ -373,6 +368,9 @@ module GraphQL
373
368
  end
374
369
  end
375
370
 
371
+ # @api private
372
+ attr_reader :execution_mode_key, :execution_mode
373
+
376
374
  # Calls the definition block, if one was given.
377
375
  # This is deferred so that references to the return type
378
376
  # can be lazily evaluated, reducing Rails boot time.
@@ -616,6 +614,14 @@ module GraphQL
616
614
  end
617
615
  end
618
616
 
617
+ def freeze
618
+ type
619
+ owner_type
620
+ arguments_statically_coercible?
621
+ connection?
622
+ super
623
+ end
624
+
619
625
  class MissingReturnTypeError < GraphQL::Error; end
620
626
  attr_writer :type
621
627
 
@@ -658,6 +664,12 @@ module GraphQL
658
664
  end
659
665
  end
660
666
 
667
+ def authorizes?(context)
668
+ method(:authorized?).owner != GraphQL::Schema::Field ||
669
+ ((args = context.types.arguments(self)) && (args.any? { |a| a.authorizes?(context) })) ||
670
+ (@resolver_class&.authorizes?(context)) || false
671
+ end
672
+
661
673
  def authorized?(object, args, context)
662
674
  if @resolver_class
663
675
  # The resolver _instance_ will check itself during `resolve()`
@@ -912,6 +924,33 @@ ERR
912
924
  end
913
925
  end
914
926
 
927
+ public
928
+
929
+ def run_next_extensions_before_resolve(objs, args, ctx, extended, idx: 0, &block)
930
+ extension = @extensions[idx]
931
+ if extension
932
+ extension.resolve(objects: objs, arguments: args, context: ctx) do |extended_objs, extended_args, memo|
933
+ if memo
934
+ memos = extended.memos ||= {}
935
+ memos[idx] = memo
936
+ end
937
+
938
+ if (extras = extension.added_extras)
939
+ ae = extended.added_extras ||= []
940
+ ae.concat(extras)
941
+ end
942
+
943
+ extended.object = extended_objs
944
+ extended.arguments = extended_args
945
+ run_next_extensions_before_resolve(extended_objs, extended_args, ctx, extended, idx: idx + 1, &block)
946
+ end
947
+ else
948
+ yield(objs, args)
949
+ end
950
+ end
951
+
952
+ private
953
+
915
954
  def run_extensions_before_resolve(obj, args, ctx, extended, idx: 0)
916
955
  extension = @extensions[idx]
917
956
  if extension
@@ -123,15 +123,16 @@ module GraphQL
123
123
  #
124
124
  # Whatever this method returns will be used for execution.
125
125
  #
126
- # @param object [Object] The object the field is being resolved on
126
+ # @param object [Object] The object the field is being resolved on (not passed by new execution)
127
+ # @param objects [Array<Object>] The objects the field is being resolved on (passed by new execution)
127
128
  # @param arguments [Hash] Ruby keyword arguments for resolving this field
128
129
  # @param context [Query::Context] the context for this query
129
- # @yieldparam object [Object] The object to continue resolving the field on
130
+ # @yieldparam object_or_objects [Object, Array<Object>] The object or objects (new execution) to continue resolving the field on
130
131
  # @yieldparam arguments [Hash] The keyword arguments to continue resolving with
131
132
  # @yieldparam memo [Object] Any extension-specific value which will be passed to {#after_resolve} later
132
133
  # @return [Object] The return value for this field.
133
- def resolve(object:, arguments:, context:)
134
- yield(object, arguments, nil)
134
+ def resolve(object: nil, objects: nil, arguments:, context:)
135
+ yield(object.nil? ? objects : object, arguments, nil)
135
136
  end
136
137
 
137
138
  # Called after {#field} was resolved, and after any lazy values (like `Promise`s) were synced,
@@ -139,14 +140,16 @@ module GraphQL
139
140
  #
140
141
  # Whatever this hook returns will be used as the return value.
141
142
  #
142
- # @param object [Object] The object the field is being resolved on
143
+ # @param object [Object] The object the field is being resolved on (not passed by new execution)
144
+ # @param objects [Array<Object>] The object the field is being resolved on (passed by new execution)
143
145
  # @param arguments [Hash] Ruby keyword arguments for resolving this field
144
146
  # @param context [Query::Context] the context for this query
145
- # @param value [Object] Whatever the field previously returned
147
+ # @param value [Object] Whatever the field previously returned (not passed by new execution)
148
+ # @param values [Array<Object>] Whatever the field previously returned (passed by new execution)
146
149
  # @param memo [Object] The third value yielded by {#resolve}, or `nil` if there wasn't one
147
150
  # @return [Object] The return value for this field.
148
- def after_resolve(object:, arguments:, context:, value:, memo:)
149
- value
151
+ def after_resolve(object: nil, objects: nil, arguments:, context:, values: nil, value: nil, memo:)
152
+ value.nil? ? values : value
150
153
  end
151
154
  end
152
155
  end
@@ -33,6 +33,26 @@ module GraphQL
33
33
  self::DefinitionMethods.module_exec(&block)
34
34
  end
35
35
 
36
+ # Instance methods defined in this block will become class methods on objects that implement this interface.
37
+ # Use it to implement `resolve_each:`, `resolve_batch:`, and `resolve_static:` fields.
38
+ # @example
39
+ # field :thing, String, resolve_static: true
40
+ #
41
+ # resolver_methods do
42
+ # def thing
43
+ # Somehow.get.thing
44
+ # end
45
+ # end
46
+ def resolver_methods(&block)
47
+ if !defined?(@_resolver_methods)
48
+ resolver_methods_module = Module.new
49
+ @_resolver_methods = resolver_methods_module
50
+ const_set(:ResolverMethods, resolver_methods_module)
51
+ extend(self::ResolverMethods)
52
+ end
53
+ self::ResolverMethods.module_exec(&block)
54
+ end
55
+
36
56
  # @see {Schema::Warden} hides interfaces without visible implementations
37
57
  def visible?(context)
38
58
  true
@@ -79,6 +99,12 @@ module GraphQL
79
99
  if !backtrace_line
80
100
  raise "Attach interfaces using `implements(#{self})`, not `include(#{self})`"
81
101
  end
102
+
103
+ child_class.ancestors.reverse_each do |ancestor|
104
+ if ancestor.const_defined?(:ResolverMethods)
105
+ child_class.extend(ancestor::ResolverMethods)
106
+ end
107
+ end
82
108
  end
83
109
 
84
110
  super
@@ -19,7 +19,11 @@ module GraphQL
19
19
  end
20
20
 
21
21
  def to_type_signature
22
- "[#{@of_type.to_type_signature}]"
22
+ @type_signature ||= -"[#{@of_type.to_type_signature}]"
23
+ end
24
+
25
+ def authorizes?(ctx)
26
+ of_type.authorizes?(ctx)
23
27
  end
24
28
 
25
29
  # This is for introspection, where it's expected the name will be `null`
@@ -26,16 +26,6 @@ module GraphQL
26
26
  end
27
27
  end
28
28
 
29
- # Just a convenience method to point out that people should use graphql_name instead
30
- def name(new_name = nil)
31
- return super() if new_name.nil?
32
-
33
- fail(
34
- "The new name override method is `graphql_name`, not `name`. Usage: "\
35
- "graphql_name \"#{new_name}\""
36
- )
37
- end
38
-
39
29
  # Call this method to provide a new description; OR
40
30
  # call it without an argument to get the description
41
31
  # @param new_description [String]
@@ -130,7 +120,7 @@ module GraphQL
130
120
  true
131
121
  end
132
122
 
133
- def default_relay
123
+ def default_relay?
134
124
  false
135
125
  end
136
126