graphql 2.4.5 → 2.5.21
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/generators/graphql/detailed_trace_generator.rb +77 -0
- data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
- data/lib/graphql/analysis/analyzer.rb +2 -1
- data/lib/graphql/analysis/query_complexity.rb +87 -7
- data/lib/graphql/analysis/visitor.rb +37 -40
- data/lib/graphql/analysis.rb +12 -9
- data/lib/graphql/autoload.rb +1 -0
- data/lib/graphql/backtrace/table.rb +118 -55
- data/lib/graphql/backtrace.rb +1 -19
- data/lib/graphql/current.rb +6 -1
- data/lib/graphql/dashboard/application_controller.rb +41 -0
- data/lib/graphql/dashboard/detailed_traces.rb +47 -0
- data/lib/graphql/dashboard/installable.rb +22 -0
- data/lib/graphql/dashboard/landings_controller.rb +9 -0
- data/lib/graphql/dashboard/limiters.rb +93 -0
- data/lib/graphql/dashboard/operation_store.rb +199 -0
- data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
- data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
- data/lib/graphql/dashboard/statics/charts.min.css +1 -0
- data/lib/graphql/dashboard/statics/dashboard.css +30 -0
- data/lib/graphql/dashboard/statics/dashboard.js +143 -0
- data/lib/graphql/dashboard/statics/header-icon.png +0 -0
- data/lib/graphql/dashboard/statics/icon.png +0 -0
- data/lib/graphql/dashboard/statics_controller.rb +31 -0
- data/lib/graphql/dashboard/subscriptions.rb +97 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -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 +24 -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 +108 -0
- data/lib/graphql/dashboard.rb +96 -0
- data/lib/graphql/dataloader/active_record_association_source.rb +84 -0
- data/lib/graphql/dataloader/active_record_source.rb +47 -0
- data/lib/graphql/dataloader/async_dataloader.rb +38 -15
- data/lib/graphql/dataloader/null_dataloader.rb +55 -10
- data/lib/graphql/dataloader/source.rb +18 -6
- data/lib/graphql/dataloader.rb +110 -26
- data/lib/graphql/date_encoding_error.rb +1 -1
- data/lib/graphql/dig.rb +2 -1
- data/lib/graphql/execution/interpreter/resolve.rb +10 -16
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +58 -5
- data/lib/graphql/execution/interpreter/runtime.rb +229 -93
- data/lib/graphql/execution/interpreter.rb +15 -24
- data/lib/graphql/execution/multiplex.rb +7 -6
- data/lib/graphql/execution/next/field_resolve_step.rb +690 -0
- data/lib/graphql/execution/next/load_argument_step.rb +60 -0
- data/lib/graphql/execution/next/prepare_object_step.rb +129 -0
- data/lib/graphql/execution/next/runner.rb +389 -0
- data/lib/graphql/execution/next/selections_step.rb +37 -0
- data/lib/graphql/execution/next.rb +69 -0
- data/lib/graphql/execution.rb +1 -0
- data/lib/graphql/execution_error.rb +13 -10
- data/lib/graphql/introspection/directive_location_enum.rb +1 -1
- data/lib/graphql/introspection/directive_type.rb +7 -3
- data/lib/graphql/introspection/dynamic_fields.rb +5 -1
- data/lib/graphql/introspection/entry_points.rb +11 -3
- data/lib/graphql/introspection/enum_value_type.rb +5 -5
- data/lib/graphql/introspection/field_type.rb +13 -5
- data/lib/graphql/introspection/input_value_type.rb +21 -13
- data/lib/graphql/introspection/type_type.rb +64 -28
- data/lib/graphql/invalid_name_error.rb +1 -1
- data/lib/graphql/invalid_null_error.rb +25 -16
- data/lib/graphql/language/document_from_schema_definition.rb +2 -1
- data/lib/graphql/language/lexer.rb +16 -5
- data/lib/graphql/language/nodes.rb +8 -1
- data/lib/graphql/language/parser.rb +16 -8
- data/lib/graphql/language/static_visitor.rb +37 -33
- data/lib/graphql/language/visitor.rb +59 -55
- data/lib/graphql/language.rb +21 -12
- data/lib/graphql/pagination/connection.rb +2 -0
- data/lib/graphql/pagination/connections.rb +32 -0
- data/lib/graphql/query/context.rb +6 -10
- data/lib/graphql/query/null_context.rb +9 -3
- data/lib/graphql/query/partial.rb +179 -0
- data/lib/graphql/query.rb +64 -64
- data/lib/graphql/railtie.rb +1 -1
- data/lib/graphql/schema/addition.rb +3 -1
- data/lib/graphql/schema/always_visible.rb +1 -0
- data/lib/graphql/schema/argument.rb +24 -8
- data/lib/graphql/schema/build_from_definition.rb +113 -54
- data/lib/graphql/schema/directive/flagged.rb +2 -0
- data/lib/graphql/schema/directive.rb +52 -2
- data/lib/graphql/schema/enum.rb +36 -1
- data/lib/graphql/schema/enum_value.rb +1 -1
- data/lib/graphql/schema/field/connection_extension.rb +15 -35
- data/lib/graphql/schema/field/scope_extension.rb +22 -13
- data/lib/graphql/schema/field.rb +101 -51
- data/lib/graphql/schema/field_extension.rb +33 -0
- data/lib/graphql/schema/input_object.rb +45 -38
- data/lib/graphql/schema/interface.rb +2 -1
- data/lib/graphql/schema/list.rb +1 -1
- data/lib/graphql/schema/member/base_dsl_methods.rb +1 -1
- data/lib/graphql/schema/member/has_arguments.rb +56 -19
- data/lib/graphql/schema/member/has_authorization.rb +35 -0
- data/lib/graphql/schema/member/has_dataloader.rb +79 -0
- data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
- data/lib/graphql/schema/member/has_directives.rb +1 -1
- data/lib/graphql/schema/member/has_fields.rb +81 -5
- data/lib/graphql/schema/member/has_interfaces.rb +3 -3
- data/lib/graphql/schema/member/scoped.rb +1 -1
- data/lib/graphql/schema/member/type_system_helpers.rb +17 -3
- data/lib/graphql/schema/member.rb +6 -0
- data/lib/graphql/schema/object.rb +18 -8
- data/lib/graphql/schema/ractor_shareable.rb +79 -0
- data/lib/graphql/schema/resolver.rb +52 -6
- data/lib/graphql/schema/scalar.rb +1 -6
- data/lib/graphql/schema/subscription.rb +50 -4
- data/lib/graphql/schema/timeout.rb +19 -2
- data/lib/graphql/schema/validator/required_validator.rb +71 -14
- data/lib/graphql/schema/visibility/migration.rb +3 -2
- data/lib/graphql/schema/visibility/profile.rb +115 -23
- data/lib/graphql/schema/visibility.rb +49 -32
- data/lib/graphql/schema/warden.rb +23 -2
- data/lib/graphql/schema.rb +333 -68
- 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 +79 -17
- 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/static_validation/validator.rb +6 -1
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
- data/lib/graphql/subscriptions/event.rb +12 -1
- data/lib/graphql/subscriptions/serialize.rb +1 -1
- data/lib/graphql/subscriptions.rb +1 -1
- data/lib/graphql/testing/helpers.rb +17 -11
- data/lib/graphql/testing/mock_action_cable.rb +111 -0
- data/lib/graphql/testing.rb +1 -0
- data/lib/graphql/tracing/active_support_notifications_trace.rb +14 -3
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
- data/lib/graphql/tracing/appoptics_trace.rb +9 -1
- data/lib/graphql/tracing/appoptics_tracing.rb +7 -0
- data/lib/graphql/tracing/appsignal_trace.rb +32 -55
- data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
- data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
- data/lib/graphql/tracing/data_dog_trace.rb +46 -158
- data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
- data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
- data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
- data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
- data/lib/graphql/tracing/detailed_trace.rb +156 -0
- data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
- data/lib/graphql/tracing/legacy_trace.rb +4 -61
- data/lib/graphql/tracing/monitor_trace.rb +283 -0
- data/lib/graphql/tracing/new_relic_trace.rb +47 -54
- data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
- data/lib/graphql/tracing/notifications_trace.rb +184 -34
- data/lib/graphql/tracing/notifications_tracing.rb +2 -0
- data/lib/graphql/tracing/null_trace.rb +9 -0
- data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
- data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
- data/lib/graphql/tracing/perfetto_trace.rb +864 -0
- data/lib/graphql/tracing/platform_trace.rb +5 -0
- data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
- data/lib/graphql/tracing/prometheus_trace.rb +72 -68
- data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
- data/lib/graphql/tracing/scout_trace.rb +32 -55
- data/lib/graphql/tracing/scout_tracing.rb +2 -0
- data/lib/graphql/tracing/sentry_trace.rb +64 -94
- data/lib/graphql/tracing/statsd_trace.rb +33 -41
- data/lib/graphql/tracing/statsd_tracing.rb +2 -0
- data/lib/graphql/tracing/trace.rb +111 -1
- data/lib/graphql/tracing.rb +31 -30
- data/lib/graphql/type_kinds.rb +1 -0
- data/lib/graphql/types/relay/connection_behaviors.rb +9 -7
- data/lib/graphql/types/relay/edge_behaviors.rb +5 -4
- data/lib/graphql/types/relay/has_node_field.rb +13 -8
- data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
- data/lib/graphql/types/relay/node_behaviors.rb +13 -2
- data/lib/graphql/unauthorized_error.rb +5 -1
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +12 -31
- metadata +174 -11
- data/lib/graphql/backtrace/inspect_result.rb +0 -38
- data/lib/graphql/backtrace/trace.rb +0 -93
- data/lib/graphql/backtrace/tracer.rb +0 -80
- data/lib/graphql/schema/null_mask.rb +0 -11
- data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
|
@@ -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_with_type(@context.query, resolved_type)
|
|
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_with_type, 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? ||
|
|
@@ -345,7 +407,7 @@ module GraphQL
|
|
|
345
407
|
fields << Field.new(node, definition, owner_type, parents)
|
|
346
408
|
when GraphQL::Language::Nodes::InlineFragment
|
|
347
409
|
fragment_type = node.type ? @types.type(node.type.name) : owner_type
|
|
348
|
-
find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type:
|
|
410
|
+
find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
|
|
349
411
|
when GraphQL::Language::Nodes::FragmentSpread
|
|
350
412
|
fragment_spreads << FragmentSpread.new(node.name, parents)
|
|
351
413
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module StaticValidation
|
|
4
|
+
module SubscriptionRootExistsAndSingleSubscriptionSelection
|
|
5
|
+
def on_operation_definition(node, parent)
|
|
6
|
+
if node.operation_type == "subscription"
|
|
7
|
+
if context.types.subscription_root.nil?
|
|
8
|
+
add_error(GraphQL::StaticValidation::SubscriptionRootExistsError.new(
|
|
9
|
+
'Schema is not configured for subscriptions',
|
|
10
|
+
nodes: node
|
|
11
|
+
))
|
|
12
|
+
elsif node.selections.size != 1
|
|
13
|
+
add_error(GraphQL::StaticValidation::NotSingleSubscriptionError.new(
|
|
14
|
+
'A subscription operation may only have one selection',
|
|
15
|
+
nodes: node,
|
|
16
|
+
))
|
|
17
|
+
else
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -19,13 +19,17 @@ module GraphQL
|
|
|
19
19
|
:on_field,
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
VALIDATE_DIRECTIVE_LOCATION_ON_NODE = <<~RUBY
|
|
23
|
+
def %{method_name}(node, parent)
|
|
24
24
|
if !node.directives.empty?
|
|
25
25
|
validate_directive_location(node)
|
|
26
26
|
end
|
|
27
27
|
super(node, parent)
|
|
28
28
|
end
|
|
29
|
+
RUBY
|
|
30
|
+
DIRECTIVE_NODE_HOOKS.each do |method_name|
|
|
31
|
+
# Can't use `define_method {...}` here because the proc can't be isolated for use in non-main Ractors
|
|
32
|
+
module_eval(VALIDATE_DIRECTIVE_LOCATION_ON_NODE % { method_name: method_name }) # rubocop:disable Development/NoEvalCop
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
private
|
|
@@ -27,6 +27,8 @@ module GraphQL
|
|
|
27
27
|
# @param max_errors [Integer] Maximum number of errors before aborting validation. Any positive number will limit the number of errors. Defaults to nil for no limit.
|
|
28
28
|
# @return [Array<Hash>]
|
|
29
29
|
def validate(query, validate: true, timeout: nil, max_errors: nil)
|
|
30
|
+
errors = nil
|
|
31
|
+
query.current_trace.begin_validate(query, validate)
|
|
30
32
|
query.current_trace.validate(validate: validate, query: query) do
|
|
31
33
|
begin_t = Time.now
|
|
32
34
|
errors = if validate == false
|
|
@@ -58,10 +60,13 @@ module GraphQL
|
|
|
58
60
|
}
|
|
59
61
|
end
|
|
60
62
|
rescue GraphQL::ExecutionError => e
|
|
63
|
+
errors = [e]
|
|
61
64
|
{
|
|
62
65
|
remaining_timeout: nil,
|
|
63
|
-
errors:
|
|
66
|
+
errors: errors,
|
|
64
67
|
}
|
|
68
|
+
ensure
|
|
69
|
+
query.current_trace.end_validate(query, validate, errors)
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
# Invoked when static validation times out.
|
|
@@ -20,12 +20,22 @@ module GraphQL
|
|
|
20
20
|
def after_resolve(value:, context:, object:, arguments:, **rest)
|
|
21
21
|
if value.is_a?(GraphQL::ExecutionError)
|
|
22
22
|
value
|
|
23
|
+
elsif @field.resolver&.method_defined?(:subscription_written?) &&
|
|
24
|
+
(subscription_namespace = context.namespace(:subscriptions)) &&
|
|
25
|
+
(subscriptions_by_path = subscription_namespace[:subscriptions])
|
|
26
|
+
(subscription_instance = subscriptions_by_path[context.current_path])
|
|
27
|
+
# If it was already written, don't append this event to be written later
|
|
28
|
+
if !subscription_instance.subscription_written?
|
|
29
|
+
events = context.namespace(:subscriptions)[:events]
|
|
30
|
+
events << subscription_instance.event
|
|
31
|
+
end
|
|
32
|
+
value
|
|
23
33
|
elsif (events = context.namespace(:subscriptions)[:events])
|
|
24
34
|
# This is the first execution, so gather an Event
|
|
25
35
|
# for the backend to register:
|
|
26
36
|
event = Subscriptions::Event.new(
|
|
27
37
|
name: field.name,
|
|
28
|
-
arguments:
|
|
38
|
+
arguments: arguments,
|
|
29
39
|
context: context,
|
|
30
40
|
field: field,
|
|
31
41
|
)
|
|
@@ -33,7 +43,7 @@ module GraphQL
|
|
|
33
43
|
value
|
|
34
44
|
elsif context.query.subscription_topic == Subscriptions::Event.serialize(
|
|
35
45
|
field.name,
|
|
36
|
-
|
|
46
|
+
arguments,
|
|
37
47
|
field,
|
|
38
48
|
scope: (field.subscription_scope ? context[field.subscription_scope] : nil),
|
|
39
49
|
)
|
|
@@ -45,14 +55,6 @@ module GraphQL
|
|
|
45
55
|
context.skip
|
|
46
56
|
end
|
|
47
57
|
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def arguments_without_field_extras(arguments:)
|
|
52
|
-
arguments.dup.tap do |event_args|
|
|
53
|
-
field.extras.each { |k| event_args.delete(k) }
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
end
|
|
@@ -20,7 +20,7 @@ module GraphQL
|
|
|
20
20
|
|
|
21
21
|
def initialize(name:, arguments:, field: nil, context: nil, scope: nil)
|
|
22
22
|
@name = name
|
|
23
|
-
@arguments = arguments
|
|
23
|
+
@arguments = self.class.arguments_without_field_extras(arguments: arguments, field: field)
|
|
24
24
|
@context = context
|
|
25
25
|
field ||= context.field
|
|
26
26
|
scope_key = field.subscription_scope
|
|
@@ -39,6 +39,7 @@ module GraphQL
|
|
|
39
39
|
# @return [String] an identifier for this unit of subscription
|
|
40
40
|
def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext.instance)
|
|
41
41
|
subscription = field.resolver || GraphQL::Schema::Subscription
|
|
42
|
+
arguments = arguments_without_field_extras(field: field, arguments: arguments)
|
|
42
43
|
normalized_args = stringify_args(field, arguments.to_h, context)
|
|
43
44
|
subscription.topic_for(arguments: normalized_args, field: field, scope: scope)
|
|
44
45
|
end
|
|
@@ -60,6 +61,16 @@ module GraphQL
|
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
class << self
|
|
64
|
+
def arguments_without_field_extras(arguments:, field:)
|
|
65
|
+
if !field.extras.empty?
|
|
66
|
+
arguments = arguments.dup
|
|
67
|
+
field.extras.each do |extra_key|
|
|
68
|
+
arguments.delete(extra_key)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
arguments
|
|
72
|
+
end
|
|
73
|
+
|
|
63
74
|
private
|
|
64
75
|
|
|
65
76
|
# This method does not support cyclic references in the Hash,
|
|
@@ -146,7 +146,7 @@ module GraphQL
|
|
|
146
146
|
elsif obj.is_a?(Date) || obj.is_a?(Time)
|
|
147
147
|
# DateTime extends Date; for TimeWithZone, call `.utc` first.
|
|
148
148
|
{ TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
|
|
149
|
-
elsif obj.is_a?(OpenStruct)
|
|
149
|
+
elsif defined?(OpenStruct) && obj.is_a?(OpenStruct)
|
|
150
150
|
{ OPEN_STRUCT_KEY => dump_value(obj.to_h) }
|
|
151
151
|
elsif defined?(ActiveRecord::Relation) && obj.is_a?(ActiveRecord::Relation)
|
|
152
152
|
dump_value(obj.to_a)
|
|
@@ -80,7 +80,7 @@ module GraphQL
|
|
|
80
80
|
|
|
81
81
|
# Normalize symbol-keyed args to strings, try camelizing them
|
|
82
82
|
# Should this accept a real context somehow?
|
|
83
|
-
normalized_args = normalize_arguments(normalized_event_name, field, args,
|
|
83
|
+
normalized_args = normalize_arguments(normalized_event_name, field, args, @schema.null_context)
|
|
84
84
|
|
|
85
85
|
event = Subscriptions::Event.new(
|
|
86
86
|
name: normalized_event_name,
|
|
@@ -39,22 +39,25 @@ module GraphQL
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, ast_node: nil, lookahead: nil)
|
|
42
|
+
def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, ast_node: nil, lookahead: nil, visibility_profile: nil)
|
|
43
43
|
type_name, *field_names = field_path.split(".")
|
|
44
|
-
dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context)
|
|
44
|
+
dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context, visibility_profile: visibility_profile)
|
|
45
45
|
query_context = dummy_query.context
|
|
46
|
+
dataloader = query_context.dataloader
|
|
46
47
|
object_type = dummy_query.types.type(type_name) # rubocop:disable Development/ContextIsPassedCop
|
|
47
48
|
if object_type
|
|
48
49
|
graphql_result = object
|
|
49
50
|
field_names.each do |field_name|
|
|
50
51
|
inner_object = graphql_result
|
|
51
|
-
|
|
52
|
+
dataloader.run_isolated {
|
|
53
|
+
graphql_result = object_type.wrap(inner_object, query_context)
|
|
54
|
+
}
|
|
52
55
|
if graphql_result.nil?
|
|
53
56
|
return nil
|
|
54
57
|
end
|
|
55
58
|
visible_field = dummy_query.types.field(object_type, field_name) # rubocop:disable Development/ContextIsPassedCop
|
|
56
59
|
if visible_field
|
|
57
|
-
|
|
60
|
+
dataloader.run_isolated {
|
|
58
61
|
query_context[:current_field] = visible_field
|
|
59
62
|
field_args = visible_field.coerce_arguments(graphql_result, arguments, query_context)
|
|
60
63
|
field_args = schema.sync_lazy(field_args)
|
|
@@ -101,32 +104,35 @@ module GraphQL
|
|
|
101
104
|
end
|
|
102
105
|
end
|
|
103
106
|
|
|
104
|
-
def with_resolution_context(schema, type:, object:, context:{})
|
|
107
|
+
def with_resolution_context(schema, type:, object:, context:{}, visibility_profile: nil)
|
|
105
108
|
resolution_context = ResolutionAssertionContext.new(
|
|
106
109
|
self,
|
|
107
110
|
schema: schema,
|
|
108
111
|
type_name: type,
|
|
109
112
|
object: object,
|
|
110
|
-
context: context
|
|
113
|
+
context: context,
|
|
114
|
+
visibility_profile: visibility_profile,
|
|
111
115
|
)
|
|
112
116
|
yield(resolution_context)
|
|
113
117
|
end
|
|
114
118
|
|
|
115
119
|
class ResolutionAssertionContext
|
|
116
|
-
def initialize(test, type_name:, object:, schema:, context:)
|
|
120
|
+
def initialize(test, type_name:, object:, schema:, context:, visibility_profile:)
|
|
117
121
|
@test = test
|
|
118
122
|
@type_name = type_name
|
|
119
123
|
@object = object
|
|
120
124
|
@schema = schema
|
|
121
125
|
@context = context
|
|
126
|
+
@visibility_profile = visibility_profile
|
|
122
127
|
end
|
|
123
128
|
|
|
129
|
+
attr_reader :visibility_profile
|
|
124
130
|
|
|
125
131
|
def run_graphql_field(field_name, arguments: {})
|
|
126
132
|
if @schema
|
|
127
|
-
@test.run_graphql_field(@schema, "#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context)
|
|
133
|
+
@test.run_graphql_field(@schema, "#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context, visibility_profile: @visibility_profile)
|
|
128
134
|
else
|
|
129
|
-
@test.run_graphql_field("#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context)
|
|
135
|
+
@test.run_graphql_field("#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context, visibility_profile: @visibility_profile)
|
|
130
136
|
end
|
|
131
137
|
end
|
|
132
138
|
end
|
|
@@ -134,8 +140,8 @@ module GraphQL
|
|
|
134
140
|
module SchemaHelpers
|
|
135
141
|
include Helpers
|
|
136
142
|
|
|
137
|
-
def run_graphql_field(field_path, object, arguments: {}, context: {})
|
|
138
|
-
super(@@schema_class_for_helpers, field_path, object, arguments: arguments, context: context)
|
|
143
|
+
def run_graphql_field(field_path, object, arguments: {}, context: {}, visibility_profile: nil)
|
|
144
|
+
super(@@schema_class_for_helpers, field_path, object, arguments: arguments, context: context, visibility_profile: visibility_profile)
|
|
139
145
|
end
|
|
140
146
|
|
|
141
147
|
def with_resolution_context(*args, **kwargs, &block)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module Testing
|
|
4
|
+
# A stub implementation of ActionCable.
|
|
5
|
+
# Any methods to support the mock backend have `mock` in the name.
|
|
6
|
+
#
|
|
7
|
+
# @example Configuring your schema to use MockActionCable in the test environment
|
|
8
|
+
# class MySchema < GraphQL::Schema
|
|
9
|
+
# # Use MockActionCable in test:
|
|
10
|
+
# use GraphQL::Subscriptions::ActionCableSubscriptions,
|
|
11
|
+
# action_cable: Rails.env.test? ? GraphQL::Testing::MockActionCable : ActionCable
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Clearing old data before each test
|
|
15
|
+
# setup do
|
|
16
|
+
# GraphQL::Testing::MockActionCable.clear_mocks
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Using MockActionCable in a test case
|
|
20
|
+
# # Create a channel to use in the test, pass it to GraphQL
|
|
21
|
+
# mock_channel = GraphQL::Testing::MockActionCable.get_mock_channel
|
|
22
|
+
# ActionCableTestSchema.execute("subscription { newsFlash { text } }", context: { channel: mock_channel })
|
|
23
|
+
#
|
|
24
|
+
# # Trigger a subscription update
|
|
25
|
+
# ActionCableTestSchema.subscriptions.trigger(:news_flash, {}, {text: "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"})
|
|
26
|
+
#
|
|
27
|
+
# # Check messages on the channel
|
|
28
|
+
# expected_msg = {
|
|
29
|
+
# result: {
|
|
30
|
+
# "data" => {
|
|
31
|
+
# "newsFlash" => {
|
|
32
|
+
# "text" => "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"
|
|
33
|
+
# }
|
|
34
|
+
# }
|
|
35
|
+
# },
|
|
36
|
+
# more: true,
|
|
37
|
+
# }
|
|
38
|
+
# assert_equal [expected_msg], mock_channel.mock_broadcasted_messages
|
|
39
|
+
#
|
|
40
|
+
class MockActionCable
|
|
41
|
+
class MockChannel
|
|
42
|
+
def initialize
|
|
43
|
+
@mock_broadcasted_messages = []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Array<Hash>] Payloads "sent" to this channel by GraphQL-Ruby
|
|
47
|
+
attr_reader :mock_broadcasted_messages
|
|
48
|
+
|
|
49
|
+
# Called by ActionCableSubscriptions. Implements a Rails API.
|
|
50
|
+
def stream_from(stream_name, coder: nil, &block)
|
|
51
|
+
# Rails uses `coder`, we don't
|
|
52
|
+
block ||= ->(msg) { @mock_broadcasted_messages << msg }
|
|
53
|
+
MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Used by mock code
|
|
58
|
+
# @api private
|
|
59
|
+
class MockStream
|
|
60
|
+
def initialize
|
|
61
|
+
@mock_channels = {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def add_mock_channel(channel, handler)
|
|
65
|
+
@mock_channels[channel] = handler
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def mock_broadcast(message)
|
|
69
|
+
@mock_channels.each do |channel, handler|
|
|
70
|
+
handler && handler.call(message)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
# Call this before each test run to make sure that MockActionCable's data is empty
|
|
77
|
+
def clear_mocks
|
|
78
|
+
@mock_streams = {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Implements Rails API
|
|
82
|
+
def server
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Implements Rails API
|
|
87
|
+
def broadcast(stream_name, message)
|
|
88
|
+
stream = @mock_streams[stream_name]
|
|
89
|
+
stream && stream.mock_broadcast(message)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Used by mock code
|
|
93
|
+
def mock_stream_for(stream_name)
|
|
94
|
+
@mock_streams[stream_name] ||= MockStream.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Use this as `context[:channel]` to simulate an ActionCable channel
|
|
98
|
+
#
|
|
99
|
+
# @return [GraphQL::Testing::MockActionCable::MockChannel]
|
|
100
|
+
def get_mock_channel
|
|
101
|
+
MockChannel.new
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [Array<String>] Streams that currently have subscribers
|
|
105
|
+
def mock_stream_names
|
|
106
|
+
@mock_streams.keys
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/graphql/testing.rb
CHANGED