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.
- checksums.yaml +4 -4
- data/lib/graphql/analysis/query_complexity.rb +87 -7
- data/lib/graphql/backtrace/table.rb +37 -14
- data/lib/graphql/current.rb +1 -1
- data/lib/graphql/dashboard/detailed_traces.rb +47 -0
- data/lib/graphql/dashboard/installable.rb +22 -0
- data/lib/graphql/dashboard/limiters.rb +93 -0
- data/lib/graphql/dashboard/operation_store.rb +199 -0
- data/lib/graphql/dashboard/statics/charts.min.css +1 -0
- data/lib/graphql/dashboard/statics/dashboard.css +27 -0
- data/lib/graphql/dashboard/statics/dashboard.js +74 -9
- data/lib/graphql/dashboard/subscriptions.rb +96 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +49 -1
- data/lib/graphql/dashboard.rb +45 -29
- data/lib/graphql/dataloader/active_record_association_source.rb +28 -8
- data/lib/graphql/dataloader/active_record_source.rb +26 -5
- data/lib/graphql/dataloader/null_dataloader.rb +7 -0
- data/lib/graphql/dataloader/source.rb +16 -4
- data/lib/graphql/dig.rb +2 -1
- data/lib/graphql/execution/interpreter/resolve.rb +3 -3
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +34 -1
- data/lib/graphql/execution/interpreter/runtime.rb +163 -59
- data/lib/graphql/execution/interpreter.rb +5 -13
- data/lib/graphql/execution/multiplex.rb +6 -1
- data/lib/graphql/invalid_null_error.rb +15 -2
- data/lib/graphql/language/lexer.rb +9 -2
- data/lib/graphql/language/nodes.rb +5 -1
- data/lib/graphql/language/parser.rb +14 -6
- data/lib/graphql/query/context.rb +3 -8
- data/lib/graphql/query/partial.rb +179 -0
- data/lib/graphql/query.rb +59 -55
- data/lib/graphql/schema/addition.rb +3 -1
- data/lib/graphql/schema/always_visible.rb +1 -0
- data/lib/graphql/schema/argument.rb +9 -3
- data/lib/graphql/schema/build_from_definition.rb +96 -47
- data/lib/graphql/schema/directive/flagged.rb +2 -0
- data/lib/graphql/schema/directive.rb +33 -1
- data/lib/graphql/schema/field.rb +23 -1
- data/lib/graphql/schema/input_object.rb +38 -30
- data/lib/graphql/schema/list.rb +1 -1
- data/lib/graphql/schema/member/has_arguments.rb +2 -2
- data/lib/graphql/schema/member/has_dataloader.rb +4 -2
- data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
- data/lib/graphql/schema/member/has_interfaces.rb +2 -2
- data/lib/graphql/schema/member/type_system_helpers.rb +16 -2
- data/lib/graphql/schema/ractor_shareable.rb +79 -0
- data/lib/graphql/schema/resolver.rb +1 -0
- data/lib/graphql/schema/scalar.rb +1 -6
- data/lib/graphql/schema/timeout.rb +19 -2
- data/lib/graphql/schema/validator/required_validator.rb +15 -6
- data/lib/graphql/schema/visibility/migration.rb +2 -2
- data/lib/graphql/schema/visibility/profile.rb +107 -21
- data/lib/graphql/schema/visibility.rb +41 -29
- data/lib/graphql/schema/warden.rb +13 -5
- data/lib/graphql/schema.rb +228 -32
- data/lib/graphql/static_validation/all_rules.rb +2 -2
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +78 -16
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
- data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
- data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
- data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
- data/lib/graphql/testing/helpers.rb +5 -2
- data/lib/graphql/tracing/active_support_notifications_trace.rb +7 -0
- data/lib/graphql/tracing/appoptics_tracing.rb +5 -0
- data/lib/graphql/tracing/appsignal_trace.rb +26 -61
- data/lib/graphql/tracing/data_dog_trace.rb +41 -164
- data/lib/graphql/tracing/monitor_trace.rb +283 -0
- data/lib/graphql/tracing/new_relic_trace.rb +34 -164
- data/lib/graphql/tracing/notifications_trace.rb +183 -37
- data/lib/graphql/tracing/null_trace.rb +1 -1
- data/lib/graphql/tracing/perfetto_trace.rb +16 -19
- data/lib/graphql/tracing/prometheus_trace.rb +47 -74
- data/lib/graphql/tracing/scout_trace.rb +25 -59
- data/lib/graphql/tracing/sentry_trace.rb +56 -99
- data/lib/graphql/tracing/statsd_trace.rb +24 -47
- data/lib/graphql/tracing/trace.rb +0 -17
- data/lib/graphql/tracing.rb +1 -0
- data/lib/graphql/type_kinds.rb +1 -0
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +1 -1
- metadata +35 -26
- data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +0 -63
- data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
data/lib/graphql/schema.rb
CHANGED
@@ -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
|
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 =
|
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
|
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
|
-
|
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
|
-
|
1107
|
-
|
1108
|
-
|
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
|
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 `
|
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
|
-
#
|
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,
|
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 =
|
1333
|
+
execution_error.path = context[:current_path]
|
1303
1334
|
|
1304
|
-
|
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 {
|
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
|
-
# @
|
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 {
|
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::
|
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
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
@
|
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
|
-
@
|
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 =
|
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 =
|
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
|
-
|
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
|