graphql 2.5.11 → 2.6.10

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 (154) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/field_extractor.rb +17 -1
  4. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  5. data/lib/generators/graphql/templates/schema.erb +2 -1
  6. data/lib/graphql/analysis/query_complexity.rb +31 -15
  7. data/lib/graphql/analysis.rb +20 -13
  8. data/lib/graphql/backtrace/table.rb +10 -1
  9. data/lib/graphql/current.rb +7 -1
  10. data/lib/graphql/dashboard/application_controller.rb +45 -0
  11. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  12. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  13. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  15. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  16. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  17. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  18. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  19. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  20. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  21. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  22. data/lib/graphql/dashboard.rb +14 -73
  23. data/lib/graphql/dataloader/async_dataloader.rb +336 -60
  24. data/lib/graphql/dataloader/null_dataloader.rb +48 -10
  25. data/lib/graphql/dataloader/source.rb +32 -19
  26. data/lib/graphql/dataloader.rb +107 -39
  27. data/lib/graphql/date_encoding_error.rb +1 -1
  28. data/lib/graphql/execution/directive_checks.rb +2 -0
  29. data/lib/graphql/execution/field_resolve_step.rb +804 -0
  30. data/lib/graphql/execution/finalize.rb +229 -0
  31. data/lib/graphql/execution/input_values.rb +333 -0
  32. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -0
  33. data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
  34. data/lib/graphql/execution/interpreter/resolve.rb +7 -13
  35. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  36. data/lib/graphql/execution/interpreter/runtime.rb +60 -40
  37. data/lib/graphql/execution/interpreter.rb +17 -23
  38. data/lib/graphql/execution/lazy.rb +1 -1
  39. data/lib/graphql/execution/load_argument_step.rb +102 -0
  40. data/lib/graphql/execution/multiplex.rb +1 -1
  41. data/lib/graphql/execution/next.rb +98 -0
  42. data/lib/graphql/execution/prepare_object_step.rb +151 -0
  43. data/lib/graphql/execution/resolve_type_step.rb +27 -0
  44. data/lib/graphql/execution/runner.rb +457 -0
  45. data/lib/graphql/execution/selections_step.rb +95 -0
  46. data/lib/graphql/execution.rb +15 -4
  47. data/lib/graphql/execution_error.rb +17 -12
  48. data/lib/graphql/float_decoding_error.rb +13 -0
  49. data/lib/graphql/float_encoding_error.rb +28 -0
  50. data/lib/graphql/introspection/directive_type.rb +7 -3
  51. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  52. data/lib/graphql/introspection/entry_points.rb +11 -3
  53. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  54. data/lib/graphql/introspection/field_type.rb +13 -5
  55. data/lib/graphql/introspection/input_value_type.rb +21 -13
  56. data/lib/graphql/introspection/schema_type.rb +6 -2
  57. data/lib/graphql/introspection/type_type.rb +64 -28
  58. data/lib/graphql/invalid_null_error.rb +11 -5
  59. data/lib/graphql/language/block_string.rb +6 -11
  60. data/lib/graphql/language/cache.rb +84 -14
  61. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  62. data/lib/graphql/language/lexer.rb +27 -17
  63. data/lib/graphql/language/nodes.rb +2 -1
  64. data/lib/graphql/language/parser.rb +1 -1
  65. data/lib/graphql/language.rb +32 -17
  66. data/lib/graphql/pagination/connection.rb +2 -0
  67. data/lib/graphql/pagination/connections.rb +30 -0
  68. data/lib/graphql/pagination/relation_connection.rb +10 -1
  69. data/lib/graphql/query/context.rb +10 -3
  70. data/lib/graphql/query/null_context.rb +9 -3
  71. data/lib/graphql/query/partial.rb +18 -3
  72. data/lib/graphql/query/variable_validation_error.rb +16 -1
  73. data/lib/graphql/query/variables.rb +1 -1
  74. data/lib/graphql/query.rb +12 -3
  75. data/lib/graphql/railtie.rb +2 -1
  76. data/lib/graphql/runtime_error.rb +6 -0
  77. data/lib/graphql/schema/argument.rb +13 -1
  78. data/lib/graphql/schema/build_from_definition.rb +28 -1
  79. data/lib/graphql/schema/directive/feature.rb +4 -0
  80. data/lib/graphql/schema/directive/transform.rb +20 -0
  81. data/lib/graphql/schema/directive.rb +48 -13
  82. data/lib/graphql/schema/field/connection_extension.rb +4 -37
  83. data/lib/graphql/schema/field/scope_extension.rb +18 -13
  84. data/lib/graphql/schema/field.rb +79 -48
  85. data/lib/graphql/schema/field_extension.rb +11 -8
  86. data/lib/graphql/schema/has_single_input_argument.rb +24 -13
  87. data/lib/graphql/schema/input_object.rb +6 -2
  88. data/lib/graphql/schema/interface.rb +26 -0
  89. data/lib/graphql/schema/introspection_system.rb +6 -21
  90. data/lib/graphql/schema/list.rb +5 -1
  91. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -11
  92. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  93. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  94. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  95. data/lib/graphql/schema/member/has_fields.rb +86 -5
  96. data/lib/graphql/schema/member.rb +5 -0
  97. data/lib/graphql/schema/non_null.rb +1 -1
  98. data/lib/graphql/schema/object.rb +1 -0
  99. data/lib/graphql/schema/printer.rb +1 -1
  100. data/lib/graphql/schema/ractor_shareable.rb +1 -0
  101. data/lib/graphql/schema/relay_classic_mutation.rb +16 -2
  102. data/lib/graphql/schema/resolver.rb +76 -1
  103. data/lib/graphql/schema/subscription.rb +53 -10
  104. data/lib/graphql/schema/timeout.rb +2 -2
  105. data/lib/graphql/schema/validator/allow_blank_validator.rb +3 -3
  106. data/lib/graphql/schema/validator/allow_null_validator.rb +3 -3
  107. data/lib/graphql/schema/validator/exclusion_validator.rb +2 -2
  108. data/lib/graphql/schema/validator/format_validator.rb +3 -3
  109. data/lib/graphql/schema/validator/inclusion_validator.rb +2 -2
  110. data/lib/graphql/schema/validator/length_validator.rb +6 -6
  111. data/lib/graphql/schema/validator/numericality_validator.rb +19 -19
  112. data/lib/graphql/schema/validator/required_validator.rb +38 -5
  113. data/lib/graphql/schema/validator.rb +9 -0
  114. data/lib/graphql/schema/visibility/profile.rb +74 -53
  115. data/lib/graphql/schema/visibility/visit.rb +1 -1
  116. data/lib/graphql/schema/visibility.rb +32 -24
  117. data/lib/graphql/schema/wrapper.rb +11 -1
  118. data/lib/graphql/schema.rb +93 -27
  119. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  120. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  121. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  122. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  123. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  124. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  125. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  126. data/lib/graphql/static_validation/rules/fields_will_merge.rb +351 -248
  127. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  128. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  129. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  130. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  131. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  132. data/lib/graphql/static_validation/validation_context.rb +1 -1
  133. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  134. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +40 -10
  135. data/lib/graphql/subscriptions/serialize.rb +10 -4
  136. data/lib/graphql/subscriptions.rb +36 -1
  137. data/lib/graphql/testing/helpers.rb +12 -9
  138. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  139. data/lib/graphql/testing.rb +1 -0
  140. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  141. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  142. data/lib/graphql/tracing/perfetto_trace.rb +213 -81
  143. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  144. data/lib/graphql/tracing/trace.rb +6 -0
  145. data/lib/graphql/types/float.rb +18 -4
  146. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  147. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  148. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  149. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  150. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  151. data/lib/graphql/unauthorized_error.rb +9 -1
  152. data/lib/graphql/version.rb +1 -1
  153. data/lib/graphql.rb +8 -2
  154. metadata +23 -59
