graphql 2.4.3 → 2.5.3

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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/analyzer.rb +2 -1
  3. data/lib/graphql/analysis/query_complexity.rb +87 -7
  4. data/lib/graphql/analysis/visitor.rb +38 -41
  5. data/lib/graphql/analysis.rb +15 -12
  6. data/lib/graphql/autoload.rb +38 -0
  7. data/lib/graphql/backtrace/table.rb +118 -55
  8. data/lib/graphql/backtrace.rb +1 -19
  9. data/lib/graphql/current.rb +7 -2
  10. data/lib/graphql/dashboard/detailed_traces.rb +47 -0
  11. data/lib/graphql/dashboard/installable.rb +22 -0
  12. data/lib/graphql/dashboard/limiters.rb +93 -0
  13. data/lib/graphql/dashboard/operation_store.rb +199 -0
  14. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
  15. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
  16. data/lib/graphql/dashboard/statics/charts.min.css +1 -0
  17. data/lib/graphql/dashboard/statics/dashboard.css +30 -0
  18. data/lib/graphql/dashboard/statics/dashboard.js +143 -0
  19. data/lib/graphql/dashboard/statics/header-icon.png +0 -0
  20. data/lib/graphql/dashboard/statics/icon.png +0 -0
  21. data/lib/graphql/dashboard/subscriptions.rb +96 -0
  22. data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
  23. data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
  24. data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
  25. data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
  26. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
  27. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
  28. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
  29. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
  30. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
  31. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
  32. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
  33. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
  34. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
  35. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
  36. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
  37. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +108 -0
  38. data/lib/graphql/dashboard.rb +158 -0
  39. data/lib/graphql/dataloader/active_record_association_source.rb +64 -0
  40. data/lib/graphql/dataloader/active_record_source.rb +26 -0
  41. data/lib/graphql/dataloader/async_dataloader.rb +21 -9
  42. data/lib/graphql/dataloader/null_dataloader.rb +1 -1
  43. data/lib/graphql/dataloader/source.rb +3 -3
  44. data/lib/graphql/dataloader.rb +43 -14
  45. data/lib/graphql/dig.rb +2 -1
  46. data/lib/graphql/execution/interpreter/resolve.rb +3 -3
  47. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +34 -4
  48. data/lib/graphql/execution/interpreter/runtime.rb +96 -52
  49. data/lib/graphql/execution/interpreter.rb +16 -7
  50. data/lib/graphql/execution/multiplex.rb +6 -5
  51. data/lib/graphql/introspection/directive_location_enum.rb +1 -1
  52. data/lib/graphql/invalid_name_error.rb +1 -1
  53. data/lib/graphql/invalid_null_error.rb +19 -16
  54. data/lib/graphql/language/cache.rb +13 -0
  55. data/lib/graphql/language/document_from_schema_definition.rb +8 -7
  56. data/lib/graphql/language/lexer.rb +11 -4
  57. data/lib/graphql/language/nodes.rb +3 -0
  58. data/lib/graphql/language/parser.rb +15 -8
  59. data/lib/graphql/language/printer.rb +8 -8
  60. data/lib/graphql/language/static_visitor.rb +37 -33
  61. data/lib/graphql/language/visitor.rb +59 -55
  62. data/lib/graphql/pagination/connection.rb +1 -1
  63. data/lib/graphql/query/context/scoped_context.rb +1 -1
  64. data/lib/graphql/query/context.rb +7 -5
  65. data/lib/graphql/query/variable_validation_error.rb +1 -1
  66. data/lib/graphql/query.rb +22 -32
  67. data/lib/graphql/railtie.rb +7 -0
  68. data/lib/graphql/schema/addition.rb +1 -1
  69. data/lib/graphql/schema/always_visible.rb +1 -0
  70. data/lib/graphql/schema/argument.rb +7 -8
  71. data/lib/graphql/schema/build_from_definition.rb +99 -53
  72. data/lib/graphql/schema/directive/flagged.rb +3 -1
  73. data/lib/graphql/schema/directive.rb +2 -2
  74. data/lib/graphql/schema/enum.rb +36 -1
  75. data/lib/graphql/schema/enum_value.rb +1 -1
  76. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  77. data/lib/graphql/schema/field.rb +27 -13
  78. data/lib/graphql/schema/field_extension.rb +1 -1
  79. data/lib/graphql/schema/has_single_input_argument.rb +3 -1
  80. data/lib/graphql/schema/input_object.rb +77 -40
  81. data/lib/graphql/schema/interface.rb +3 -2
  82. data/lib/graphql/schema/list.rb +1 -1
  83. data/lib/graphql/schema/loader.rb +1 -1
  84. data/lib/graphql/schema/member/has_arguments.rb +25 -17
  85. data/lib/graphql/schema/member/has_dataloader.rb +62 -0
  86. data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
  87. data/lib/graphql/schema/member/has_directives.rb +4 -4
  88. data/lib/graphql/schema/member/has_fields.rb +19 -1
  89. data/lib/graphql/schema/member/has_interfaces.rb +5 -5
  90. data/lib/graphql/schema/member/has_validators.rb +1 -1
  91. data/lib/graphql/schema/member/scoped.rb +1 -1
  92. data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
  93. data/lib/graphql/schema/member.rb +1 -0
  94. data/lib/graphql/schema/object.rb +25 -8
  95. data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
  96. data/lib/graphql/schema/resolver.rb +12 -10
  97. data/lib/graphql/schema/subscription.rb +52 -6
  98. data/lib/graphql/schema/union.rb +1 -1
  99. data/lib/graphql/schema/validator/required_validator.rb +23 -6
  100. data/lib/graphql/schema/validator.rb +1 -1
  101. data/lib/graphql/schema/visibility/migration.rb +1 -0
  102. data/lib/graphql/schema/visibility/profile.rb +98 -244
  103. data/lib/graphql/schema/visibility/visit.rb +190 -0
  104. data/lib/graphql/schema/visibility.rb +178 -38
  105. data/lib/graphql/schema/warden.rb +18 -5
  106. data/lib/graphql/schema.rb +266 -54
  107. data/lib/graphql/static_validation/all_rules.rb +1 -1
  108. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
  109. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
  110. data/lib/graphql/static_validation/rules/fields_will_merge.rb +79 -17
  111. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
  112. data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
  113. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
  114. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
  115. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
  116. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +1 -1
  117. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
  118. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
  119. data/lib/graphql/static_validation/validation_context.rb +1 -0
  120. data/lib/graphql/static_validation/validator.rb +6 -1
  121. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -1
  122. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
  123. data/lib/graphql/subscriptions/event.rb +12 -1
  124. data/lib/graphql/subscriptions/serialize.rb +1 -1
  125. data/lib/graphql/subscriptions.rb +1 -1
  126. data/lib/graphql/testing/helpers.rb +7 -4
  127. data/lib/graphql/tracing/active_support_notifications_trace.rb +14 -3
  128. data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
  129. data/lib/graphql/tracing/appoptics_trace.rb +9 -1
  130. data/lib/graphql/tracing/appoptics_tracing.rb +7 -0
  131. data/lib/graphql/tracing/appsignal_trace.rb +32 -55
  132. data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
  133. data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
  134. data/lib/graphql/tracing/data_dog_trace.rb +46 -158
  135. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  136. data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
  137. data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
  138. data/lib/graphql/tracing/detailed_trace.rb +93 -0
  139. data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
  140. data/lib/graphql/tracing/legacy_trace.rb +4 -61
  141. data/lib/graphql/tracing/monitor_trace.rb +283 -0
  142. data/lib/graphql/tracing/new_relic_trace.rb +47 -54
  143. data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
  144. data/lib/graphql/tracing/notifications_trace.rb +182 -34
  145. data/lib/graphql/tracing/notifications_tracing.rb +2 -0
  146. data/lib/graphql/tracing/null_trace.rb +9 -0
  147. data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
  148. data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
  149. data/lib/graphql/tracing/perfetto_trace.rb +734 -0
  150. data/lib/graphql/tracing/platform_trace.rb +5 -0
  151. data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
  152. data/lib/graphql/tracing/prometheus_trace.rb +72 -68
  153. data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
  154. data/lib/graphql/tracing/scout_trace.rb +32 -55
  155. data/lib/graphql/tracing/scout_tracing.rb +2 -0
  156. data/lib/graphql/tracing/sentry_trace.rb +62 -94
  157. data/lib/graphql/tracing/statsd_trace.rb +33 -41
  158. data/lib/graphql/tracing/statsd_tracing.rb +2 -0
  159. data/lib/graphql/tracing/trace.rb +111 -1
  160. data/lib/graphql/tracing.rb +31 -30
  161. data/lib/graphql/types/relay/connection_behaviors.rb +3 -3
  162. data/lib/graphql/types/relay/edge_behaviors.rb +2 -2
  163. data/lib/graphql/types.rb +18 -11
  164. data/lib/graphql/version.rb +1 -1
  165. data/lib/graphql.rb +55 -47
  166. metadata +146 -11
  167. data/lib/graphql/backtrace/inspect_result.rb +0 -38
  168. data/lib/graphql/backtrace/trace.rb +0 -93
  169. data/lib/graphql/backtrace/tracer.rb +0 -80
  170. data/lib/graphql/schema/null_mask.rb +0 -11
  171. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
