graphql 2.4.13 → 2.5.11

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/query_complexity.rb +87 -7
  3. data/lib/graphql/backtrace/table.rb +37 -14
  4. data/lib/graphql/current.rb +1 -1
  5. data/lib/graphql/dashboard/detailed_traces.rb +47 -0
  6. data/lib/graphql/dashboard/installable.rb +22 -0
  7. data/lib/graphql/dashboard/limiters.rb +93 -0
  8. data/lib/graphql/dashboard/operation_store.rb +199 -0
  9. data/lib/graphql/dashboard/statics/charts.min.css +1 -0
  10. data/lib/graphql/dashboard/statics/dashboard.css +27 -0
  11. data/lib/graphql/dashboard/statics/dashboard.js +74 -9
  12. data/lib/graphql/dashboard/subscriptions.rb +96 -0
  13. data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
  14. data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
  15. data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
  16. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
  17. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
  18. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
  19. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
  20. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
  21. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
  22. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
  23. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
  24. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
  25. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
  26. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
  27. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +49 -1
  28. data/lib/graphql/dashboard.rb +45 -29
  29. data/lib/graphql/dataloader/active_record_association_source.rb +28 -8
  30. data/lib/graphql/dataloader/active_record_source.rb +26 -5
  31. data/lib/graphql/dataloader/null_dataloader.rb +7 -0
  32. data/lib/graphql/dataloader/source.rb +16 -4
  33. data/lib/graphql/dig.rb +2 -1
  34. data/lib/graphql/execution/interpreter/resolve.rb +3 -3
  35. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +34 -1
  36. data/lib/graphql/execution/interpreter/runtime.rb +163 -59
  37. data/lib/graphql/execution/interpreter.rb +5 -13
  38. data/lib/graphql/execution/multiplex.rb +6 -1
  39. data/lib/graphql/invalid_null_error.rb +15 -2
  40. data/lib/graphql/language/lexer.rb +9 -2
  41. data/lib/graphql/language/nodes.rb +5 -1
  42. data/lib/graphql/language/parser.rb +14 -6
  43. data/lib/graphql/query/context.rb +3 -8
  44. data/lib/graphql/query/partial.rb +179 -0
  45. data/lib/graphql/query.rb +59 -55
  46. data/lib/graphql/schema/addition.rb +3 -1
  47. data/lib/graphql/schema/always_visible.rb +1 -0
  48. data/lib/graphql/schema/argument.rb +9 -3
  49. data/lib/graphql/schema/build_from_definition.rb +96 -47
  50. data/lib/graphql/schema/directive/flagged.rb +2 -0
  51. data/lib/graphql/schema/directive.rb +33 -1
  52. data/lib/graphql/schema/field.rb +23 -1
  53. data/lib/graphql/schema/input_object.rb +38 -30
  54. data/lib/graphql/schema/list.rb +1 -1
  55. data/lib/graphql/schema/member/has_arguments.rb +2 -2
  56. data/lib/graphql/schema/member/has_dataloader.rb +4 -2
  57. data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
  58. data/lib/graphql/schema/member/has_interfaces.rb +2 -2
  59. data/lib/graphql/schema/member/type_system_helpers.rb +16 -2
  60. data/lib/graphql/schema/ractor_shareable.rb +79 -0
  61. data/lib/graphql/schema/resolver.rb +1 -0
  62. data/lib/graphql/schema/scalar.rb +1 -6
  63. data/lib/graphql/schema/timeout.rb +19 -2
  64. data/lib/graphql/schema/validator/required_validator.rb +15 -6
  65. data/lib/graphql/schema/visibility/migration.rb +2 -2
  66. data/lib/graphql/schema/visibility/profile.rb +107 -21
  67. data/lib/graphql/schema/visibility.rb +41 -29
  68. data/lib/graphql/schema/warden.rb +13 -5
  69. data/lib/graphql/schema.rb +228 -32
  70. data/lib/graphql/static_validation/all_rules.rb +2 -2
  71. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
  72. data/lib/graphql/static_validation/rules/fields_will_merge.rb +78 -16
  73. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
  74. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
  75. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
  76. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
  77. data/lib/graphql/testing/helpers.rb +5 -2
  78. data/lib/graphql/tracing/active_support_notifications_trace.rb +7 -0
  79. data/lib/graphql/tracing/appoptics_tracing.rb +5 -0
  80. data/lib/graphql/tracing/appsignal_trace.rb +26 -61
  81. data/lib/graphql/tracing/data_dog_trace.rb +41 -164
  82. data/lib/graphql/tracing/monitor_trace.rb +283 -0
  83. data/lib/graphql/tracing/new_relic_trace.rb +34 -164
  84. data/lib/graphql/tracing/notifications_trace.rb +183 -37
  85. data/lib/graphql/tracing/null_trace.rb +1 -1
  86. data/lib/graphql/tracing/perfetto_trace.rb +16 -19
  87. data/lib/graphql/tracing/prometheus_trace.rb +47 -74
  88. data/lib/graphql/tracing/scout_trace.rb +25 -59
  89. data/lib/graphql/tracing/sentry_trace.rb +56 -99
  90. data/lib/graphql/tracing/statsd_trace.rb +24 -47
  91. data/lib/graphql/tracing/trace.rb +0 -17
  92. data/lib/graphql/tracing.rb +1 -0
  93. data/lib/graphql/type_kinds.rb +1 -0
  94. data/lib/graphql/version.rb +1 -1
  95. data/lib/graphql.rb +1 -1
  96. metadata +35 -26
  97. data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +0 -63
  98. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
