graphql 2.5.11 → 2.5.19

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 (43) 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/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  5. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  6. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  7. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  8. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  9. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  10. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  11. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  12. data/lib/graphql/dashboard.rb +5 -2
  13. data/lib/graphql/dataloader/async_dataloader.rb +22 -11
  14. data/lib/graphql/dataloader/null_dataloader.rb +44 -10
  15. data/lib/graphql/dataloader.rb +75 -23
  16. data/lib/graphql/date_encoding_error.rb +1 -1
  17. data/lib/graphql/execution/interpreter/resolve.rb +7 -13
  18. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  19. data/lib/graphql/execution/interpreter/runtime.rb +21 -16
  20. data/lib/graphql/execution/interpreter.rb +2 -13
  21. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  22. data/lib/graphql/language.rb +21 -12
  23. data/lib/graphql/schema/argument.rb +7 -0
  24. data/lib/graphql/schema/build_from_definition.rb +3 -1
  25. data/lib/graphql/schema/directive.rb +22 -4
  26. data/lib/graphql/schema/field.rb +6 -47
  27. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  28. data/lib/graphql/schema/member/has_fields.rb +76 -4
  29. data/lib/graphql/schema/validator/required_validator.rb +33 -2
  30. data/lib/graphql/schema/visibility.rb +2 -2
  31. data/lib/graphql/schema.rb +20 -3
  32. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +2 -2
  33. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  34. data/lib/graphql/testing/helpers.rb +12 -9
  35. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  36. data/lib/graphql/testing.rb +1 -0
  37. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  38. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  39. data/lib/graphql/tracing/perfetto_trace.rb +208 -78
  40. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  41. data/lib/graphql/version.rb +1 -1
  42. data/lib/graphql.rb +5 -2
  43. metadata +7 -3
@@ -35,11 +35,10 @@ module GraphQL
35
35
  # @return [GraphQL::Query::Context]
36
36
  attr_reader :context
37
37
 
38
- def initialize(query:, lazies_at_depth:)
38
+ def initialize(query:)
39
39
  @query = query
40
40
  @current_trace = query.current_trace
41
41
  @dataloader = query.multiplex.dataloader
42
- @lazies_at_depth = lazies_at_depth
43
42
  @schema = query.schema
44
43
  @context = query.context
45
44
  @response = nil
@@ -365,6 +364,10 @@ module GraphQL
365
364
  else
366
365
  @query.arguments_cache.dataload_for(ast_node, field_defn, owner_object) do |resolved_arguments|
367
366
  runtime_state = get_current_runtime_state # This might be in a different fiber
367
+ runtime_state.current_field = field_defn
368
+ runtime_state.current_arguments = resolved_arguments
369
+ runtime_state.current_result_name = result_name
370
+ runtime_state.current_result = selections_result
368
371
  evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_object, result_name, selections_result, runtime_state)
369
372
  end
370
373
  end
@@ -373,6 +376,8 @@ module GraphQL
373
376
  def evaluate_selection_with_args(arguments, field_defn, ast_node, field_ast_nodes, object, result_name, selection_result, runtime_state) # rubocop:disable Metrics/ParameterLists
374
377
  after_lazy(arguments, field: field_defn, ast_node: ast_node, owner_object: object, arguments: arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |resolved_arguments, runtime_state|
375
378
  if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
379
+ next if selection_result.collect_result(result_name, resolved_arguments)
380
+
376
381
  return_type_non_null = field_defn.type.non_null?
377
382
  continue_value(resolved_arguments, field_defn, return_type_non_null, ast_node, result_name, selection_result)
378
383
  next
@@ -446,7 +451,7 @@ module GraphQL
446
451
  }
447
452
  end
448
453
 
449
- field_result = call_method_on_directives(:resolve, object, directives) do
454
+ call_method_on_directives(:resolve, object, directives) do
450
455
  if !directives.empty?
451
456
  # This might be executed in a different context; reset this info
452
457
  runtime_state = get_current_runtime_state
@@ -472,6 +477,8 @@ module GraphQL
472
477
  end
473
478
  @current_trace.end_execute_field(field_defn, object, kwarg_arguments, query, app_result)