@@ -7,7 +7,6 @@ require "graphql/schema/find_inherited_value"
7
7
  require "graphql/schema/finder"
8
8
  require "graphql/schema/introspection_system"
9
9
  require "graphql/schema/late_bound_type"
10
- require "graphql/schema/null_mask"
11
10
  require "graphql/schema/timeout"
12
11
  require "graphql/schema/type_expression"
13
12
  require "graphql/schema/unique_within_type"
@@ -61,7 +60,7 @@ module GraphQL
61
60
  # Any undiscoverable types may be provided with the `types` configuration.
62
61
  #
63
62
  # Schemas can restrict large incoming queries with `max_depth` and `max_complexity` configurations.
64
- # (These configurations can be overridden by specific calls to {Schema#execute})
63
+ # (These configurations can be overridden by specific calls to {Schema.execute})
65
64
  #
66
65
  # @example defining a schema
67
66
  # class MySchema < GraphQL::Schema
@@ -73,6 +72,9 @@ module GraphQL
73
72
  class Schema
74
73
  extend GraphQL::Schema::Member::HasAstNode
75
74
  extend GraphQL::Schema::FindInheritedValue
75
+ extend Autoload
76
+
77
+ autoload :BUILT_IN_TYPES, "graphql/schema/built_in_types"
76
78
 
77
79
  class DuplicateNamesError < GraphQL::Error
78
80
  attr_reader :duplicated_name
@@ -109,7 +111,7 @@ module GraphQL
109
111
  # @param parser [Object] An object for handling definition string parsing (must respond to `parse`)
110
112
  # @param using [Hash] Plugins to attach to the created schema with `use(key, value)`
111
113
  # @return [Class] the schema described by `document`