@@ -23,6 +23,7 @@ module GraphQL
23
23
  # Really we only need description & comment from here, but:
24
24
  extend Schema::Member::BaseDSLMethods
25
25
  extend GraphQL::Schema::Member::HasArguments
26
+ extend GraphQL::Schema::Member::HasAuthorization
26
27
  extend GraphQL::Schema::Member::HasValidators
27
28
  include Schema::Member::HasPath
28
29
  extend Schema::Member::HasPath
@@ -45,8 +46,10 @@ module GraphQL
45
46
  @prepared_arguments = nil
46
47
  end
47
48
 
49
+ attr_accessor :exec_result, :exec_index, :field_resolve_step, :raw_arguments
50
+
48
51
  # @return [Object] The application object this field is being resolved on
49
- attr_reader :object
52
+ attr_accessor :object
50
53
 
51
54
  # @return [GraphQL::Query::Context]
52
55
  attr_reader :context
@@ -54,6 +57,74 @@ module GraphQL
54
57
  # @return [GraphQL::Schema::Field]
55
58
  attr_reader :field
56
59
 
60
+ attr_writer :prepared_arguments
61
+
62
+ def call
63
+ q = context.query
64
+ trace_objs = [object]
65
+ q.current_trace.begin_execute_field(field, @prepared_arguments, trace_objs, q)
66
+ is_ready = ready?(**@prepared_arguments)
67
+ runner = @field_resolve_step.runner
68
+ if runner.resolves_lazies && runner.schema.lazy?(is_ready)
69
+ is_ready, new_return_value = runner.schema.sync_lazy(is_ready)
70
+ end
71
+
72
+ if is_ready.is_a?(Array)
73
+ is_ready, new_return_value = is_ready
74
+ if is_ready != false
75
+ raise "Unexpected result from #ready? (expected `true`, `false` or `[false, {...}]`): [#{is_ready.inspect}, #{new_return_value.inspect}]"
76
+ else
77
+ new_return_value
78
+ end
79
+ end
80
+
81
+ if is_ready
82
+ begin
83
+ is_authed, new_return_value = authorized?(**@prepared_arguments)
84
+ rescue GraphQL::UnauthorizedError => err
85
+ new_return_value = q.schema.unauthorized_object(err)
86
+ is_authed = false
87
+ end
88
+ end
89
+
90
+ if runner.resolves_lazies && runner.schema.lazy?(is_authed)
91
+ is_authed, new_return_value = runner.schema.sync_lazy(is_authed)
92
+ end
93
+
94
+ result = if is_authed
95
+ Schema::Validator.validate!(self.class.validators, object, context, @prepared_arguments, as: @field)
96
+ call_resolve(@prepared_arguments)
97
+ elsif new_return_value.nil?
98
+ err = UnauthorizedFieldError.new(object: object, type: @field_resolve_step.parent_type, context: context, field: @field)
99
+ context.schema.unauthorized_field(err)
100
+ else
101
+ new_return_value
102
+ end
103
+ q = context.query
104
+ q.current_trace.end_execute_field(field, @prepared_arguments, trace_objs, q, [result])
105
+ exec_result[exec_index] = result
106
+ rescue GraphQL::UnauthorizedError => auth_err
107
+ exec_result[exec_index] = begin
108
+ context.schema.unauthorized_object(auth_err)
109
+ rescue GraphQL::ExecutionError => exec_err
110
+ exec_err
111
+ end
112
+ rescue GraphQL::RuntimeError => err
113
+ exec_result[exec_index] = err
114
+ rescue StandardError => stderr
115
+ exec_result[exec_index] = begin
116
+ context.query.handle_or_reraise(stderr)
117
+ rescue GraphQL::ExecutionError => ex_err
118
+ ex_err
119
+ end
120
+ ensure
121
+ field_pending_steps = field_resolve_step.pending_steps
122
+ field_pending_steps.delete(self)
123
+ if field_pending_steps.size == 0 && field_resolve_step.field_results
124
+ field_resolve_step.runner.add_step(field_resolve_step)
125
+ end
126
+ end
127
+
57
128
  def arguments
