graphql 2.5.11 → 2.5.23

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 (105) 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/async_dataloader.rb +22 -11
  18. data/lib/graphql/dataloader/null_dataloader.rb +48 -10
  19. data/lib/graphql/dataloader.rb +75 -23
  20. data/lib/graphql/date_encoding_error.rb +1 -1
  21. data/lib/graphql/execution/interpreter/resolve.rb +7 -13
  22. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  23. data/lib/graphql/execution/interpreter/runtime.rb +24 -18
  24. data/lib/graphql/execution/interpreter.rb +8 -22
  25. data/lib/graphql/execution/lazy.rb +1 -1
  26. data/lib/graphql/execution/multiplex.rb +1 -1
  27. data/lib/graphql/execution/next/field_resolve_step.rb +743 -0
  28. data/lib/graphql/execution/next/load_argument_step.rb +64 -0
  29. data/lib/graphql/execution/next/prepare_object_step.rb +129 -0
  30. data/lib/graphql/execution/next/runner.rb +411 -0
  31. data/lib/graphql/execution/next/selections_step.rb +37 -0
  32. data/lib/graphql/execution/next.rb +72 -0
  33. data/lib/graphql/execution.rb +8 -4
  34. data/lib/graphql/execution_error.rb +17 -10
  35. data/lib/graphql/introspection/directive_type.rb +7 -3
  36. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  37. data/lib/graphql/introspection/entry_points.rb +11 -3
  38. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  39. data/lib/graphql/introspection/field_type.rb +13 -5
  40. data/lib/graphql/introspection/input_value_type.rb +21 -13
  41. data/lib/graphql/introspection/type_type.rb +64 -28
  42. data/lib/graphql/invalid_null_error.rb +11 -5
  43. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  44. data/lib/graphql/language.rb +21 -12
  45. data/lib/graphql/pagination/connection.rb +2 -0
  46. data/lib/graphql/pagination/connections.rb +32 -0
  47. data/lib/graphql/query/context.rb +4 -3
  48. data/lib/graphql/query/null_context.rb +9 -3
  49. data/lib/graphql/schema/argument.rb +12 -0
  50. data/lib/graphql/schema/build_from_definition.rb +10 -1
  51. data/lib/graphql/schema/directive.rb +22 -4
  52. data/lib/graphql/schema/field/connection_extension.rb +15 -35
  53. data/lib/graphql/schema/field/scope_extension.rb +22 -13
  54. data/lib/graphql/schema/field.rb +79 -48
  55. data/lib/graphql/schema/field_extension.rb +33 -0
  56. data/lib/graphql/schema/list.rb +1 -1
  57. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -1
  58. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  59. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  60. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  61. data/lib/graphql/schema/member/has_fields.rb +86 -5
  62. data/lib/graphql/schema/member.rb +5 -0
  63. data/lib/graphql/schema/non_null.rb +1 -1
  64. data/lib/graphql/schema/object.rb +1 -0
  65. data/lib/graphql/schema/resolver.rb +60 -1
  66. data/lib/graphql/schema/subscription.rb +0 -2
  67. data/lib/graphql/schema/validator/required_validator.rb +33 -2
  68. data/lib/graphql/schema/visibility/profile.rb +68 -49
  69. data/lib/graphql/schema/visibility.rb +3 -3
  70. data/lib/graphql/schema/wrapper.rb +7 -1
  71. data/lib/graphql/schema.rb +53 -10
  72. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  73. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  74. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  75. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  76. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  77. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  78. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  79. data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
  80. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  81. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  82. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  83. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  84. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  85. data/lib/graphql/static_validation/validation_context.rb +1 -1
  86. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  87. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +25 -1
  88. data/lib/graphql/subscriptions/event.rb +1 -0
  89. data/lib/graphql/subscriptions.rb +21 -1
  90. data/lib/graphql/testing/helpers.rb +12 -9
  91. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  92. data/lib/graphql/testing.rb +1 -0
  93. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  94. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  95. data/lib/graphql/tracing/perfetto_trace.rb +209 -79
  96. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  97. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  98. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  99. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  100. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  101. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  102. data/lib/graphql/unauthorized_error.rb +9 -1
  103. data/lib/graphql/version.rb +1 -1
  104. data/lib/graphql.rb +8 -2
  105. metadata +17 -3