112
- def from_definition(definition_or_path, default_resolve: nil, parser: GraphQL.default_parser, using: {})
114
+ def from_definition(definition_or_path, default_resolve: nil, parser: GraphQL.default_parser, using: {}, base_types: {})
113
115
  # If the file ends in `.graphql` or `.graphqls`, treat it like a filepath
114
116
  if definition_or_path.end_with?(".graphql") || definition_or_path.end_with?(".graphqls")
115
117
  GraphQL::Schema::BuildFromDefinition.from_definition_path(
@@ -118,6 +120,7 @@ module GraphQL
118
120
  default_resolve: default_resolve,
119
121
  parser: parser,
120
122
  using: using,
123
+ base_types: base_types,
121
124
  )
122
125
  else
123
126
  GraphQL::Schema::BuildFromDefinition.from_definition(
@@ -126,6 +129,7 @@ module GraphQL
126
129
  default_resolve: default_resolve,
127
130
  parser: parser,
128
131
  using: using,
132
+ base_types: base_types,
129
133
  )
130
134
  end
131
135
  end
@@ -164,9 +168,6 @@ module GraphQL
164
168
  mods.each { |mod| new_class.include(mod) }
165
169
  new_class.include(DefaultTraceClass)
166
170
  trace_mode(:default, new_class)
167
- backtrace_class = Class.new(new_class)
168
- backtrace_class.include(GraphQL::Backtrace::Trace)
169
- trace_mode(:default_backtrace, backtrace_class)
170
171
  end
171
172
  trace_class_for(:default, build: true)
172
173
  end
@@ -213,11 +214,6 @@ module GraphQL
213
214
  const_set(:DefaultTrace, Class.new(base_class) do
214
215
  include DefaultTraceClass
215
216
  end)
216
- when :default_backtrace
217
- schema_base_class = trace_class_for(:default, build: true)
218
- const_set(:DefaultTraceBacktrace, Class.new(schema_base_class) do
219
- include(GraphQL::Backtrace::Trace)
220
- end)
221
217
  else
222
218
  # First, see if the superclass has a custom-defined class for this.
223
219
  # Then, if it doesn't, use this class's default trace
@@ -233,7 +229,7 @@ module GraphQL
233
229
  add_trace_options_for(mode, default_options)
234
230
 
235
231
  Class.new(base_class) do
236
- mods.any? && include(*mods)
232
+ !mods.empty? && include(*mods)
237
233
  end
238
234
  end
239
235
  end
@@ -253,7 +249,7 @@ module GraphQL
253
249
 
254
250
 
255
251
  # Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
256
- # @see {#as_json}
252
+ # @see #as_json Return a Hash representation of the schema
257
253
  # @return [String]
258
254
  def to_json(**args)
259
255
  JSON.pretty_generate(as_json(**args))
@@ -261,8 +257,6 @@ module GraphQL
261
257
 
262
258
  # Return the Hash response of {Introspection::INTROSPECTION_QUERY}.
263
259
  # @param context [Hash]
264
- # @param only [<#call(member, ctx)>]
265
- # @param except [<#call(member, ctx)>]
266
260
  # @param include_deprecated_args [Boolean] If true, deprecated arguments will be included in the JSON response
267
261
  # @param include_schema_description [Boolean] If true, the schema's description will be queried and included in the response
268
262
  # @param include_is_repeatable [Boolean] If true, `isRepeatable: true|false` will be included with the schema's directives
@@ -321,7 +315,7 @@ module GraphQL
321
315
  # @param plugin [#use] A Schema plugin
322
316
  # @return void
323
317
  def use(plugin, **kwargs)
324
- if kwargs.any?
318
+ if !kwargs.empty?
325
319
  plugin.use(self, **kwargs)
326
320
  else
327
321
  plugin.use(self)
@@ -446,7 +440,12 @@ module GraphQL
446
440
  raise GraphQL::Error, "Second definition of `query(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@query_object.inspect}"
447
441
  elsif use_visibility_profile?
448
442
  if block_given?
449
- @query_object = lazy_load_block
443
+ if visibility.preload?
444
+ @query_object = lazy_load_block.call
445
+ self.visibility.query_configured(@query_object)
446
+ else
447
+ @query_object = lazy_load_block
448
+ end
450
449
  else
451
450
  @query_object = new_query_object
452
451
  self.visibility.query_configured(@query_object)
@@ -480,7 +479,12 @@ module GraphQL
480
479
  raise GraphQL::Error, "Second definition of `mutation(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@mutation_object.inspect}"
481
480
  elsif use_visibility_profile?
482
481
  if block_given?
483
- @mutation_object = lazy_load_block
482
+ if visibility.preload?
483
+ @mutation_object = lazy_load_block.call
484
+ self.visibility.mutation_configured(@mutation_object)
485
+ else
486
+ @mutation_object = lazy_load_block
487
+ end
484
488
  else
485
489
  @mutation_object = new_mutation_object
486
490
  self.visibility.mutation_configured(@mutation_object)
@@ -514,7 +518,12 @@ module GraphQL
514
518
  raise GraphQL::Error, "Second definition of `subscription(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@subscription_object.inspect}"
515
519
  elsif use_visibility_profile?
516
520
  if block_given?
517
- @subscription_object = lazy_load_block
521
+ if visibility.preload?
522
+ @subscription_object = lazy_load_block.call
523
+ visibility.subscription_configured(@subscription_object)
524
+ else
525
+ @subscription_object = lazy_load_block
526
+ end
518
527
  else
519
528
  @subscription_object = new_subscription_object