58
129
  @prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
59
130
  end
@@ -154,6 +225,10 @@ module GraphQL
154
225
  authorize_arguments(args, inputs)
155
226
  end
156
227
 
228
+ def self.authorizes?(context)
229
+ self.instance_method(:authorized?).owner != GraphQL::Schema::Resolver
230
+ end
231
+
157
232
  # Called when an object loaded by `loads:` fails the `.authorized?` check for its resolved GraphQL object type.
158
233
  #
159
234
  # By default, the error is re-raised and passed along to {{Schema.unauthorized_object}}.
@@ -15,8 +15,6 @@ module GraphQL
15
15
  extend GraphQL::Schema::Resolver::HasPayloadType
16
16
  extend GraphQL::Schema::Member::HasFields
17
17
  NO_UPDATE = :no_update
18
- # The generated payload type is required; If there's no payload,
19
- # propagate null.
20
18
  null false
21
19
 
22
20
  # @api private
@@ -32,14 +30,47 @@ module GraphQL
32
30
  end
33
31
  end
34
32
 
33
+ # @api private
34
+ def call_resolve(args_hash)
35
+ if @field_resolve_step.nil?
36
+ super
37
+ else
38
+ context.namespace(:subscriptions)[:update_event] = event
39
+ result = nil
40
+ unsubscribed = true
41
+ unsubscribed_result = nil
42
+ begin
43
+ result = super
44
+ unsubscribed = false
45
+ rescue EarlyUnsubscribe => err
46
+ unsubscribed_result = err.unsubscribed_result
47
+ end
48
+
49
+
50
+ if unsubscribed
51
+ if unsubscribed_result
52
+ context.namespace(:subscriptions)[:final_update] = true
53
+ unsubscribed_result
54
+ else
55
+ context.skip
56
+ end
57
+ else
58
+ result
59
+ end
60
+ end
61
+ end
62
+
35
63
  # @api private