@@ -7,6 +7,7 @@ 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/ractor_shareable"
10
11
  require "graphql/schema/timeout"
11
12
  require "graphql/schema/type_expression"
12
13
  require "graphql/schema/unique_within_type"
@@ -60,7 +61,7 @@ module GraphQL
60
61
  # Any undiscoverable types may be provided with the `types` configuration.
61
62
  #
62
63
  # Schemas can restrict large incoming queries with `max_depth` and `max_complexity` configurations.
63
- # (These configurations can be overridden by specific calls to {Schema#execute})
64
+ # (These configurations can be overridden by specific calls to {Schema.execute})
64
65
  #
65
66
  # @example defining a schema
66
67
  # class MySchema < GraphQL::Schema
@@ -111,7 +112,7 @@ module GraphQL
111
112
  # @param parser [Object] An object for handling definition string parsing (must respond to `parse`)
112
113
  # @param using [Hash] Plugins to attach to the created schema with `use(key, value)`
113
114
  # @return [Class] the schema described by `document`
114
- def from_definition(definition_or_path, default_resolve: nil, parser: GraphQL.default_parser, using: {})
115
+ def from_definition(definition_or_path, default_resolve: nil, parser: GraphQL.default_parser, using: {}, base_types: {})
115
116
  # If the file ends in `.graphql` or `.graphqls`, treat it like a filepath
116
117
  if definition_or_path.end_with?(".graphql") || definition_or_path.end_with?(".graphqls")
117
118
  GraphQL::Schema::BuildFromDefinition.from_definition_path(
@@ -120,6 +121,7 @@ module GraphQL
120
121
  default_resolve: default_resolve,
121
122
  parser: parser,
122
123
  using: using,
124
+ base_types: base_types,
123
125
  )
124
126
  else
125
127
  GraphQL::Schema::BuildFromDefinition.from_definition(
@@ -128,6 +130,7 @@ module GraphQL
128
130
  default_resolve: default_resolve,
129
131
  parser: parser,
130
132
  using: using,
133
+ base_types: base_types,
131
134
  )
132
135
  end
133
136
  end
@@ -146,10 +149,12 @@ module GraphQL
146
149
  end
147
150
 
148
151
  # @param new_mode [Symbol] If configured, this will be used when `context: { trace_mode: ... }` isn't set.
149
- def default_trace_mode(new_mode = nil)
150
- if new_mode
152
+ def default_trace_mode(new_mode = NOT_CONFIGURED)
153
+ if !NOT_CONFIGURED.equal?(new_mode)
151
154
  @default_trace_mode = new_mode
152
- elsif defined?(@default_trace_mode)
155
+ elsif defined?(@default_trace_mode) &&
156
+ !@default_trace_mode.nil? # This `nil?` check seems necessary because of
157
+ # Ractors silently initializing @default_trace_mode somehow
153
158
  @default_trace_mode