520
529
  self.visibility.subscription_configured(@subscription_object)
@@ -676,7 +685,7 @@ module GraphQL
676
685
  # and generally speaking, we won't inherit any values.
677
686
  # So optimize the most common case -- don't create a duplicate Hash.
678
687
  inherited_value = find_inherited_value(:references_to, EMPTY_HASH)
679
- if inherited_value.any?
688
+ if !inherited_value.empty?
680
689
  inherited_value.merge(own_references_to)
681
690
  else
682
691
  own_references_to
@@ -812,13 +821,13 @@ module GraphQL
812
821
 
813
822
  attr_writer :validate_timeout
814
823
 
815
- def validate_timeout(new_validate_timeout = nil)
816
- if new_validate_timeout
824
+ def validate_timeout(new_validate_timeout = NOT_CONFIGURED)
825
+ if !NOT_CONFIGURED.equal?(new_validate_timeout)
817
826
  @validate_timeout = new_validate_timeout
818
827
  elsif defined?(@validate_timeout)
819
828
  @validate_timeout
820
829
  else
821
- find_inherited_value(:validate_timeout)
830
+ find_inherited_value(:validate_timeout) || 3
822
831
  end
823
832
  end
824
833
 
@@ -962,7 +971,7 @@ module GraphQL
962
971
  # @param new_extra_types [Module] Type definitions to include in printing and introspection, even though they aren't referenced in the schema
963
972
  # @return [Array<Module>] Type definitions added to this schema
964
973
  def extra_types(*new_extra_types)
965
- if new_extra_types.any?
974
+ if !new_extra_types.empty?
966
975
  new_extra_types = new_extra_types.flatten
967
976
  @own_extra_types ||= []
968
977
  @own_extra_types.concat(new_extra_types)
@@ -987,10 +996,10 @@ module GraphQL
987
996
  # @param new_orphan_types [Array<Class<GraphQL::Schema::Object>>] Object types to register as implementations of interfaces in the schema.
988
997
  # @return [Array<Class<GraphQL::Schema::Object>>] All previously-registered orphan types for this schema
989
998
  def orphan_types(*new_orphan_types)
990
- if new_orphan_types.any?
999
+ if !new_orphan_types.empty?
991
1000
  new_orphan_types = new_orphan_types.flatten
992
1001
  non_object_types = new_orphan_types.reject { |ot| ot.is_a?(Class) && ot < GraphQL::Schema::Object }
993
- if non_object_types.any?
1002
+ if !non_object_types.empty?
994
1003
  raise ArgumentError, <<~ERR
995
1004
  Only object type classes should be added as `orphan_types(...)`.
996
1005
 
@@ -1007,7 +1016,7 @@ module GraphQL
1007
1016
 
1008
1017
  inherited_ot = find_inherited_value(:orphan_types, nil)
1009
1018
  if inherited_ot
1010
- if own_orphan_types.any?
1019
+ if !own_orphan_types.empty?
1011
1020
  inherited_ot + own_orphan_types
1012
1021
  else
1013
1022
  inherited_ot
@@ -1055,6 +1064,18 @@ module GraphQL
1055
1064
  end
1056
1065
  end
1057
1066
 
1067
+ # @param context [GraphQL::Query::Context, nil]
1068
+ # @return [Logger] A logger to use for this context configuration, falling back to {.default_logger}
1069
+ def logger_for(context)
1070
+ if context && context[:logger] == false
1071
+ Logger.new(IO::NULL)
1072
+ elsif context && (l = context[:logger])
1073
+ l
1074
+ else
1075
+ default_logger
1076
+ end
1077
+ end
1078
+
1058
1079
  # @param new_context_class [Class<GraphQL::Query::Context>] A subclass to use when executing queries
1059
1080
  def context_class(new_context_class = nil)
1060
1081
  if new_context_class
@@ -1100,6 +1121,9 @@ module GraphQL
1100
1121
  }
1101
1122
  end
1102
1123
 
1124
+ # @api private
1125
+ attr_accessor :using_backtrace
1126
+
1103
1127
  # @api private
1104
1128
  def handle_or_reraise(context, err)
1105
1129
  handler = Execution::Errors.find_handler_for(self, err.class)
@@ -1113,6 +1137,10 @@ module GraphQL
1113
1137
  end
1114
1138
  handler[:handler].call(err, obj, args, context, field)
1115
1139
  else
1140
+ if (context[:backtrace] || using_backtrace) && !err.is_a?(GraphQL::ExecutionError)
1141
+ err = GraphQL::Backtrace::TracedError.new(err, context)
1142
+ end
1143
+
1116
1144
  raise err
1117
1145
  end
1118
1146
  end
@@ -1147,7 +1175,7 @@ module GraphQL
1147
1175
  # GraphQL-Ruby calls this method during execution when it needs the application to determine the type to use for an object.
1148
1176
  #
1149
1177
  # Usually, this object was returned from a field whose return type is an {GraphQL::Schema::Interface} or a {GraphQL::Schema::Union}.
1150
- # But this method is called in other cases, too -- for example, when {GraphQL::Schema::Argument.loads} cases an object to be directly loaded from the database.
1178
+ # But this method is called in other cases, too -- for example, when {GraphQL::Schema::Argument#loads} cases an object to be directly loaded from the database.
1151
1179
  #
1152
1180
  # @example Returning a GraphQL type based on the object's class name
1153
1181
  # class MySchema < GraphQL::Schema
@@ -1202,7 +1230,7 @@ module GraphQL
1202
1230
 