@@ -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
@@ -111,15 +112,15 @@ module GraphQL
111
112
  # Return this value to tell the runtime
112
113
  # to exclude this field from the response altogether
113
114
  def skip
114
- GraphQL::Execution::SKIP
115
+ GraphQL::Execution::Skip.new
115
116
  end
116
117
 
117
118
  # Add error at query-level.
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
@@ -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
@@ -266,6 +266,8 @@ module GraphQL
266
266
  build_scalar_type(definition, type_resolver, base_types[:scalar], default_resolve: default_resolve)
267
267
  when GraphQL::Language::Nodes::InputObjectTypeDefinition
268
268
  build_input_object_type(definition, type_resolver, base_types[:input_object])
269
+ when GraphQL::Language::Nodes::DirectiveDefinition
270
+ build_directive(definition, type_resolver)
269
271
  end
270
272
  end
271
273
 
@@ -510,6 +512,7 @@ module GraphQL
510
512
  camelize: false,
511
513
  directives: prepare_directives(field_definition, type_resolver),
512
514
  resolver_method: resolve_method_name,
515
+ resolve_batch: resolve_method_name,
513
516
  )
514
517
 
515
518
  builder.build_arguments(schema_field_defn, field_definition.arguments, type_resolver)
@@ -526,6 +529,12 @@ module GraphQL
526
529
  field_instance = context.types.field(owner, field_name)
527
530
  context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context)
528
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
+ }
529
538
  end
530
539
 
531
540
  def build_resolve_type(lookup_hash, directives, missing_type_handler)
@@ -544,7 +553,7 @@ module GraphQL
544
553
  when GraphQL::Language::Nodes::ListType
545
554
  resolve_type_proc.call(ast_node.of_type).to_list_type
546
555
  when String
547
- directives[ast_node]
556
+ directives[ast_node] ||= missing_type_handler.call(ast_node)
548
557
  else
549
558
  raise "Unexpected ast_node: #{ast_node.inspect}"
550
559
  end
@@ -129,11 +129,29 @@ module GraphQL
129
129
  # not runtime arguments.
130
130
  context = Query::NullContext.instance
131
131
  self.class.all_argument_definitions.each do |arg_defn|
132
- if arguments.key?(arg_defn.keyword)
133
- value = arguments[arg_defn.keyword]
132
+ keyword = arg_defn.keyword
133
+ arg_type = arg_defn.type
134
+ if arguments.key?(keyword)
135
+ value = arguments[keyword]
134
136
  # This is a Ruby-land value; convert it to graphql for validation
135
137
  graphql_value = begin
136
- arg_defn.type.unwrap.coerce_isolated_result(value)
138
+ coerce_value = value
139
+ if arg_type.list? && (!coerce_value.nil?) && (!coerce_value.is_a?(Array))
140
+ # When validating inputs, GraphQL accepts a single item
141
+ # and implicitly converts it to a one-item list.
142
+ # However, we're using result coercion here to go from Ruby value
143
+ # to GraphQL value, so it doesn't have that feature.
144
+ # Keep the GraphQL-type behavior but implement it manually:
145
+ wrap_type = arg_type
146
+ while wrap_type.list?
147
+ if wrap_type.non_null?
148
+ wrap_type = wrap_type.of_type
149
+ end
150
+ wrap_type = wrap_type.of_type
151
+ coerce_value = [coerce_value]
152
+ end
153
+ end
154
+ arg_type.coerce_isolated_result(coerce_value)
137
155
  rescue GraphQL::Schema::Enum::UnresolvedValueError