154
159
  elsif superclass.respond_to?(:default_trace_mode)
155
160
  superclass.default_trace_mode
@@ -247,7 +252,7 @@ module GraphQL
247
252
 
248
253
 
249
254
  # Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
250
- # @see {#as_json}
255
+ # @see #as_json Return a Hash representation of the schema
251
256
  # @return [String]
252
257
  def to_json(**args)
253
258
  JSON.pretty_generate(as_json(**args))
@@ -255,8 +260,6 @@ module GraphQL
255
260
 
256
261
  # Return the Hash response of {Introspection::INTROSPECTION_QUERY}.
257
262
  # @param context [Hash]
258
- # @param only [<#call(member, ctx)>]
259
- # @param except [<#call(member, ctx)>]
260
263
  # @param include_deprecated_args [Boolean] If true, deprecated arguments will be included in the JSON response
261
264
  # @param include_schema_description [Boolean] If true, the schema's description will be queried and included in the response
262
265
  # @param include_is_repeatable [Boolean] If true, `isRepeatable: true|false` will be included with the schema's directives
@@ -365,7 +368,8 @@ module GraphQL
365
368
  # @return [Module, nil] A type, or nil if there's no type called `type_name`
366
369
  def get_type(type_name, context = GraphQL::Query::NullContext.instance, use_visibility_profile = use_visibility_profile?)
367
370
  if use_visibility_profile
368
- return Visibility::Profile.from_context(context, self).type(type_name)
371
+ profile = Visibility::Profile.from_context(context, self)
372
+ return profile.type(type_name)
369
373
  end
370
374
  local_entry = own_types[type_name]
371
375
  type_defn = case local_entry
@@ -697,7 +701,21 @@ module GraphQL
697
701
  GraphQL::Schema::TypeExpression.build_type(context.query.types, ast_node)
698
702
  end
699
703
 
700
- def get_field(type_or_name, field_name, context = GraphQL::Query::NullContext.instance)
704
+ def get_field(type_or_name, field_name, context = GraphQL::Query::NullContext.instance, use_visibility_profile = use_visibility_profile?)
705
+ if use_visibility_profile
706
+ profile = Visibility::Profile.from_context(context, self)
707
+ parent_type = case type_or_name
708
+ when String
709
+ profile.type(type_or_name)
710
+ when Module
711
+ type_or_name
712
+ when LateBoundType
713
+ profile.type(type_or_name.name)
714
+ else
715
+ raise GraphQL::InvariantError, "Unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})"
716
+ end
717
+ return profile.field(parent_type, field_name)
718
+ end
701
719
  parent_type = case type_or_name
702
720
  when LateBoundType
703
721
  get_type(type_or_name.name, context)
@@ -1064,6 +1082,18 @@ module GraphQL
1064
1082
  end
1065
1083
  end
1066
1084
 
1085
+ # @param context [GraphQL::Query::Context, nil]
1086
+ # @return [Logger] A logger to use for this context configuration, falling back to {.default_logger}
1087
+ def logger_for(context)
1088
+ if context && context[:logger] == false
1089
+ Logger.new(IO::NULL)
1090
+ elsif context && (l = context[:logger])
1091
+ l
1092
+ else
1093
+ default_logger
1094
+ end
1095
+ end
1096
+
1067
1097
  # @param new_context_class [Class<GraphQL::Query::Context>] A subclass to use when executing queries
1068
1098
  def context_class(new_context_class = nil)
1069
1099
  if new_context_class
@@ -1093,20 +1123,21 @@ module GraphQL
1093
1123
  end
1094
1124
  end
1095
1125
 
1096
- NEW_HANDLER_HASH = ->(h, k) {
1097
- h[k] = {
1098
- class: k,
1099
- handler: nil,
1100
- subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
1101
- }
1102
- }
1103
-
1104
1126
  def error_handlers