1203
1231
  # Return a stable ID string for `object` so that it can be refetched later, using {.object_from_id}.
1204
1232
  #
1205
- # {GlobalID}(https://github.com/rails/globalid) and {SQIDs}(https://sqids.org/ruby) can both be used to create IDs.
1233
+ # [GlobalID](https://github.com/rails/globalid) and [SQIDs](https://sqids.org/ruby) can both be used to create IDs.
1206
1234
  #
1207
1235
  # @example Using Rails's GlobalID to generate IDs
1208
1236
  # def self.id_from_object(application_object, graphql_type, context)
@@ -1279,10 +1307,13 @@ module GraphQL
1279
1307
  # @return [void]
1280
1308
  # @raise [GraphQL::ExecutionError] to return this error to the client
1281
1309
  # @raise [GraphQL::Error] to crash the query and raise a developer-facing error
1282
- def type_error(type_error, ctx)
1310
+ def type_error(type_error, context)
1283
1311
  case type_error
1284
1312
  when GraphQL::InvalidNullError
1285
- ctx.errors << type_error
1313
+ execution_error = GraphQL::ExecutionError.new(type_error.message, ast_node: type_error.ast_node)
1314
+ execution_error.path = context[:current_path]
1315
+
1316
+ context.errors << execution_error
1286
1317
  when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
1287
1318
  raise type_error
1288
1319
  when GraphQL::IntegerDecodingError
@@ -1290,7 +1321,7 @@ module GraphQL
1290
1321
  end
1291
1322
  end
1292
1323
 
1293
- # A function to call when {#execute} receives an invalid query string
1324
+ # A function to call when {.execute} receives an invalid query string
1294
1325
  #
1295
1326
  # The default is to add the error to `context.errors`
1296
1327
  # @param parse_err [GraphQL::ParseError] The error encountered during parsing
@@ -1317,12 +1348,12 @@ module GraphQL
1317
1348
  # Add several directives at once
1318
1349
  # @param new_directives [Class]
1319
1350
  def directives(*new_directives)
1320
- if new_directives.any?
1351
+ if !new_directives.empty?
1321
1352
  new_directives.flatten.each { |d| directive(d) }
1322
1353
  end
1323
1354
 
1324
1355
  inherited_dirs = find_inherited_value(:directives, default_directives)
1325
- if own_directives.any?
1356
+ if !own_directives.empty?
1326
1357
  inherited_dirs.merge(own_directives)
1327
1358
  else
1328
1359
  inherited_dirs
@@ -1350,6 +1381,16 @@ module GraphQL
1350
1381
  }.freeze
1351
1382
  end
1352
1383
 
1384
+ # @return [GraphQL::Tracing::DetailedTrace] if it has been configured for this schema
1385
+ attr_accessor :detailed_trace
1386
+
1387
+ # @param query [GraphQL::Query, GraphQL::Execution::Multiplex] Called with a multiplex when multiple queries are executed at once (with {.multiplex})
1388
+ # @return [Boolean] When `true`, save a detailed trace for this query.
1389
+ # @see Tracing::DetailedTrace DetailedTrace saves traces when this method returns true
1390
+ def detailed_trace?(query)
1391
+ raise "#{self} must implement `def.detailed_trace?(query)` to use DetailedTrace. Implement this method in your schema definition."
1392
+ end
1393
+
1353
1394
  def tracer(new_tracer, silence_deprecation_warning: false)
1354
1395
  if !silence_deprecation_warning
1355
1396
  warn("`Schema.tracer(#{new_tracer.inspect})` is deprecated; use module-based `trace_with` instead. See: https://graphql-ruby.org/queries/tracing.html")
@@ -1367,14 +1408,22 @@ module GraphQL
1367
1408
  find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers
1368
1409
  end
1369
1410
 
1370
- # Mix `trace_mod` into this schema's `Trace` class so that its methods
1371
- # will be called at runtime.
1411
+ # Mix `trace_mod` into this schema's `Trace` class so that its methods will be called at runtime.
1412
+ #
1413
+ # You can attach a module to run in only _some_ circumstances by using `mode:`. When a module is added with `mode:`,
1414
+ # it will only run for queries with a matching `context[:trace_mode]`.
1415
+ #
1416
+ # Any custom trace modes _also_ include the default `trace_with ...` modules (that is, those added _without_ any particular `mode: ...` configuration).
1417
+ #
1418
+ # @example Adding a trace in a special mode
1419
+ # # only runs when `query.context[:trace_mode]` is `:special`
1420
+ # trace_with SpecialTrace, mode: :special
1372
1421
  #
1373
1422
  # @param trace_mod [Module] A module that implements tracing methods
1374
1423
  # @param mode [Symbol] Trace module will only be used for this trade mode
1375
1424
  # @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
1376
1425
  # @return [void]
1377
- # @see GraphQL::Tracing::Trace for available tracing methods
1426
+ # @see GraphQL::Tracing::Trace Tracing::Trace for available tracing methods
1378
1427
  def trace_with(trace_mod, mode: :default, **options)
1379
1428
  if mode.is_a?(Array)
1380
1429
  mode.each { |m| trace_with(trace_mod, mode: m, **options) }
@@ -1424,29 +1473,36 @@ module GraphQL
1424
1473
  #
1425
1474
  # If no `mode:` is given, then {default_trace_mode} will be used.
1426
1475
  #
1476
+ # If this schema is using {Tracing::DetailedTrace} and {.detailed_trace?} returns `true`, then
1477
+ # DetailedTrace's mode will override the passed-in `mode`.
1478
+ #
1427
1479
  # @param mode [Symbol] Trace modules for this trade mode will be included