138
156
  # Let validation handle this
139
157
  value
@@ -142,7 +160,7 @@ module GraphQL
142
160
  value = graphql_value = nil
143
161
  end
144
162
 
145
- result = arg_defn.type.validate_input(graphql_value, context)
163
+ result = arg_type.validate_input(graphql_value, context)
146
164
  if !result.valid?
147
165
  raise InvalidArgumentError, "@#{graphql_name}.#{arg_defn.graphql_name} on #{owner.path} is invalid (#{value.inspect}): #{result.problems.first["explanation"]}"
148
166
  end
@@ -21,45 +21,25 @@ module GraphQL
21
21
  yield(object, next_args, arguments)
22
22
  end
23
23
 
24
+ def resolve_next(objects:, arguments:, context:)
25
+ next_args = arguments.dup
26
+ next_args.delete(:first)
27
+ next_args.delete(:last)
28
+ next_args.delete(:before)
29
+ next_args.delete(:after)
30
+ yield(objects, next_args, arguments)
31
+ end
32
+
24
33
  def after_resolve(value:, object:, arguments:, context:, memo:)
25
34
  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
35
+ context.query.after_lazy(value) do |resolved_value|
36
+ context.schema.connections.populate_connection(field, object.object, resolved_value, original_arguments, context)
61
37
  end
62
38
  end
39
+
40
+ def after_resolve_next(**kwargs)
41
+ raise "This should never be called -- it's hardcoded in execution instead."
42
+ end
63
43
  end
64
44
  end
65
45
  end
@@ -5,24 +5,33 @@ 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
31
+
32
+ def after_resolve_next(**kwargs)
33
+ raise "This should never be called -- it's hardcoded in execution instead."
34
+ end
26
35
  end
27
36
  end
28
37
  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_next_mode = :resolve_static
274
+ @execution_next_mode_key = resolve_static == true ? @method_sym : resolve_static
275
+ elsif resolve_batch
276
+ @execution_next_mode = :resolve_batch
277
+ @execution_next_mode_key = resolve_batch == true ? @method_sym : resolve_batch
278
+ elsif resolve_each
279
+ @execution_next_mode = :resolve_each
280
+ @execution_next_mode_key = resolve_each == true ? @method_sym : resolve_each
281
+ elsif hash_key
282
+ @execution_next_mode = :hash_key
283
+ @execution_next_mode_key = hash_key
284
+ elsif dig
285
+ @execution_next_mode = :dig
286
+ @execution_next_mode_key = dig
287
+ elsif resolver_class
288
+ @execution_next_mode = :resolver_class
289
+ @execution_next_mode_key = resolver_class
290
+ elsif resolve_legacy_instance_method
291
+ @execution_next_mode = :resolve_legacy_instance_method
292
+ @execution_next_mode_key = resolve_legacy_instance_method == true ? @method_sym : resolve_legacy_instance_method
293
+ elsif dataload
294
+ @execution_next_mode = :dataload
295
+ @execution_next_mode_key = dataload
296
+ else
297
+ @execution_next_mode = :direct_send
298
+ @execution_next_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_next_mode_key, :execution_next_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.
@@ -666,6 +664,12 @@ module GraphQL
666
664
  end
667
665
  end
668
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
+
669
673
  def authorized?(object, args, context)
670
674
  if @resolver_class
671
675
  # The resolver _instance_ will check itself during `resolve()`
@@ -920,6 +924,33 @@ ERR
920
924
  end
921
925
  end