1105
- @error_handlers ||= {
1106
- class: nil,
1107
- handler: nil,
1108
- subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
1109
- }
1127
+ @error_handlers ||= begin
1128
+ new_handler_hash = ->(h, k) {
1129
+ h[k] = {
1130
+ class: k,
1131
+ handler: nil,
1132
+ subclass_handlers: Hash.new(&new_handler_hash),
1133
+ }
1134
+ }
1135
+ {
1136
+ class: nil,
1137
+ handler: nil,
1138
+ subclass_handlers: Hash.new(&new_handler_hash),
1139
+ }
1140
+ end
1110
1141
  end
1111
1142
 
1112
1143
  # @api private
@@ -1163,7 +1194,7 @@ module GraphQL
1163
1194
  # GraphQL-Ruby calls this method during execution when it needs the application to determine the type to use for an object.
1164
1195
  #
1165
1196
  # Usually, this object was returned from a field whose return type is an {GraphQL::Schema::Interface} or a {GraphQL::Schema::Union}.
1166
- # 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.
1197
+ # 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.
1167
1198
  #
1168
1199
  # @example Returning a GraphQL type based on the object's class name
1169
1200
  # class MySchema < GraphQL::Schema
@@ -1177,7 +1208,7 @@ module GraphQL
1177
1208
  # @param context [GraphQL::Query::Context] The query context for the currently-executing query
1178
1209
  # @return [Class<GraphQL::Schema::Object] The Object type definition to use for `obj`
1179
1210
  def resolve_type(abstract_type, application_object, context)
1180
- raise GraphQL::RequiredImplementationMissingError, "#{self.name}.resolve_type(abstract_type, application_object, context) must be implemented to use Union types, Interface types, or `loads:` (tried to resolve: #{abstract_type.name})"
1211
+ raise GraphQL::RequiredImplementationMissingError, "#{self.name}.resolve_type(abstract_type, application_object, context) must be implemented to use Union types, Interface types, `loads:`, or `run_partials` (tried to resolve: #{abstract_type.name})"
1181
1212
  end
1182
1213
  # rubocop:enable Lint/DuplicateMethods
1183
1214
 
@@ -1218,7 +1249,7 @@ module GraphQL
1218
1249
 
1219
1250
  # Return a stable ID string for `object` so that it can be refetched later, using {.object_from_id}.
1220
1251
  #
1221
- # {GlobalID}(https://github.com/rails/globalid) and {SQIDs}(https://sqids.org/ruby) can both be used to create IDs.
1252
+ # [GlobalID](https://github.com/rails/globalid) and [SQIDs](https://sqids.org/ruby) can both be used to create IDs.
1222
1253
  #
1223
1254
  # @example Using Rails's GlobalID to generate IDs
1224
1255
  # def self.id_from_object(application_object, graphql_type, context)
@@ -1295,13 +1326,13 @@ module GraphQL
1295
1326
  # @return [void]
1296
1327
  # @raise [GraphQL::ExecutionError] to return this error to the client
1297
1328
  # @raise [GraphQL::Error] to crash the query and raise a developer-facing error
1298
- def type_error(type_error, ctx)
1329
+ def type_error(type_error, context)
1299
1330
  case type_error
1300
1331
  when GraphQL::InvalidNullError
1301
1332
  execution_error = GraphQL::ExecutionError.new(type_error.message, ast_node: type_error.ast_node)
1302
- execution_error.path = ctx[:current_path]
1333
+ execution_error.path = context[:current_path]
1303
1334
 
1304
- ctx.errors << execution_error
1335
+ context.errors << execution_error
1305
1336
  when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
1306
1337
  raise type_error
1307
1338
  when GraphQL::IntegerDecodingError
@@ -1309,7 +1340,7 @@ module GraphQL
1309
1340
  end
1310
1341
  end
1311
1342
 
1312
- # A function to call when {#execute} receives an invalid query string
1343
+ # A function to call when {.execute} receives an invalid query string
1313
1344
  #
1314
1345
  # The default is to add the error to `context.errors`
1315
1346
  # @param parse_err [GraphQL::ParseError] The error encountered during parsing
@@ -1565,7 +1596,8 @@ module GraphQL
1565
1596
  # @see {Query#initialize} for query keyword arguments
1566
1597
  # @see {Execution::Multiplex#run_all} for multiplex keyword arguments
1567
1598
  # @param queries [Array<Hash>] Keyword arguments for each query