1428
1480
  # @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
1429
1481
  # @return [Tracing::Trace]
1430
1482
  def new_trace(mode: nil, **options)
1431
- target = options[:query] || options[:multiplex]
1432
- mode ||= target && target.context[:trace_mode]
1433
-
1434
- trace_mode = if mode
1435
- mode
1436
- elsif target && target.context[:backtrace]
1437
- if default_trace_mode != :default
1438
- raise ArgumentError, "Can't use `context[:backtrace]` with a custom default trace mode (`#{dm.inspect}`)"
1439
- else
1440
- own_trace_modes[:default_backtrace] ||= build_trace_mode(:default_backtrace)
1441
- options_trace_mode = :default
1442
- :default_backtrace
1483
+ should_sample = if detailed_trace
1484
+ if (query = options[:query])
1485
+ detailed_trace?(query)
1486
+ elsif (multiplex = options[:multiplex])
1487
+ if multiplex.queries.length == 1
1488
+ detailed_trace?(multiplex.queries.first)
1489
+ else
1490
+ detailed_trace?(multiplex)
1491
+ end
1443
1492
  end
1444
1493
  else
1445
- default_trace_mode
1494
+ false
1446
1495
  end
1447
1496
 
1448
- options_trace_mode ||= trace_mode
1449
- base_trace_options = trace_options_for(options_trace_mode)
1497
+ if should_sample
1498
+ mode = detailed_trace.trace_mode
1499
+ else
1500
+ target = options[:query] || options[:multiplex]
1501
+ mode ||= target && target.context[:trace_mode]
1502
+ end
1503
+
1504
+ trace_mode = mode || default_trace_mode
1505
+ base_trace_options = trace_options_for(trace_mode)
1450
1506
  trace_options = base_trace_options.merge(options)
1451
1507
  trace_class_for_mode = trace_class_for(trace_mode, build: true)
1452
1508
  trace_class_for_mode.new(**trace_options)
@@ -1521,7 +1577,8 @@ module GraphQL
1521
1577
  # @see {Query#initialize} for query keyword arguments
1522
1578
  # @see {Execution::Multiplex#run_all} for multiplex keyword arguments
1523
1579
  # @param queries [Array<Hash>] Keyword arguments for each query
1524
- # @param context [Hash] Multiplex-level context
1580
+ # @option kwargs [Hash] :context ({}) Multiplex-level context
1581
+ # @option kwargs [nil, Integer] :max_complexity (nil)
1525
1582
  # @return [Array<GraphQL::Query::Result>] One result for each query in the input
1526
1583
  def multiplex(queries, **kwargs)
1527
1584
  GraphQL::Execution::Interpreter.run_all(self, queries, **kwargs)
@@ -1586,7 +1643,7 @@ module GraphQL
1586
1643
  end
1587
1644
  end
1588
1645
 
1589
- # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {#lazy_resolve}.
1646
+ # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {.lazy_resolve}.
1590
1647
  def lazy_method_name(obj)
1591
1648
  lazy_methods.get(obj)
1592
1649
  end
@@ -1624,6 +1681,158 @@ module GraphQL
1624
1681
  end
1625
1682
  end
1626
1683
 