36
64
  def resolve_with_support(**args)
37
65
  @original_arguments = args # before `loads:` have been run
38
66
  result = nil
39
67
  unsubscribed = true
40
- unsubscribed_result = catch :graphql_subscription_unsubscribed do
68
+ unsubscribed_result = nil
69
+ begin
41
70
  result = super
42
71
  unsubscribed = false
72
+ rescue EarlyUnsubscribe => err
73
+ unsubscribed_result = err.unsubscribed_result
43
74
  end
44
75
 
45
76
 
@@ -116,7 +147,13 @@ module GraphQL
116
147
  # @return [void]
117
148
  def unsubscribe(update_value = nil)
118
149
  context.namespace(:subscriptions)[:unsubscribed] = true
119
- throw :graphql_subscription_unsubscribed, update_value
150
+ err = EarlyUnsubscribe.new
151
+ err.unsubscribed_result = update_value
152
+ raise err
153
+ end
154
+
155
+ class EarlyUnsubscribe < GraphQL::RuntimeError
156
+ attr_accessor :unsubscribed_result
120
157
  end
121
158
 
122
159
  # Call this method to provide a new subscription_scope; OR
@@ -189,12 +226,18 @@ module GraphQL
189
226
 
190
227
  # @return [Subscriptions::Event] This object is used as a representation of this subscription for the backend
191
228
  def event
192
- @event ||= Subscriptions::Event.new(
193
- name: field.name,
194
- arguments: @original_arguments,
195
- context: context,
196
- field: field,
197
- )
229
+ @event ||= begin
230
+ if @original_arguments.nil? && @field_resolve_step
231
+ @original_arguments, _errors = @field_resolve_step.arguments_without_loads
232
+ end
233
+
234
+ Subscriptions::Event.new(
235
+ name: field.name,
236
+ arguments: @original_arguments,
237
+ context: context,
238
+ field: field,
239
+ )
240
+ end
198
241
  end
199
242
  end
200
243
  end
@@ -68,7 +68,7 @@ module GraphQL
68
68
  super
69
69
  end
70
70
 
71
- def execute_field(query:, field:, **_rest)
71
+ def begin_execute_field(field, _arguments, _objects, query)
72
72
  timeout_state = query.context.namespace(@timeout).fetch(:state)
73
73
  # If the `:state` is `false`, then `max_seconds(query)` opted out of timeout for this query.
74
74
  if timeout_state == false
@@ -84,7 +84,7 @@ module GraphQL
84
84
 
85
85
  # `handle_timeout` may have set this to be `false`
86
86
  if timeout_state != false
87
- error
87
+ raise error
88
88
  else
89
89
  super
90
90
  end
@@ -8,7 +8,7 @@ module GraphQL
8
8
  # @example Require a non-empty string for an argument
9
9
  # argument :name, String, required: true, validate: { allow_blank: false }
10
10
  class AllowBlankValidator < Validator
11
- def initialize(allow_blank_positional, allow_blank: nil, message: "%{validated} can't be blank", **default_options)
11
+ def initialize(allow_blank_positional = nil, allow_blank: nil, message: "%{validated} can't be blank", **default_options)
12
12
  @message = message
13
13
  super(**default_options)
14
14
  @allow_blank = allow_blank.nil? ? allow_blank_positional : allow_blank
@@ -16,10 +16,10 @@ module GraphQL
16
16
 
17
17
  def validate(_object, _context, value)
18
18
  if value.respond_to?(:blank?) && value.blank?
19
- if (value.nil? && @allow_null) || @allow_blank
19
+ if (value.nil? && validation_parameter(@allow_null)) || validation_parameter(@allow_blank)
20
20
  # pass
21
21
  else
22
- @message
22
+ validation_parameter(@message)
23
23
  end
24
24
  end
25
25
  end