1568
- # @param context [Hash] Multiplex-level context
1599
+ # @option kwargs [Hash] :context ({}) Multiplex-level context
1600
+ # @option kwargs [nil, Integer] :max_complexity (nil)
1569
1601
  # @return [Array<GraphQL::Query::Result>] One result for each query in the input
1570
1602
  def multiplex(queries, **kwargs)
1571
1603
  GraphQL::Execution::Interpreter.run_all(self, queries, **kwargs)
@@ -1630,7 +1662,7 @@ module GraphQL
1630
1662
  end
1631
1663
  end
1632
1664
 
1633
- # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {#lazy_resolve}.
1665
+ # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {.lazy_resolve}.
1634
1666
  def lazy_method_name(obj)
1635
1667
  lazy_methods.get(obj)
1636
1668
  end
@@ -1668,6 +1700,170 @@ module GraphQL
1668
1700
  end
1669
1701
  end
1670
1702
 
1703
+
1704
+ # This setting controls how GraphQL-Ruby handles empty selections on Union types.
1705
+ #
1706
+ # To opt into future, spec-compliant behavior where these selections are rejected, set this to `false`.
1707
+ #
1708
+ # If you need to support previous, non-spec behavior which allowed selecting union fields
1709
+ # but *not* selecting any fields on that union, set this to `true` to continue allowing that behavior.
1710
+ #
1711
+ # If this is `true`, then {.legacy_invalid_empty_selections_on_union} will be called with {Query} objects
1712
+ # with that kind of selections. You must implement that method
1713
+ # @param new_value [Boolean]
1714
+ # @return [true, false, nil]
1715
+ def allow_legacy_invalid_empty_selections_on_union(new_value = NOT_CONFIGURED)
1716
+ if NOT_CONFIGURED.equal?(new_value)
1717
+ if defined?(@allow_legacy_invalid_empty_selections_on_union)
1718
+ @allow_legacy_invalid_empty_selections_on_union
1719
+ else
1720
+ find_inherited_value(:allow_legacy_invalid_empty_selections_on_union)
1721
+ end
1722
+ else
1723
+ @allow_legacy_invalid_empty_selections_on_union = new_value
1724
+ end
1725
+ end
1726
+
1727
+ # This method is called during validation when a previously-allowed, but non-spec
1728
+ # query is encountered where a union field has no child selections on it.
1729
+ #
1730
+ # You should implement this method to log the violation so that you can contact clients
1731
+ # and notify them about changing their queries. Then return a suitable value to
1732
+ # tell GraphQL-Ruby how to continue.
1733
+ # @param query [GraphQL::Query]
1734
+ # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
1735
+ # @return [String] A validation error to return for this query
1736
+ # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
1737
+ def legacy_invalid_empty_selections_on_union(query)
1738
+ raise "Implement `def self.legacy_invalid_empty_selections_on_union(query)` to handle this scenario"
1739
+ end
1740
+
1741
+ # This setting controls how GraphQL-Ruby handles overlapping selections on scalar types when the types
1742
+ # don't match.
1743
+ #
1744
+ # When set to `false`, GraphQL-Ruby will reject those queries with a validation error (as per the GraphQL spec).
1745
+ #
1746
+ # When set to `true`, GraphQL-Ruby will call {.legacy_invalid_return_type_conflicts} when the scenario is encountered.
1747
+ #
1748
+ # @param new_value [Boolean] `true` permits the legacy behavior, `false` rejects it.
1749
+ # @return [true, false, nil]
1750
+ def allow_legacy_invalid_return_type_conflicts(new_value = NOT_CONFIGURED)
1751
+ if NOT_CONFIGURED.equal?(new_value)
1752
+ if defined?(@allow_legacy_invalid_return_type_conflicts)
1753
+ @allow_legacy_invalid_return_type_conflicts
1754
+ else
1755
+ find_inherited_value(:allow_legacy_invalid_return_type_conflicts)
1756
+ end
1757
+ else
1758
+ @allow_legacy_invalid_return_type_conflicts = new_value
1759
+ end
1760
+ end
1761
+
1762
+ # This method is called when the query contains fields which don't contain matching scalar types.
1763
+ # This was previously allowed by GraphQL-Ruby but it's a violation of the GraphQL spec.
1764
+ #
1765
+ # You should implement this method to log the violation so that you observe usage of these fields.
1766
+ # Fixing this scenario might mean adding new fields, and telling clients to use those fields.
1767
+ # (Changing the field return type would be a breaking change, but if it works for your client use cases,
1768
+ # that might work, too.)
1769
+ #
1770
+ # @param query [GraphQL::Query]
1771
+ # @param type1 [Module] A GraphQL type definition
1772
+ # @param type2 [Module] A GraphQL type definition
1773
+ # @param node1 [GraphQL::Language::Nodes::Field] This node is recognized as conflicting. You might call `.line` and `.col` for custom error reporting.
1774
+ # @param node2 [GraphQL::Language::Nodes::Field] The other node recognized as conflicting.
1775
+ # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
1776
+ # @return [String] A validation error to return for this query
1777
+ # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
1778
+ def legacy_invalid_return_type_conflicts(query, type1, type2, node1, node2)
1779
+ raise "Implement #{self}.legacy_invalid_return_type_conflicts to handle this invalid selection"
1780
+ end
1781
+
1782
+ # The legacy complexity implementation included several bugs:
1783
+ #
1784
+ # - In some cases, it used the lexically _last_ field to determine a cost, instead of calculating the maximum among selections
1785
+ # - In some cases, it called field complexity hooks repeatedly (when it should have only called them once)
1786
+ #
1787
+ # The future implementation may produce higher total complexity scores, so it's not active by default yet. You can opt into
1788
+ # the future default behavior by configuring `:future` here. Or, you can choose a mode for each query with {.complexity_cost_calculation_mode_for}.
1789
+ #
1790
+ # The legacy mode is currently maintained alongside the future one, but it will be removed in a future GraphQL-Ruby version.
1791
+ #
1792
+ # If you choose `:compare`, you must also implement {.legacy_complexity_cost_calculation_mismatch} to handle the input somehow.
1793
+ #
1794
+ # @example Opting into the future calculation mode
1795
+ # complexity_cost_calculation_mode(:future)
1796
+ #
1797
+ # @example Choosing the legacy mode (which will work until that mode is removed...)
1798
+ # complexity_cost_calculation_mode(:legacy)
1799
+ #
1800
+ # @example Run both modes for every query, call {.legacy_complexity_cost_calculation_mismatch} when they don't match:
1801
+ # complexity_cost_calculation_mode(:compare)
1802
+ def complexity_cost_calculation_mode(new_mode = NOT_CONFIGURED)
1803
+ if NOT_CONFIGURED.equal?(new_mode)
1804
+ if defined?(@complexity_cost_calculation_mode)
1805
+ @complexity_cost_calculation_mode
1806
+ else
1807
+ find_inherited_value(:complexity_cost_calculation_mode)
1808
+ end
1809
+ else
1810
+ @complexity_cost_calculation_mode = new_mode
1811
+ end
1812
+ end
1813
+
1814
+ # Implement this method to produce a per-query complexity cost calculation mode. (Technically, it's per-multiplex.)
1815
+ #
1816
+ # This is a way to check the compatibility of queries coming to your API without adding overhead of running `:compare`
1817
+ # for every query. You could sample traffic, turn it off/on with feature flags, or anything else.
1818
+ #
1819
+ # @example Sampling traffic
1820
+ # def self.complexity_cost_calculation_mode_for(_context)
1821
+ # if rand < 0.1 # 10% of the time
1822
+ # :compare
1823
+ # else
1824
+ # :legacy
1825
+ # end
1826
+ # end
1827
+ #
1828
+ # @example Using a feature flag to manage future mode
1829
+ # def complexity_cost_calculation_mode_for(context)
1830
+ # current_user = context[:current_user]
1831
+ # if Flipper.enabled?(:future_complexity_cost, current_user)
1832
+ # :future
1833
+ # elsif rand < 0.5 # 50%
1834
+ # :compare
1835
+ # else
1836
+ # :legacy
1837
+ # end
1838
+ # end
1839
+ #
1840
+ # @param multiplex_context [Hash] The context for the currently-running {Execution::Multiplex} (which contains one or more queries)
1841
+ # @return [:future] Use the new calculation algorithm -- may be higher than `:legacy`
1842
+ # @return [:legacy] Use the legacy calculation algorithm, warts and all
1843
+ # @return [:compare] Run both algorithms and call {.legacy_complexity_cost_calculation_mismatch} if they don't match
1844
+ def complexity_cost_calculation_mode_for(multiplex_context)
1845
+ complexity_cost_calculation_mode
1846
+ end
1847
+
1848
+ # Implement this method in your schema to handle mismatches when `:compare` is used.
1849
+ #
1850
+ # @example Logging the mismatch
1851
+ # def self.legacy_cost_calculation_mismatch(multiplex, future_cost, legacy_cost)
1852
+ # client_id = multiplex.context[:api_client].id
1853
+ # operation_names = multiplex.queries.map { |q| q.selected_operation_name || "anonymous" }.join(", ")
1854
+ # Stats.increment(:complexity_mismatch, tags: { client: client_id, ops: operation_names })
1855
+ # legacy_cost
1856
+ # end
1857
+ # @see Query::Context#add_error Adding an error to the response to notify the client
1858
+ # @see Query::Context#response_extensions Adding key-value pairs to the response `"extensions" => { ... }`
1859
+ # @param multiplex [GraphQL::Execution::Multiplex]
1860
+ # @param future_complexity_cost [Integer]
1861
+ # @param legacy_complexity_cost [Integer]
1862
+ # @return [Integer] the cost to use for this query (probably one of `future_complexity_cost` or `legacy_complexity_cost`)
1863
+ def legacy_complexity_cost_calculation_mismatch(multiplex, future_complexity_cost, legacy_complexity_cost)
1864
+ 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"
1865
+ end
1866
+
1671
1867
  private