474
479
  after_lazy(app_result, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |inner_result, runtime_state|
480
+ next if selection_result.collect_result(result_name, inner_result)
481
+
475
482
  owner_type = selection_result.graphql_result_type
476
483
  return_type = field_defn.type
477
484
  continue_value = continue_value(inner_result, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
@@ -488,7 +495,7 @@ module GraphQL
488
495
  # all of its child fields before moving on to the next root mutation field.
489
496
  # (Subselections of this mutation will still be resolved level-by-level.)
490
497
  if selection_result.graphql_is_eager
491
- Interpreter::Resolve.resolve_all([field_result], @dataloader)
498
+ @dataloader.run
492
499
  end
493
500
  end
494
501
 
@@ -667,7 +674,11 @@ module GraphQL
667
674
  rescue GraphQL::ExecutionError => ex_err
668
675
  return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result)
669
676
  rescue StandardError => err
670
- query.handle_or_reraise(err)
677
+ begin
678
+ query.handle_or_reraise(err)
679
+ rescue GraphQL::ExecutionError => ex_err
680
+ return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result)
681
+ end
671
682
  end
672
683
  set_result(selection_result, result_name, r, false, is_non_null)
673
684
  r
@@ -879,7 +890,6 @@ module GraphQL
879
890
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
880
891
  def after_lazy(lazy_obj, field:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, runtime_state:, trace: true, &block)
881
892
  if lazy?(lazy_obj)
882
- orig_result = result
883
893
  was_authorized_by_scope_items = runtime_state.was_authorized_by_scope_items
884
894
  lazy = GraphQL::Execution::Lazy.new(field: field) do
885
895
  # This block might be called in a new fiber;
@@ -889,13 +899,13 @@ module GraphQL
889
899
  runtime_state.current_field = field
890
900
  runtime_state.current_arguments = arguments
891
901
  runtime_state.current_result_name = result_name
892
- runtime_state.current_result = orig_result
902
+ runtime_state.current_result = result
893
903
  runtime_state.was_authorized_by_scope_items = was_authorized_by_scope_items
894
904
  # Wrap the execution of _this_ method with tracing,
895
905
  # but don't wrap the continuation below
896
- result = nil
906
+ sync_result = nil
897
907
  inner_obj = begin
898
- result = if trace
908
+ sync_result = if trace
899
909
  @current_trace.begin_execute_field(field, owner_object, arguments, query)
900
910
  @current_trace.execute_field_lazy(field: field, query: query, object: owner_object, arguments: arguments, ast_node: ast_node) do
901
911
  schema.sync_lazy(lazy_obj)
@@ -913,7 +923,7 @@ module GraphQL
913
923
  end
914
924
  ensure
915
925
  if trace
916
- @current_trace.end_execute_field(field, owner_object, arguments, query, result)
926
+ @current_trace.end_execute_field(field, owner_object, arguments, query, sync_result)
917
927
  end
918
928
  end
919
929
  yield(inner_obj, runtime_state)
@@ -923,12 +933,7 @@ module GraphQL
923
933
  lazy.value
924
934
  else
925
935
  set_result(result, result_name, lazy, false, false) # is_non_null is irrelevant here
926
- current_depth = 0
927
- while result
928
- current_depth += 1
929
- result = result.graphql_parent
930
- end
931
- @lazies_at_depth[current_depth] << lazy
936
+ @dataloader.lazy_at_depth(result.depth, lazy)
932
937
  lazy
933
938
  end
934
939
  else
@@ -42,7 +42,6 @@ module GraphQL
42
42
  trace.execute_multiplex(multiplex: multiplex) do
43
43
  schema = multiplex.schema
44
44
  queries = multiplex.queries
45
- lazies_at_depth = Hash.new { |h, k| h[k] = [] }
46
45
  multiplex_analyzers = schema.multiplex_analyzers
47
46
  if multiplex.max_complexity
48
47
  multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity]
@@ -73,7 +72,7 @@ module GraphQL
73
72
  # Although queries in a multiplex _share_ an Interpreter instance,
74
73
  # they also have another item of state, which is private to that query
75
74
  # in particular, assign it here:
76
- runtime = Runtime.new(query: query, lazies_at_depth: lazies_at_depth)
75
+ runtime = Runtime.new(query: query)
77
76
  query.context.namespace(:interpreter_runtime)[:runtime] = runtime
78
77
 
79
78
  query.current_trace.execute_query(query: query) do
@@ -81,23 +80,13 @@ module GraphQL
81
80
  end