1684
+
1685
+ # This setting controls how GraphQL-Ruby handles empty selections on Union types.
1686
+ #
1687
+ # To opt into future, spec-compliant behavior where these selections are rejected, set this to `false`.
1688
+ #
1689
+ # If you need to support previous, non-spec behavior which allowed selecting union fields
1690
+ # but *not* selecting any fields on that union, set this to `true` to continue allowing that behavior.
1691
+ #
1692
+ # If this is `true`, then {.legacy_invalid_empty_selections_on_union} will be called with {Query} objects
1693
+ # with that kind of selections. You must implement that method
1694
+ # @param new_value [Boolean]
1695
+ # @return [true, false, nil]
1696
+ def allow_legacy_invalid_empty_selections_on_union(new_value = NOT_CONFIGURED)
1697
+ if NOT_CONFIGURED.equal?(new_value)
1698
+ @allow_legacy_invalid_empty_selections_on_union
1699
+ else
1700
+ @allow_legacy_invalid_empty_selections_on_union = new_value
1701
+ end
1702
+ end
1703
+
1704
+ # This method is called during validation when a previously-allowed, but non-spec
1705
+ # query is encountered where a union field has no child selections on it.
1706
+ #
1707
+ # You should implement this method to log the violation so that you can contact clients
1708
+ # and notify them about changing their queries. Then return a suitable value to
1709
+ # tell GraphQL-Ruby how to continue.
1710
+ # @param query [GraphQL::Query]
1711
+ # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
1712
+ # @return [String] A validation error to return for this query
1713
+ # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
1714
+ def legacy_invalid_empty_selections_on_union(query)
1715
+ raise "Implement `def self.legacy_invalid_empty_selections_on_union(query)` to handle this scenario"
1716
+ end
1717
+
1718
+ # This setting controls how GraphQL-Ruby handles overlapping selections on scalar types when the types
1719
+ # don't match.
1720
+ #
1721
+ # When set to `false`, GraphQL-Ruby will reject those queries with a validation error (as per the GraphQL spec).
1722
+ #
1723
+ # When set to `true`, GraphQL-Ruby will call {.legacy_invalid_return_type_conflicts} when the scenario is encountered.
1724
+ #
1725
+ # @param new_value [Boolean] `true` permits the legacy behavior, `false` rejects it.
1726
+ # @return [true, false, nil]
1727
+ def allow_legacy_invalid_return_type_conflicts(new_value = NOT_CONFIGURED)
1728
+ if NOT_CONFIGURED.equal?(new_value)
1729
+ @allow_legacy_invalid_return_type_conflicts
1730
+ else
1731
+ @allow_legacy_invalid_return_type_conflicts = new_value
1732
+ end
1733
+ end
1734
+
1735
+ # This method is called when the query contains fields which don't contain matching scalar types.
1736
+ # This was previously allowed by GraphQL-Ruby but it's a violation of the GraphQL spec.
1737
+ #
1738
+ # You should implement this method to log the violation so that you observe usage of these fields.
1739
+ # Fixing this scenario might mean adding new fields, and telling clients to use those fields.
1740
+ # (Changing the field return type would be a breaking change, but if it works for your client use cases,
1741
+ # that might work, too.)
1742
+ #
1743
+ # @param query [GraphQL::Query]
1744
+ # @param type1 [Module] A GraphQL type definition
1745
+ # @param type2 [Module] A GraphQL type definition
1746
+ # @param node1 [GraphQL::Language::Nodes::Field] This node is recognized as conflicting. You might call `.line` and `.col` for custom error reporting.
1747
+ # @param node2 [GraphQL::Language::Nodes::Field] The other node recognized as conflicting.
1748
+ # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
1749
+ # @return [String] A validation error to return for this query
1750
+ # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
1751
+ def legacy_invalid_return_type_conflicts(query, type1, type2, node1, node2)
1752
+ raise "Implement #{self}.legacy_invalid_return_type_conflicts to handle this invalid selection"
1753
+ end
1754
+
1755
+ # The legacy complexity implementation included several bugs:
1756
+ #
1757
+ # - In some cases, it used the lexically _last_ field to determine a cost, instead of calculating the maximum among selections
1758
+ # - In some cases, it called field complexity hooks repeatedly (when it should have only called them once)
1759
+ #
1760
+ # The future implementation may produce higher total complexity scores, so it's not active by default yet. You can opt into
1761
+ # the future default behavior by configuring `:future` here. Or, you can choose a mode for each query with {.complexity_cost_calculation_mode_for}.
1762
+ #
1763
+ # The legacy mode is currently maintained alongside the future one, but it will be removed in a future GraphQL-Ruby version.
1764
+ #
1765
+ # If you choose `:compare`, you must also implement {.legacy_complexity_cost_calculation_mismatch} to handle the input somehow.
1766
+ #
1767
+ # @example Opting into the future calculation mode
1768
+ # complexity_cost_calculation_mode(:future)
1769
+ #
1770
+ # @example Choosing the legacy mode (which will work until that mode is removed...)
1771
+ # complexity_cost_calculation_mode(:legacy)
1772
+ #
1773
+ # @example Run both modes for every query, call {.legacy_complexity_cost_calculation_mismatch} when they don't match:
1774
+ # complexity_cost_calculation_mode(:compare)
1775
+ def complexity_cost_calculation_mode(new_mode = NOT_CONFIGURED)
1776
+ if NOT_CONFIGURED.equal?(new_mode)
1777
+ @complexity_cost_calculation_mode
1778
+ else
1779
+ @complexity_cost_calculation_mode = new_mode
1780
+ end
1781
+ end
1782
+
1783
+ # Implement this method to produce a per-query complexity cost calculation mode. (Technically, it's per-multiplex.)
1784
+ #
1785
+ # This is a way to check the compatibility of queries coming to your API without adding overhead of running `:compare`
1786
+ # for every query. You could sample traffic, turn it off/on with feature flags, or anything else.
1787
+ #
1788
+ # @example Sampling traffic
1789
+ # def self.complexity_cost_calculation_mode_for(_context)
1790
+ # if rand < 0.1 # 10% of the time
1791
+ # :compare
1792
+ # else
1793
+ # :legacy
1794
+ # end
1795
+ # end
1796
+ #
1797
+ # @example Using a feature flag to manage future mode
1798
+ # def complexity_cost_calculation_mode_for(context)
1799
+ # current_user = context[:current_user]
1800
+ # if Flipper.enabled?(:future_complexity_cost, current_user)
1801
+ # :future
1802
+ # elsif rand < 0.5 # 50%
1803
+ # :compare
1804
+ # else
1805
+ # :legacy
1806
+ # end
1807
+ # end
1808
+ #
1809
+ # @param multiplex_context [Hash] The context for the currently-running {Execution::Multiplex} (which contains one or more queries)
1810
+ # @return [:future] Use the new calculation algorithm -- may be higher than `:legacy`
1811
+ # @return [:legacy] Use the legacy calculation algorithm, warts and all
1812
+ # @return [:compare] Run both algorithms and call {.legacy_complexity_cost_calculation_mismatch} if they don't match
1813
+ def complexity_cost_calculation_mode_for(multiplex_context)
1814
+ complexity_cost_calculation_mode
1815
+ end
1816
+
1817
+ # Implement this method in your schema to handle mismatches when `:compare` is used.
1818
+ #
1819
+ # @example Logging the mismatch
1820
+ # def self.legacy_cost_calculation_mismatch(multiplex, future_cost, legacy_cost)
1821
+ # client_id = multiplex.context[:api_client].id
1822
+ # operation_names = multiplex.queries.map { |q| q.selected_operation_name || "anonymous" }.join(", ")
1823
+ # Stats.increment(:complexity_mismatch, tags: { client: client_id, ops: operation_names })
1824
+ # legacy_cost
1825
+ # end
1826
+ # @see Query::Context#add_error Adding an error to the response to notify the client
1827
+ # @see Query::Context#response_extensions Adding key-value pairs to the response `"extensions" => { ... }`
1828
+ # @param multiplex [GraphQL::Execution::Multiplex]
1829
+ # @param future_complexity_cost [Integer]
1830
+ # @param legacy_complexity_cost [Integer]
1831
+ # @return [Integer] the cost to use for this query (probably one of `future_complexity_cost` or `legacy_complexity_cost`)
1832
+ def legacy_complexity_cost_calculation_mismatch(multiplex, future_complexity_cost, legacy_complexity_cost)
1833
+ raise "Implement #{self}.legacy_complexity_cost(multiplex, future_complexity_cost, legacy_complexity_cost) to handle this mismatch (#{future_complexity_cost} vs. #{legacy_complexity_cost}) and return a value to use"
1834
+ end
1835
+
1627
1836
  private