1672
1868
 
1673
1869
  def add_trace_options_for(mode, new_options)
@@ -34,9 +34,9 @@ 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
+ ].freeze
41
41
  end
42
42
  end
@@ -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.empty? && 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
@@ -33,26 +33,19 @@ module GraphQL
33
33
 
34
34
  private
35
35
 
36
- def field_conflicts
37
- @field_conflicts ||= Hash.new do |errors, field|
38
- errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :field, field_name: field)
39
- end
40
- end
41
-
42
- def arg_conflicts
43
- @arg_conflicts ||= Hash.new do |errors, field|
44
- errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :argument, field_name: field)
36
+ def conflicts
37
+ @conflicts ||= Hash.new do |h, error_type|
38
+ h[error_type] = Hash.new do |h2, field_name|
39
+ h2[field_name] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: error_type, field_name: field_name)
40
+ end
45
41
  end
46
42
  end
47
43
 
48
44
  def setting_errors
49
- @field_conflicts = nil
50
- @arg_conflicts = nil
51
-
45
+ @conflicts = nil
52
46
  yield
53
47
  # don't initialize these if they weren't initialized in the block:
54
- @field_conflicts && @field_conflicts.each_value { |error| add_error(error) }
55
- @arg_conflicts && @arg_conflicts.each_value { |error| add_error(error) }
48
+ @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
56
49
  end