@@ -9,15 +9,15 @@ module GraphQL
9
9
  # argument :name, String, required: false, validates: { allow_null: false }
10
10
  class AllowNullValidator < Validator
11
11
  MESSAGE = "%{validated} can't be null"
12
- def initialize(allow_null_positional, allow_null: nil, message: MESSAGE, **default_options)
12
+ def initialize(allow_null_positional = nil, allow_null: nil, message: MESSAGE, **default_options)
13
13
  @message = message
14
14
  super(**default_options)
15
15
  @allow_null = allow_null.nil? ? allow_null_positional : allow_null
16
16
  end
17
17
 
18
18
  def validate(_object, _context, value)
19
- if value.nil? && !@allow_null
20
- @message
19
+ if value.nil? && !validation_parameter(@allow_null)
20
+ validation_parameter(@message)
21
21
  end
22
22
  end
23
23
  end
@@ -23,8 +23,8 @@ module GraphQL
23
23
  def validate(_object, _context, value)
24
24
  if permitted_empty_value?(value)
25
25
  # pass
26
- elsif @in_list.include?(value)
27
- @message
26
+ elsif validation_parameter(@in_list).include?(value)
27
+ validation_parameter(@message)
28
28
  end
29
29
  end
30
30
  end
@@ -37,9 +37,9 @@ module GraphQL
37
37
  if permitted_empty_value?(value)
38
38
  # Do nothing
39
39
  elsif value.nil? ||
40
- (@with_pattern && !value.match?(@with_pattern)) ||
41
- (@without_pattern && value.match?(@without_pattern))
42
- @message
40
+ (@with_pattern && !value.match?(validation_parameter(@with_pattern))) ||
41
+ (@without_pattern && value.match?(validation_parameter(@without_pattern)))
42
+ validation_parameter(@message)
43
43
  end
44
44
  end
45
45
  end
@@ -25,8 +25,8 @@ module GraphQL
25
25
  def validate(_object, _context, value)
26
26
  if permitted_empty_value?(value)
27
27
  # pass
28
- elsif !@in_list.include?(value)
29
- @message
28
+ elsif !validation_parameter(@in_list).include?(value)
29
+ validation_parameter(@message)
30
30
  end
31
31
  end
32
32
  end
@@ -45,12 +45,12 @@ module GraphQL
45
45
  def validate(_object, _context, value)
46
46
  return if permitted_empty_value?(value) # pass in this case
47
47
  length = value.nil? ? 0 : value.length
48
- if @maximum && length > @maximum
49
- partial_format(@too_long, { count: @maximum })
50
- elsif @minimum && length < @minimum
51
- partial_format(@too_short, { count: @minimum })
52
- elsif @is && length != @is
53
- partial_format(@wrong_length, { count: @is })
48
+ if (current_max = validation_parameter(@maximum)) && length > current_max
49
+ partial_format(validation_parameter(@too_long), { count: current_max })
50
+ elsif (current_min = validation_parameter(@minimum)) && length < current_min
51
+ partial_format(validation_parameter(@too_short), { count: current_min })
52
+ elsif (current_is = validation_parameter(@is)) && length != current_is
53
+ partial_format(validation_parameter(@wrong_length), { count: current_is })
54
54
  end
55
55
  end
56
56
  end
@@ -55,25 +55,25 @@ module GraphQL
55
55
  if permitted_empty_value?(value)
56
56
  # pass in this case
57
57
  elsif value.nil? # @allow_null is handled in the parent class