82
81
  rescue GraphQL::ExecutionError => err
83
82
  query.context.errors << err
84
- NO_OPERATION
85
83
  end
86
84
  end
87
85
  results[idx] = result
88
86
  }
89
87
  end
90
88
 
91
- multiplex.dataloader.run
92
-
93
- # Then, work through lazy results in a breadth-first way
94
- multiplex.dataloader.append_job {
95
- query = multiplex.queries.length == 1 ? multiplex.queries[0] : nil
96
- multiplex.current_trace.execute_query_lazy(multiplex: multiplex, query: query) do
97
- Interpreter::Resolve.resolve_each_depth(lazies_at_depth, multiplex.dataloader)
98
- end
99
- }
100
- multiplex.dataloader.run
89
+ multiplex.dataloader.run(trace_query_lazy: multiplex)
101
90
 
102
91
  # Then, find all errors and assign the result to the query object
103
92
  results.each_with_index do |data_result, idx|
@@ -52,8 +52,9 @@ module GraphQL
52
52
 
53
53
  def build_object_type_node(object_type)
54
54
  ints = @types.interfaces(object_type)
55
+
55
56
  if !ints.empty?
56
- ints.sort_by!(&:graphql_name)
57
+ ints = ints.sort_by(&:graphql_name)
57
58
  ints.map! { |iface| build_type_name_node(iface) }
58
59
  end
59
60
 
@@ -77,21 +77,30 @@ module GraphQL
77
77
  new_query_str || query_str
78
78
  end
79
79
 