57
50
 
58
51
  def conflicts_within_selection_set(node, parent_type)
@@ -222,7 +215,7 @@ module GraphQL
222
215
 
223
216
  if !are_mutually_exclusive
224
217
  if node1.name != node2.name
225
- conflict = field_conflicts[response_key]
218
+ conflict = conflicts[:field][response_key]
226
219
 
227
220
  conflict.add_conflict(node1, node1.name)
228
221
  conflict.add_conflict(node2, node2.name)
@@ -231,7 +224,7 @@ module GraphQL
231
224
  end
232
225
 
233
226
  if !same_arguments?(node1, node2)
234
- conflict = arg_conflicts[response_key]
227
+ conflict = conflicts[:argument][response_key]
235
228
 
236
229
  conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1)))
237
230
  conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2)))
@@ -240,6 +233,49 @@ module GraphQL
240
233
  end
241
234
  end
242
235
 
236
+ if !conflicts[:field].key?(response_key) &&
237
+ (t1 = field1.definition&.type) &&
238
+ (t2 = field2.definition&.type) &&
239
+ return_types_conflict?(t1, t2)
240
+
241
+ return_error = nil
242
+ message_override = nil
243
+ case @schema.allow_legacy_invalid_return_type_conflicts
244
+ when false
245
+ return_error = true
246
+ when true
247
+ legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2)
248
+ case legacy_handling
249
+ when nil
250
+ return_error = false
251
+ when :return_validation_error
252
+ return_error = true
253
+ when String
254
+ return_error = true
255
+ message_override = legacy_handling
256
+ else
257
+ raise GraphQL::Error, "#{@schema}.legacy_invalid_scalar_conflicts returned unexpected value: #{legacy_handling.inspect}. Expected `nil`, String, or `:return_validation_error`."
258
+ end
259
+ else
260
+ return_error = false
261
+ @context.query.logger.warn <<~WARN
262
+ GraphQL-Ruby encountered mismatched types in this query: `#{t1.to_type_signature}` (at #{node1.line}:#{node1.col}) vs. `#{t2.to_type_signature}` (at #{node2.line}:#{node2.col}).
263
+ This will return an error in future GraphQL-Ruby versions, as per the GraphQL specification
264
+ Learn about migrating here: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_return_type_conflicts-class_method
265
+ WARN
266
+ end
267
+
268
+ if return_error
269
+ conflict = conflicts[:return_type][response_key]
270
+ if message_override
271
+ conflict.message = message_override
272
+ end
273
+ conflict.add_conflict(node1, "`#{t1.to_type_signature}`")
274
+ conflict.add_conflict(node2, "`#{t2.to_type_signature}`")
275
+ @conflict_count += 1
276
+ end
277
+ end
278
+
243
279
  find_conflicts_between_sub_selection_sets(
244
280
  field1,
245
281
  field2,
@@ -247,6 +283,32 @@ module GraphQL
247
283
  )
248
284
  end
249
285
 
286
+ def return_types_conflict?(type1, type2)
287
+ if type1.list?
288
+ if type2.list?
289
+ return_types_conflict?(type1.of_type, type2.of_type)
290
+ else
291
+ true
292
+ end
293
+ elsif type2.list?
294
+ true
295
+ elsif type1.non_null?
296
+ if type2.non_null?
297
+ return_types_conflict?(type1.of_type, type2.of_type)
298
+ else
299
+ true
300
+ end
301
+ elsif type2.non_null?
302
+ true
303
+ elsif type1.kind.leaf? && type2.kind.leaf?
304
+ type1 != type2
305
+ else
306
+ # One or more of these are composite types,
307
+ # their selections will be validated later on.
308
+ false
309
+ end
310
+ end
311
+
250
312
  def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
251
313
  return if field1.definition.nil? ||
252
314
  field2.definition.nil? ||
@@ -14,9 +14,11 @@ module GraphQL
14
14
  end
15
15
 
16
16
  def message
17
- "Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?"
17
+ @message || "Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?"
18
18
  end
19
19
 
20
+ attr_writer :message
21
+
20
22
  def path
21
23
  []
22
24
  end
@@ -26,7 +28,13 @@ module GraphQL
26
28
  end
27
29
 
28
30
  def add_conflict(node, conflict_str)
29
- return if nodes.include?(node)
31
+ # Can't use `.include?` here because AST nodes implement `#==`
32
+ # based on string value, not including location. But sometimes,
33
+ # identical nodes conflict because of their differing return types.
34
+ if nodes.any? { |n| n == node && n.line == node.line && n.col == node.col }
35
+ # already have an error for this node
36
+ return
37
+ end
30
38
 
31
39
  @nodes << node
32
40
  @conflicts << conflict_str
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ class NotSingleSubscriptionError < StaticValidation::Error
5
+ def initialize(message, path: nil, nodes: [])
6
+ super(message, path: path, nodes: nodes)
7
+ end
8
+
9
+ # A hash representation of this Message
10
+ def to_h
11
+ extensions = {
12
+ "code" => code,
13
+ }
14
+
15
+ super.merge({
16
+ "extensions" => extensions
17
+ })
18
+ end
19
+
20
+ def code
21
+ "notSingleSubscription"
22
+ end
23
+ end
24
+ end
25
+ end