58
- @null_message
59
- elsif @greater_than && value <= @greater_than
60
- partial_format(@message, { comparison: "greater than", target: @greater_than })
61
- elsif @greater_than_or_equal_to && value < @greater_than_or_equal_to
62
- partial_format(@message, { comparison: "greater than or equal to", target: @greater_than_or_equal_to })
63
- elsif @less_than && value >= @less_than
64
- partial_format(@message, { comparison: "less than", target: @less_than })
65
- elsif @less_than_or_equal_to && value > @less_than_or_equal_to
66
- partial_format(@message, { comparison: "less than or equal to", target: @less_than_or_equal_to })
67
- elsif @equal_to && value != @equal_to
68
- partial_format(@message, { comparison: "equal to", target: @equal_to })
69
- elsif @other_than && value == @other_than
70
- partial_format(@message, { comparison: "something other than", target: @other_than })
71
- elsif @even && !value.even?
72
- (partial_format(@message, { comparison: "even", target: "" })).strip
73
- elsif @odd && !value.odd?
74
- (partial_format(@message, { comparison: "odd", target: "" })).strip
75
- elsif @within && !@within.include?(value)
76
- partial_format(@message, { comparison: "within", target: @within })
58
+ validation_parameter(@null_message)
59
+ elsif (current_greater_than = validation_parameter(@greater_than)) && value <= current_greater_than
60
+ partial_format(validation_parameter(@message), { comparison: "greater than", target: current_greater_than })
61
+ elsif (current_greater_than_or_equal_to = validation_parameter(@greater_than_or_equal_to)) && value < current_greater_than_or_equal_to
62
+ partial_format(validation_parameter(@message), { comparison: "greater than or equal to", target: current_greater_than_or_equal_to })
63
+ elsif (current_less_than = validation_parameter(@less_than)) && value >= current_less_than
64
+ partial_format(validation_parameter(@message), { comparison: "less than", target: current_less_than })
65
+ elsif (current_less_than_or_equal_to = validation_parameter(@less_than_or_equal_to)) && value > current_less_than_or_equal_to
66
+ partial_format(validation_parameter(@message), { comparison: "less than or equal to", target: current_less_than_or_equal_to })
67
+ elsif (current_equal_to = validation_parameter(@equal_to)) && value != current_equal_to
68
+ partial_format(validation_parameter(@message), { comparison: "equal to", target: current_equal_to })
69
+ elsif (current_other_than = validation_parameter(@other_than)) && value == current_other_than
70
+ partial_format(validation_parameter(@message), { comparison: "something other than", target: current_other_than })
71
+ elsif validation_parameter(@even) && !value.even?
72
+ (partial_format(validation_parameter(@message), { comparison: "even", target: "" })).strip
73
+ elsif validation_parameter(@odd) && !value.odd?
74
+ (partial_format(validation_parameter(@message), { comparison: "odd", target: "" })).strip
75
+ elsif (current_within = validation_parameter(@within)) && !current_within.include?(value)
76
+ partial_format(validation_parameter(@message), { comparison: "within", target: current_within })
77
77
  end
78
78
  end
79
79
  end
@@ -8,6 +8,13 @@ module GraphQL
8
8
  #
9
9
  # (This is for specifying mutually exclusive sets of arguments.)
10
10
  #
11
+ # If you use {GraphQL::Schema::Visibility} to hide all the arguments in a `one_of: [..]` set,
12
+ # then a developer-facing {GraphQL::Error} will be raised during execution. Pass `allow_all_hidden: true` to
13
+ # skip validation in this case instead.
14
+ #
15
+ # This validator also implements `argument ... required: :nullable`. If an argument has `required: :nullable`
16
+ # but it's hidden with {GraphQL::Schema::Visibility}, then this validator doesn't run.
17
+ #
11
18
  # @example Require exactly one of these arguments
12
19
  #
13
20
  # field :update_amount, IngredientAmount, null: false do
@@ -37,15 +44,17 @@ module GraphQL
37
44
  class RequiredValidator < Validator
38
45
  # @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
39
46
  # @param argument [Symbol] An argument that is required for this field
47
+ # @param allow_all_hidden [Boolean] If `true`, then this validator won't run if all the `one_of: ...` arguments have been hidden
40
48
  # @param message [String]
41
- def initialize(one_of: nil, argument: nil, message: nil, **default_options)
49
+ def initialize(one_of: nil, argument: nil, allow_all_hidden: nil, message: nil, **default_options)
42
50
  @one_of = if one_of
43
51
  one_of
44
52
  elsif argument
45
- [argument]
53
+ [ argument ]
46
54
  else
47
55
  raise ArgumentError, "`one_of:` or `argument:` must be given in `validates required: {...}`"
48
56
  end
57
+ @allow_all_hidden = allow_all_hidden.nil? ? !!argument : allow_all_hidden
49
58
  @message = message
50
59
  super(**default_options)
51
60
  end
@@ -54,10 +63,18 @@ 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
- @one_of.each do |one_of_condition|
70
+ validation_parameter(@one_of).each do |one_of_condition|
71
+ one_of_condition = validation_parameter(one_of_condition)
59
72
  case one_of_condition
60
73
  when Symbol
74
+ if no_visible_conditions && visible_keywords.include?(one_of_condition)
75
+ no_visible_conditions = false
76
+ end
77
+
61
78
  if value.key?(one_of_condition)
62
79
  fully_matched_conditions += 1