80
+ LEADING_REGEX = Regexp.union(" ", *Lexer::Punctuation.constants.map { |const| Lexer::Punctuation.const_get(const) })
81
+
82
+ # Optimized pattern using:
83
+ # - Possessive quantifiers (*+, ++) to prevent backtracking in number patterns
84
+ # - Atomic group (?>...) for IGNORE to prevent backtracking
85
+ # - Single unified number pattern instead of three alternatives
86
+ EFFICIENT_NUMBER_REGEXP = /-?(?:0|[1-9][0-9]*+)(?:\.[0-9]++)?(?:[eE][+-]?[0-9]++)?/
87
+ EFFICIENT_IGNORE_REGEXP = /(?>[, \r\n\t]+|\#[^\n]*$)*/
88
+
89
+ MAYBE_INVALID_NUMBER = /\d[_a-zA-Z]/
90
+
80
91
  INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP = %r{
81
- (
82
- ((?<num>#{Lexer::INT_REGEXP}(#{Lexer::FLOAT_EXP_REGEXP})?)(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
83
- |
84
- ((?<num>#{Lexer::INT_REGEXP}#{Lexer::FLOAT_DECIMAL_REGEXP}#{Lexer::FLOAT_EXP_REGEXP})(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
85
- |
86
- ((?<num>#{Lexer::INT_REGEXP}#{Lexer::FLOAT_DECIMAL_REGEXP})(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
87
- )}x
92
+ (?<leading>#{LEADING_REGEX})
93
+ (?<num>#{EFFICIENT_NUMBER_REGEXP})
94
+ (?<name>#{Lexer::IDENTIFIER_REGEXP})
95
+ #{EFFICIENT_IGNORE_REGEXP}
96
+ :
97
+ }x
88
98
 
89
99
  def self.add_space_between_numbers_and_names(query_str)
90
- if query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP)
91
- query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k<num> \\k<name>:")
92
- else
93
- query_str
94
- end
100
+ # Fast check for digit followed by identifier char. If this doesn't match, skip the more expensive regexp entirely.
101
+ return query_str unless query_str.match?(MAYBE_INVALID_NUMBER)
102
+ return query_str unless query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP)
103
+ query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k<leading>\\k<num> \\k<name>:")
95
104
  end
96
105
  end
97
106
  end
@@ -39,9 +39,14 @@ module GraphQL
39
39
  # @param arg_name [Symbol]
40
40
  # @param type_expr
41
41
  # @param desc [String]
42
+ # @param type [Class, Array<Class>] Input type; positional argument also accepted
43
+ # @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
44
+ # @param definition_block [Proc] Called with the newly-created {Argument}
45
+ # @param owner [Class] Private, used by GraphQL-Ruby during schema definition
42
46
  # @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
47
  # @param description [String]
44
48
  # @param default_value [Object]
49
+ # @param loads [Class, Array<Class>] A GraphQL type to load for the given ID when one is present
45
50
  # @param as [Symbol] Override the keyword name when passed to a method
46
51
  # @param prepare [Symbol] A method to call to transform this argument's valuebefore sending it to field resolution
47
52
  # @param camelize [Boolean] if true, the name will be camelized when building the schema
@@ -50,6 +55,8 @@ module GraphQL
50
55
  # @param deprecation_reason [String]
51
56
  # @param validates [Hash, nil] Options for building validators, if any should be applied
52
57
  # @param replace_null_with_default [Boolean] if `true`, incoming values of `null` will be replaced with the configured `default_value`
58
+ # @param comment [String] Private, used by GraphQL-Ruby when parsing GraphQL schema files
59
+ # @param ast_node [GraphQL::Language::Nodes::InputValueDefinition] Private, used by GraphQL-Ruby when parsing schema files
53
60
  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
61
  arg_name ||= name
55
62
  @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
@@ -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
 
@@ -544,7 +546,7 @@ module GraphQL
544
546
  when GraphQL::Language::Nodes::ListType
545
547
  resolve_type_proc.call(ast_node.of_type).to_list_type
546
548
  when String
547
- directives[ast_node]
549
+ directives[ast_node] ||= missing_type_handler.call(ast_node)
548
550
  else
549
551
  raise "Unexpected ast_node: #{ast_node.inspect}"
550
552
  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
@@ -109,52 +109,6 @@ module GraphQL
109
109
  end
110
110
  attr_writer :subscription_scope
111
111
 
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
112
  # Can be set with `connection: true|false` or inferred from a type name ending in `*Connection`
159
113
  # @return [Boolean] if true, this field will be wrapped with Relay connection behavior
160
114
  def connection?
@@ -255,6 +209,11 @@ module GraphQL
255
209
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
256
210
  # @param validates [Array<Hash>] Configurations for validating this field
257
211
  # @param fallback_value [Object] A fallback value if the method is not defined
212
+ # @param dynamic_introspection [Boolean] (Private, used by GraphQL-Ruby)
213
+ # @param relay_node_field [Boolean] (Private, used by GraphQL-Ruby)
214
+ # @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby)
215
+ # @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
+ # @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}.
258
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)
259
218
  if name.nil?
260
219
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
@@ -347,7 +306,7 @@ module GraphQL
347
306
 
348
307
  @extensions = EMPTY_ARRAY
349
308
  @call_after_define = false
350
- set_pagination_extensions(connection_extension: connection_extension)
309
+ set_pagination_extensions(connection_extension: NOT_CONFIGURED.equal?(connection_extension) ? self.class.connection_extension : connection_extension)
351
310
  # Do this last so we have as much context as possible when initializing them:
352
311
  if !extensions.empty?
353
312
  self.extensions(extensions)
@@ -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
@@ -5,11 +5,83 @@ module GraphQL
5
5
  class Member
6
6
  # Shared code for Objects, Interfaces, Mutations, Subscriptions
7
7
  module HasFields
8
+ include EmptyObjects
8
9
  # Add a field to this object or interface with the given definition
9
- # @see {GraphQL::Schema::Field#initialize} for method signature
10
+ # @param name_positional [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); `name:` keyword is also accepted
11
+ # @param type_positional [Class, GraphQL::BaseType, Array] The return type of this field; `type:` keyword is also accepted
12
+ # @param desc_positional [String] Field description; `description:` keyword is also accepted
13
+ # @option kwargs [Symbol] :name The underscore-cased version of this field name (will be camelized for the GraphQL API); positional argument also accepted
14
+ # @option kwargs [Class, GraphQL::BaseType, Array] :type The return type of this field; positional argument is also accepted
15
+ # @option kwargs [Boolean] :null (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
16
+ # @option kwargs [String] :description Field description; positional argument also accepted
17
+ # @option kwargs [String] :comment Field comment
18
+ # @option kwargs [String] :deprecation_reason If present, the field is marked "deprecated" with this message
19
+ # @option kwargs [Symbol] :method The method to call on the underlying object to resolve this field (defaults to `name`)
20
+ # @option kwargs [String, Symbol] :hash_key The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
21
+ # @option kwargs [Array<String, Symbol>] :dig The nested hash keys to lookup on the underlying hash to resolve this field using dig
22
+ # @option kwargs [Symbol] :resolver_method The method on the type to call to resolve this field (defaults to `name`)
23
+ # @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
+ # @option kwargs [Class] :connection_extension The extension to add, to implement connections. If `nil`, no extension is added.
25
+ # @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.
26
+ # @option kwargs [Integer, nil] :default_page_size For connections, the default number of items to return from this field, or `nil` to return unlimited results.
27
+ # @option kwargs [Boolean] :introspection If true, this field will be marked as `#introspection?` and the name may begin with `__`
28
+ # @option kwargs [{String=>GraphQL::Schema::Argument, Hash}] :arguments Arguments for this field (may be added in the block, also)
29
+ # @option kwargs [Boolean] :camelize If true, the field name will be camelized when building the schema
30
+ # @option kwargs [Numeric] :complexity When provided, set the complexity for this field
31
+ # @option kwargs [Boolean] :scope If true, the return type's `.scope_items` method will be called on the return value
32
+ # @option kwargs [Symbol, String] :subscription_scope A key in `context` which will be used to scope subscription payloads
33
+ # @option kwargs [Array<Class, Hash<Class => Object>>] :extensions Named extensions to apply to this field (see also {#extension})
34
+ # @option kwargs [Hash{Class => Hash}] :directives Directives to apply to this field
35
+ # @option kwargs [Boolean] :trace If true, a {GraphQL::Tracing} tracer will measure this scalar field
36
+ # @option kwargs [Boolean] :broadcastable Whether or not this field can be distributed in subscription broadcasts
37
+ # @option kwargs [Language::Nodes::FieldDefinition, nil] :ast_node If this schema was parsed from definition, this AST node defined the field
38
+ # @option kwargs [Boolean] :method_conflict_warning If false, skip the warning if this field's method conflicts with a built-in method
39
+ # @option kwargs [Array<Hash>] :validates Configurations for validating this field
40
+ # @option kwargs [Object] :fallback_value A fallback value if the method is not defined
41
+ # @option kwargs [Class<GraphQL::Schema::Mutation>] :mutation
42
+ # @option kwargs [Class<GraphQL::Schema::Resolver>] :resolver
43
+ # @option kwargs [Class<GraphQL::Schema::Subscription>] :subscription
44
+ # @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby)
45
+ # @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby)
46
+ # @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby)
47
+ # @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field
48
+ # @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled.
49
+ # @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}.
50
+ # @yieldparam field [GraphQL::Schema::Field] The newly-created field instance
51
+ # @yieldreturn [void]
10
52
  # @return [GraphQL::Schema::Field]
11
- def field(*args, **kwargs, &block)
12
- field_defn = field_class.from_options(*args, owner: self, **kwargs, &block)
53
+ def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &definition_block)
54
+ resolver = kwargs.delete(:resolver)
55
+ mutation = kwargs.delete(:mutation)
56
+ subscription = kwargs.delete(:subscription)
57
+ if (resolver_class = resolver || mutation || subscription)
58
+ # Add a reference to that parent class
59
+ kwargs[:resolver_class] = resolver_class
60
+ end
61
+
62
+ kwargs[:name] ||= name_positional
63
+ if !type_positional.nil?
64
+ if desc_positional
65
+ if kwargs[:description]
66
+ raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{kwargs[:description].inspect})"
67
+ end
68
+
69
+ kwargs[:description] = desc_positional
70
+ kwargs[:type] = type_positional
71
+ elsif (resolver || mutation) && type_positional.is_a?(String)
72
+ # The return type should be copied from the resolver, and the second positional argument is the description
73
+ kwargs[:description] = type_positional
74
+ else
75
+ kwargs[:type] = type_positional
76
+ end
77
+
78
+ if type_positional.is_a?(Class) && type_positional < GraphQL::Schema::Mutation
79
+ raise ArgumentError, "Use `field #{name_positional.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
80
+ end
81
+ end
82
+
83
+ kwargs[:owner] = self
84
+ field_defn = field_class.new(**kwargs, &definition_block)
13
85
  add_field(field_defn)
14
86
  field_defn
15
87
  end
@@ -232,7 +304,7 @@ module GraphQL
232
304
  end
233
305
  end
234
306
 
235
- # @param [GraphQL::Schema::Field]
307
+ # @param field_defn [GraphQL::Schema::Field]
236
308
  # @return [String] A warning to give when this field definition might conflict with a built-in method
237
309
  def conflict_field_name_warning(field_defn)
238
310
  "#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
@@ -8,6 +8,13 @@ module GraphQL
8
8
  #
9
9
  # (This is for specifying mutually exclusive sets of arguments.)
10
10
  #
11
+ # If you use {GraphQL::Schema::Visibility} to hide all the arguments in a `one_of: [..]` set,
12
+ # then a developer-facing {GraphQL::Error} will be raised during execution. Pass `allow_all_hidden: true` to
13
+ # skip validation in this case instead.
14
+ #
15
+ # This validator also implements `argument ... required: :nullable`. If an argument has `required: :nullable`
16
+ # but it's hidden with {GraphQL::Schema::Visibility}, then this validator doesn't run.
17
+ #
11
18
  # @example Require exactly one of these arguments
12
19
  #
13
20
  # field :update_amount, IngredientAmount, null: false do
@@ -37,15 +44,17 @@ module GraphQL
37
44
  class RequiredValidator < Validator
38
45
  # @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
39
46
  # @param argument [Symbol] An argument that is required for this field
47
+ # @param allow_all_hidden [Boolean] If `true`, then this validator won't run if all the `one_of: ...` arguments have been hidden
40
48
  # @param message [String]
41
- def initialize(one_of: nil, argument: nil, message: nil, **default_options)
49
+ def initialize(one_of: nil, argument: nil, allow_all_hidden: nil, message: nil, **default_options)
42
50
  @one_of = if one_of
43
51
  one_of
44
52
  elsif argument
45
- [argument]
53
+ [ argument ]
46
54
  else
47
55
  raise ArgumentError, "`one_of:` or `argument:` must be given in `validates required: {...}`"
48
56
  end
57
+ @allow_all_hidden = allow_all_hidden.nil? ? !!argument : allow_all_hidden
49
58
  @message = message
50
59
  super(**default_options)
51
60
  end
@@ -54,10 +63,17 @@ module GraphQL
54
63
  fully_matched_conditions = 0
55
64
  partially_matched_conditions = 0
56
65
 
66
+ visible_keywords = context.types.arguments(@validated).map(&:keyword)
67
+ no_visible_conditions = true
68
+
57
69
  if !value.nil?
58
70
  @one_of.each do |one_of_condition|
59
71
  case one_of_condition
60
72
  when Symbol
73
+ if no_visible_conditions && visible_keywords.include?(one_of_condition)
74
+ no_visible_conditions = false
75
+ end
76
+
61
77
  if value.key?(one_of_condition)
62
78
  fully_matched_conditions += 1
63
79
  end
@@ -66,6 +82,9 @@ module GraphQL
66
82
  full_match = true
67
83
 
68
84
  one_of_condition.each do |k|
85
+ if no_visible_conditions && visible_keywords.include?(k)
86
+ no_visible_conditions = false
87
+ end
69
88
  if value.key?(k)
70
89
  any_match = true
71
90
  else
@@ -88,6 +107,18 @@ module GraphQL
88
107
  end
89
108
  end
90
109
 
110
+ if no_visible_conditions
111
+ if @allow_all_hidden
112
+ return nil
113
+ else
114
+ raise GraphQL::Error, <<~ERR
115
+ #{@validated.path} validates `required: ...` but all required arguments were hidden.
116
+
117
+ Update your schema definition to allow the client to see some fields or skip validation by adding `required: { ..., allow_all_hidden: true }`
118
+ ERR
119
+ end
120
+ end
121
+
91
122
  if fully_matched_conditions == 1 && partially_matched_conditions == 0
92
123
  nil # OK
93
124
  else
@@ -10,9 +10,9 @@ module GraphQL
10
10
  class Visibility
11
11
  # @param schema [Class<GraphQL::Schema>]
12
12
  # @param profiles [Hash<Symbol => Hash>] A hash of `name => context` pairs for preloading visibility profiles
13
- # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?`)
13
+ # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?` and `Rails.env.staging?`)
14
14
  # @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
15
- def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? Rails.env.production? : nil), migration_errors: false)
15
+ def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? (Rails.env.production? || Rails.env.staging?) : nil), migration_errors: false)
16
16
  profiles&.each { |name, ctx|
17
17
  ctx[:visibility_profile] = name
18
18
  ctx.freeze