1628
1837
 
1629
1838
  def add_trace_options_for(mode, new_options)
@@ -1787,3 +1996,6 @@ module GraphQL
1787
1996
  end
1788
1997
  end
1789
1998
  end
1999
+
2000
+ require "graphql/schema/loader"
2001
+ require "graphql/schema/printer"
@@ -34,7 +34,7 @@ module GraphQL
34
34
  GraphQL::StaticValidation::VariableUsagesAreAllowed,
35
35
  GraphQL::StaticValidation::MutationRootExists,
36
36
  GraphQL::StaticValidation::QueryRootExists,
37
- GraphQL::StaticValidation::SubscriptionRootExists,
37
+ GraphQL::StaticValidation::SubscriptionRootExistsAndSingleSubscriptionSelection,
38
38
  GraphQL::StaticValidation::InputObjectNamesAreUnique,
39
39
  GraphQL::StaticValidation::OneOfInputObjectsAreValid,
40
40
  ]
@@ -16,7 +16,7 @@ module GraphQL
16
16
 
17
17
  def validate_arguments(node)
18
18
  argument_defns = node.arguments
19
- if argument_defns.any?
19
+ if !argument_defns.empty?
20
20
  args_by_name = Hash.new { |h, k| h[k] = [] }
21
21
  argument_defns.each { |a| args_by_name[a.name] << a }
22
22
  args_by_name.each do |name, defns|
@@ -25,22 +25,56 @@ module GraphQL
25
25
  def validate_field_selections(ast_node, resolved_type)
26
26
  msg = if resolved_type.nil?
27
27
  nil
28
- elsif ast_node.selections.any? && resolved_type.kind.leaf?
29
- selection_strs = ast_node.selections.map do |n|
30
- case n
31
- when GraphQL::Language::Nodes::InlineFragment
32
- "\"... on #{n.type.name} { ... }\""
33
- when GraphQL::Language::Nodes::Field
34
- "\"#{n.name}\""
35
- when GraphQL::Language::Nodes::FragmentSpread
36
- "\"#{n.name}\""
28
+ elsif resolved_type.kind.leaf?
29
+ if !ast_node.selections.empty?
30
+ selection_strs = ast_node.selections.map do |n|
31
+ case n
32
+ when GraphQL::Language::Nodes::InlineFragment
33
+ "\"... on #{n.type.name} { ... }\""
34
+ when GraphQL::Language::Nodes::Field
35
+ "\"#{n.name}\""
36
+ when GraphQL::Language::Nodes::FragmentSpread
37
+ "\"#{n.name}\""
38
+ else
39
+ raise "Invariant: unexpected selection node: #{n}"
40
+ end
41
+ end
42
+ "Selections can't be made on #{resolved_type.kind.name.sub("_", " ").downcase}s (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
43
+ else
44
+ nil
45
+ end
46
+ elsif ast_node.selections.empty?
47
+ return_validation_error = true
48
+ legacy_invalid_empty_selection_result = nil
49
+ if !resolved_type.kind.fields?
50
+ case @schema.allow_legacy_invalid_empty_selections_on_union
51
+ when true
52
+ legacy_invalid_empty_selection_result = @schema.legacy_invalid_empty_selections_on_union(@context.query)
53
+ case legacy_invalid_empty_selection_result
54
+ when :return_validation_error
55
+ # keep `return_validation_error = true`
56
+ when String
57
+ return_validation_error = false
58
+ # the string is returned below
59
+ when nil
60
+ # No error:
61
+ return_validation_error = false
62
+ legacy_invalid_empty_selection_result = nil
63
+ else
64
+ raise GraphQL::InvariantError, "Unexpected return value from legacy_invalid_empty_selections_on_union, must be `:return_validation_error`, String, or nil (got: #{legacy_invalid_empty_selection_result.inspect})"
65
+ end
66
+ when false
67
+ # pass -- error below
37
68
  else
38
- raise "Invariant: unexpected selection node: #{n}"
69
+ return_validation_error = false
70
+ @context.query.logger.warn("Unions require selections but #{ast_node.alias || ast_node.name} (#{resolved_type.graphql_name}) doesn't have any. This will fail with a validation error on a future GraphQL-Ruby version. More info: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_empty_selections_on_union-class_method")
39
71
  end
40
72
  end
41
- "Selections can't be made on #{resolved_type.kind.name.sub("_", " ").downcase}s (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
42
- elsif resolved_type.kind.fields? && ast_node.selections.empty?
43
- "Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)"
73
+ if return_validation_error
74
+ "Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)"
75
+ else
76
+ legacy_invalid_empty_selection_result
77
+ end
44
78
  else
45
79
  nil
46
80
  end