63
80
  end
@@ -66,6 +83,9 @@ module GraphQL
66
83
  full_match = true
67
84
 
68
85
  one_of_condition.each do |k|
86
+ if no_visible_conditions && visible_keywords.include?(k)
87
+ no_visible_conditions = false
88
+ end
69
89
  if value.key?(k)
70
90
  any_match = true
71
91
  else
@@ -88,10 +108,22 @@ module GraphQL
88
108
  end
89
109
  end
90
110
 
111
+ if no_visible_conditions
112
+ if validation_parameter(@allow_all_hidden)
113
+ return nil
114
+ else
115
+ raise GraphQL::Error, <<~ERR
116
+ #{@validated.path} validates `required: ...` but all required arguments were hidden.
117
+
118
+ Update your schema definition to allow the client to see some fields or skip validation by adding `required: { ..., allow_all_hidden: true }`
119
+ ERR
120
+ end
121
+ end
122
+
91
123
  if fully_matched_conditions == 1 && partially_matched_conditions == 0
92
124
  nil # OK
93
125
  else
94
- @message || build_message(context)
126
+ validation_parameter(@message) || build_message(context)
95
127
  end
96
128
  end
97
129
 
@@ -99,8 +131,9 @@ module GraphQL
99
131
  argument_definitions = context.types.arguments(@validated)
100
132
 
101
133
  required_names = @one_of.map do |arg_keyword|
134
+ arg_keyword = validation_parameter(arg_keyword)
102
135
  if arg_keyword.is_a?(Array)
103
- names = arg_keyword.map { |arg| arg_keyword_to_graphql_name(argument_definitions, arg) }
136
+ names = arg_keyword.map { |arg| arg_keyword_to_graphql_name(argument_definitions, validation_parameter(arg)) }
104
137
  names.compact! # hidden arguments are `nil`
105
138
  "(" + names.join(" and ") + ")"
106
139
  else
@@ -34,6 +34,15 @@ module GraphQL
34
34
  string
35
35
  end
36
36
 
37
+ # @return [Object] The current value to use for validation, based on `config_value` from configuration time. If a Proc is given, this calls it and returns it.
38
+ def validation_parameter(config_value)
39
+ if config_value.is_a?(Proc)
40
+ config_value.call
41
+ else
42
+ config_value
43
+ end
44
+ end
45
+
37
46
  # @return [Boolean] `true` if `value` is `nil` and this validator has `allow_null: true` or if value is `.blank?` and this validator has `allow_blank: true`
38
47
  def permitted_empty_value?(value)
39
48
  (value.nil? && @allow_null) ||
@@ -54,6 +54,9 @@ module GraphQL
54
54
  @cached_fields.default_proc = nil
55
55
  @cached_arguments.default_proc = nil
56
56
  @loadable_possible_types.default_proc = nil
57
+ @cached_field_result.default_proc = nil
58
+ @cached_field_result.each { |_, h| h.default_proc = nil }
59
+ @cached_type_result.default_proc = nil
57
60
  super
58
61
  end
59
62
 
@@ -122,6 +125,14 @@ module GraphQL
122
125
  end.compare_by_identity
123
126
 
124
127
  @loadable_possible_types = Hash.new { |h, union_type| h[union_type] = union_type.possible_types }.compare_by_identity
128
+
129
+ # Combined cache for field(owner, field_name) — avoids repeated kind check + parent lookup + visibility check
130
+ @cached_field_result = Hash.new { |h, owner|
131
+ h[owner] = Hash.new { |h2, field_name| h2[field_name] = compute_field(owner, field_name) }
132
+ }.compare_by_identity
133
+
134
+ # Cache for type(type_name) — avoids repeated get_type + visibility + referenced? checks
135
+ @cached_type_result = Hash.new { |h, type_name| h[type_name] = compute_type(type_name) }
125
136
  end
126
137
 
127
138
  def field_on_visible_interface?(field, owner)
@@ -149,58 +160,11 @@ module GraphQL
149
160
  end
150
161
 
151
162
  def type(type_name)
152
- t = @visibility.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
153
- if t
154
- if t.is_a?(Array)
155
- vis_t = nil
156
- t.each do |t_defn|
157
- if @cached_visible[t_defn] && referenced?(t_defn)
158
- if vis_t.nil?
159
- vis_t = t_defn
160
- else
161
- raise_duplicate_definition(vis_t, t_defn)
162
- end
163
- end
164
- end
165
- vis_t
166
- else
167
- if t && @cached_visible[t] && referenced?(t)
168
- t
169
- else
170
- nil
171
- end
172
- end
173
- end
163
+ @cached_type_result[type_name]
174
164
  end