922
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_next(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
+
923
954
  def run_extensions_before_resolve(obj, args, ctx, extended, idx: 0)
924
955
  extension = @extensions[idx]
925
956
  if extension
@@ -134,6 +134,24 @@ module GraphQL
134
134
  yield(object, arguments, nil)
135
135
  end
136
136
 
137
+ # Called before batch-resolving {#field}. It should either:
138
+ #
139
+ # - `yield` values to continue execution; OR
140
+ # - return something else to shortcut field execution.
141
+ #
142
+ # Whatever this method returns will be used for execution.
143
+ #
144
+ # @param objects [Array<Object>] The objects the field is being resolved on
145
+ # @param arguments [Hash] Ruby keyword arguments for resolving this field
146
+ # @param context [Query::Context] the context for this query
147
+ # @yieldparam objects [Array<Object>] The objects to continue resolving the field on. Length must be the same as passed-in `objects:`
148
+ # @yieldparam arguments [Hash] The keyword arguments to continue resolving with
149
+ # @yieldparam memo [Object] Any extension-specific value which will be passed to {#after_resolve} later
150
+ # @return [Array<Object>] The return value for this field, length matching passed-in `objects:`.
151
+ def resolve_next(objects:, arguments:, context:)
152
+ yield(objects, arguments, nil)
153
+ end
154
+
137
155
  # Called after {#field} was resolved, and after any lazy values (like `Promise`s) were synced,
138
156
  # but before the value was added to the GraphQL response.
139
157
  #
@@ -148,6 +166,21 @@ module GraphQL
148
166
  def after_resolve(object:, arguments:, context:, value:, memo:)
149
167
  value
150
168
  end
169
+
170
+ # Called after {#field} was batch-resolved, and after any lazy values (like `Promise`s) were synced,
171
+ # but before the value was added to the GraphQL response.
172
+ #
173
+ # Whatever this hook returns will be used as the return value.
174
+ #
175
+ # @param objects [Array<Object>] The objects the field is being resolved on
176
+ # @param arguments [Hash] Ruby keyword arguments for resolving this field
177
+ # @param context [Query::Context] the context for this query
178
+ # @param values [Array<Object>] Whatever the field returned, one for each of `objects`
179
+ # @param memo [Object] The third value yielded by {#resolve}, or `nil` if there wasn't one
180
+ # @return [Array<Object>] The return values for this field, length matching `objects:`.
181
+ def after_resolve_next(objects:, arguments:, context:, values:, memo:)
182
+ values
183
+ end
151
184
  end
152
185
  end
153
186
  end
@@ -19,7 +19,7 @@ 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
23
  end
24
24
 
25
25
  # This is for introspection, where it's expected the name will be `null`
@@ -130,7 +130,7 @@ module GraphQL
130
130
  true
131
131
  end
132
132
 
133
- def default_relay
133
+ def default_relay?
134
134
  false
135
135
  end
136
136
 
@@ -14,29 +14,52 @@ module GraphQL
14
14
  cls.extend(ClassConfigured)
15
15
  end
16
16
 
17
- # @see {GraphQL::Schema::Argument#initialize} for parameters
18
- # @return [GraphQL::Schema::Argument] An instance of {argument_class}, created from `*args`
19
- def argument(*args, **kwargs, &block)
20
- kwargs[:owner] = self
21
- loads = kwargs[:loads]
22
- if loads
23
- name = args[0]
24
- name_as_string = name.to_s
25
-
26
- inferred_arg_name = case name_as_string
17
+ # @param arg_name [Symbol] The underscore-cased name of this argument, `name:` keyword also accepted
18
+ # @param type_expr The GraphQL type of this argument; `type:` keyword also accepted
19
+ # @param desc [String] Argument description, `description:` keyword also accepted
20
+ # @option kwargs [Boolean, :nullable] :required 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`.
21
+ # @option kwargs [String] :description Positional argument also accepted
22
+ # @option kwargs [Class, Array<Class>] :type Input type; positional argument also accepted
23
+ # @option kwargs [Symbol] :name positional argument also accepted
24
+ # @option kwargs [Object] :default_value
25
+ # @option kwargs [Class, Array<Class>] :loads A GraphQL type to load for the given ID when one is present
26
+ # @option kwargs [Symbol] :as Override the keyword name when passed to a method
27
+ # @option kwargs [Symbol] :prepare A method to call to transform this argument's valuebefore sending it to field resolution
28
+ # @option kwargs [Boolean] :camelize if true, the name will be camelized when building the schema
29
+ # @option kwargs [Boolean] :from_resolver if true, a Resolver class defined this argument
30
+ # @option kwargs [Hash{Class => Hash}] :directives
31
+ # @option kwargs [String] :deprecation_reason
32
+ # @option kwargs [String] :comment Private, used by GraphQL-Ruby when parsing GraphQL schema files
33
+ # @option kwargs [GraphQL::Language::Nodes::InputValueDefinition] :ast_node Private, used by GraphQL-Ruby when parsing schema files
34
+ # @option kwargs [Hash, nil] :validates Options for building validators, if any should be applied
35
+ # @option kwargs [Boolean] :replace_null_with_default if `true`, incoming values of `null` will be replaced with the configured `default_value`
36
+ # @param definition_block [Proc] Called with the newly-created {Argument}
37
+ # @param kwargs [Hash] Keywords for defining an argument. Any keywords not documented here must be handled by your base Argument class.
38
+ # @return [GraphQL::Schema::Argument] An instance of {argument_class} created from these arguments
39
+ def argument(arg_name = nil, type_expr = nil, desc = nil, **kwargs, &definition_block)
40
+ if kwargs[:loads]
41
+ loads_name = arg_name || kwargs[:name]
42
+ loads_name_as_string = loads_name.to_s
43
+
44
+ inferred_arg_name = case loads_name_as_string
27
45
  when /_id$/
28
- name_as_string.sub(/_id$/, "").to_sym
46
+ loads_name_as_string.sub(/_id$/, "").to_sym
29
47
  when /_ids$/
30
- name_as_string.sub(/_ids$/, "")
48
+ loads_name_as_string.sub(/_ids$/, "")
31
49
  .sub(/([^s])$/, "\\1s")
32
50
  .to_sym
33
51
  else
34
- name
52
+ loads_name
35
53
  end
36
54
 
37
55
  kwargs[:as] ||= inferred_arg_name
38
56
  end
39
- arg_defn = self.argument_class.new(*args, **kwargs, &block)
57
+ kwargs[:owner] = self
58
+ arg_defn = self.argument_class.new(
59
+ arg_name, type_expr, desc,
60
+ **kwargs,
61
+ &definition_block
62
+ )
40
63
  add_argument(arg_defn)
41
64
  arg_defn
42
65
  end
@@ -413,6 +436,12 @@ module GraphQL
413
436
  end
414
437
  end
415
438
 
439
+ # Called when an argument's `loads:` configuration fails to fetch an application object.
440
+ # By default, this method raises the given error, but you can override it to handle failures differently.
441
+ #
442
+ # @param err [GraphQL::LoadApplicationObjectFailedError] The error that occurred
443
+ # @return [Object, nil] If a value is returned, it will be used instead of the failed load
444
+ # @api public
416
445
  def load_application_object_failed(err)
417
446
  raise err
418
447
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Schema
4
+ class Member
5
+ module HasAuthorization
6
+ def self.included(child_class)
7
+ child_class.include(InstanceConfigured)
8
+ end
9
+
10
+ def self.extended(child_class)
11
+ child_class.extend(ClassConfigured)
12
+ child_class.class_exec do
13
+ @authorizes = false
14
+ end
15
+ end
16
+
17
+ def authorized?(object, context)
18
+ true
19
+ end
20
+
21
+ module InstanceConfigured
22
+ def authorizes?(context)
23
+ raise RequiredImplementationMissingError, "#{self.class} must implement #authorizes?(context)"
24
+ end
25
+ end
26
+
27
+ module ClassConfigured
28
+ def authorizes?(context)
29
+ method(:authorized?).owner != GraphQL::Schema::Member::HasAuthorization
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end