175
165
 
176
166
  def field(owner, field_name)
177
- f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name])
178
- field
179
- elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name))
180
- entry_point_field
181
- elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name))
182
- dynamic_field
183
- else
184
- nil
185
- end
186
- if f.is_a?(Array)
187
- visible_f = nil
188
- f.each do |f_defn|
189
- if @cached_visible_fields[owner][f_defn]
190
-
191
- if visible_f.nil?
192
- visible_f = f_defn
193
- else
194
- raise_duplicate_definition(visible_f, f_defn)
195
- end
196
- end
197
- end
198
- visible_f&.ensure_loaded
199
- elsif f && @cached_visible_fields[owner][f.ensure_loaded]
200
- f
201
- else
202
- nil
203
- end
167
+ @cached_field_result[owner][field_name]
204
168
  end
205
169
 
206
170
  def fields(owner)
@@ -306,6 +270,7 @@ module GraphQL
306
270
  def preload
307
271
  load_all_types
308
272
  @all_types.each do |type_name, type_defn|
273
+ type(type_name)
309
274
  if type_defn.kind.fields?
310
275
  fields(type_defn).each do |f|
311
276
  field(type_defn, f.graphql_name)
@@ -325,11 +290,13 @@ module GraphQL
325
290
  end
326
291
  # Lots more to do here
327
292
  end
328
- @schema.introspection_system.entry_points.each do |f|
329
- arguments(f).each do |arg|
330
- argument(f, arg.graphql_name)
293
+ if @schema.query
294
+ @schema.introspection_system.entry_points.each do |f|
295
+ arguments(f).each do |arg|
296
+ argument(f, arg.graphql_name)
297
+ end
298
+ field(@schema.query, f.graphql_name)
331
299
  end
332
- field(@schema.query, f.graphql_name)
333
300
  end
334
301
  @schema.introspection_system.dynamic_fields.each do |f|
335
302
  arguments(f).each do |arg|
@@ -341,6 +308,60 @@ module GraphQL
341
308
 
342
309
  private
343
310
 
311
+ def compute_type(type_name)
312
+ t = @visibility.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
313
+ if t
314
+ if t.is_a?(Array)
315
+ vis_t = nil
316
+ t.each do |t_defn|
317
+ if @cached_visible[t_defn] && referenced?(t_defn)
318
+ if vis_t.nil?
319
+ vis_t = t_defn
320
+ else
321
+ raise_duplicate_definition(vis_t, t_defn)
322
+ end
323
+ end
324
+ end
325
+ vis_t
326
+ else
327
+ if t && @cached_visible[t] && referenced?(t)
328
+ t
329
+ else
330
+ nil
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ def compute_field(owner, field_name)
337
+ f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name])
338
+ field
339
+ elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name))
340
+ entry_point_field
341
+ elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name))
342
+ dynamic_field
343
+ else
344
+ nil
345
+ end
346
+ if f.is_a?(Array)
347
+ visible_f = nil
348
+ f.each do |f_defn|
349
+ if @cached_visible_fields[owner][f_defn]
350
+ if visible_f.nil?
351
+ visible_f = f_defn
352
+ else
353
+ raise_duplicate_definition(visible_f, f_defn)
354
+ end
355
+ end
356
+ end
357
+ visible_f&.ensure_loaded
358
+ elsif f && @cached_visible_fields[owner][f.ensure_loaded]
359
+ f
360
+ else
361
+ nil
362
+ end
363
+ end
364
+
344
365
  def non_duplicate_items(definitions, visibility_cache)
345
366
  non_dups = []
346
367
  names = Set.new
@@ -102,7 +102,7 @@ module GraphQL
102
102
 
103
103
  missed_late_types_streak = 0
104
104
  while (owner, late_type = @late_bound_types.shift)
105
- if (late_type.is_a?(String) && (type = Member::BuildType.constantize(type))) ||
105
+ if (late_type.is_a?(String) && (type = Member::BuildType.constantize(late_type))) ||
106
106
  (late_type.is_a?(LateBoundType) && (type = @visited_types.find { |t| t.graphql_name == late_type.graphql_name }))
107
107
  missed_late_types_streak = 0 # might succeed next round
108
108
  update_type_owner(owner, type)