graphql 2.5.11 → 2.5.23
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/dashboard/application_controller.rb +41 -0
- data/lib/graphql/dashboard/landings_controller.rb +9 -0
- data/lib/graphql/dashboard/statics_controller.rb +31 -0
- data/lib/graphql/dashboard/subscriptions.rb +2 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
- data/lib/graphql/dashboard.rb +11 -73
- data/lib/graphql/dataloader/async_dataloader.rb +22 -11
- data/lib/graphql/dataloader/null_dataloader.rb +48 -10
- data/lib/graphql/dataloader.rb +75 -23
- data/lib/graphql/date_encoding_error.rb +1 -1
- data/lib/graphql/execution/interpreter/resolve.rb +7 -13
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
- data/lib/graphql/execution/interpreter/runtime.rb +24 -18
- data/lib/graphql/execution/interpreter.rb +8 -22
- data/lib/graphql/execution/lazy.rb +1 -1
- data/lib/graphql/execution/multiplex.rb +1 -1
- data/lib/graphql/execution/next/field_resolve_step.rb +743 -0
- data/lib/graphql/execution/next/load_argument_step.rb +64 -0
- data/lib/graphql/execution/next/prepare_object_step.rb +129 -0
- data/lib/graphql/execution/next/runner.rb +411 -0
- data/lib/graphql/execution/next/selections_step.rb +37 -0
- data/lib/graphql/execution/next.rb +72 -0
- data/lib/graphql/execution.rb +8 -4
- data/lib/graphql/execution_error.rb +17 -10
- 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_null_error.rb +11 -5
- data/lib/graphql/language/document_from_schema_definition.rb +2 -1
- 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 +4 -3
- data/lib/graphql/query/null_context.rb +9 -3
- data/lib/graphql/schema/argument.rb +12 -0
- data/lib/graphql/schema/build_from_definition.rb +10 -1
- data/lib/graphql/schema/directive.rb +22 -4
- 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 +79 -48
- data/lib/graphql/schema/field_extension.rb +33 -0
- 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 +43 -14
- data/lib/graphql/schema/member/has_authorization.rb +35 -0
- data/lib/graphql/schema/member/has_dataloader.rb +37 -0
- data/lib/graphql/schema/member/has_fields.rb +86 -5
- data/lib/graphql/schema/member.rb +5 -0
- data/lib/graphql/schema/non_null.rb +1 -1
- data/lib/graphql/schema/object.rb +1 -0
- data/lib/graphql/schema/resolver.rb +60 -1
- data/lib/graphql/schema/subscription.rb +0 -2
- data/lib/graphql/schema/validator/required_validator.rb +33 -2
- data/lib/graphql/schema/visibility/profile.rb +68 -49
- data/lib/graphql/schema/visibility.rb +3 -3
- data/lib/graphql/schema/wrapper.rb +7 -1
- data/lib/graphql/schema.rb +53 -10
- data/lib/graphql/static_validation/base_visitor.rb +90 -66
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
- data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
- data/lib/graphql/static_validation/validation_context.rb +1 -1
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +25 -1
- data/lib/graphql/subscriptions/event.rb +1 -0
- data/lib/graphql/subscriptions.rb +21 -1
- data/lib/graphql/testing/helpers.rb +12 -9
- data/lib/graphql/testing/mock_action_cable.rb +111 -0
- data/lib/graphql/testing.rb +1 -0
- data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
- data/lib/graphql/tracing/detailed_trace.rb +70 -7
- data/lib/graphql/tracing/perfetto_trace.rb +209 -79
- data/lib/graphql/tracing/sentry_trace.rb +3 -1
- data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
- data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
- 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 +9 -1
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +8 -2
- metadata +17 -3
|
@@ -28,10 +28,10 @@ module GraphQL
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def add_conflict(node, conflict_str)
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
if nodes.any? { |n| n
|
|
31
|
+
# Check if we already have an error for this exact node.
|
|
32
|
+
# Use object identity first (fast path), then fall back to
|
|
33
|
+
# value + location comparison for duplicate AST nodes.
|
|
34
|
+
if nodes.any? { |n| n.equal?(node) || (n.line == node.line && n.col == node.col && n == node) }
|
|
35
35
|
# already have an error for this node
|
|
36
36
|
return
|
|
37
37
|
end
|
|
@@ -8,8 +8,8 @@ module GraphQL
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def on_inline_fragment(node, parent)
|
|
11
|
-
fragment_parent =
|
|
12
|
-
fragment_child =
|
|
11
|
+
fragment_parent = @parent_object_type
|
|
12
|
+
fragment_child = @current_object_type
|
|
13
13
|
if fragment_child
|
|
14
14
|
validate_fragment_in_scope(fragment_parent, fragment_child, node, context, context.path)
|
|
15
15
|
end
|
|
@@ -17,7 +17,7 @@ module GraphQL
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def on_fragment_spread(node, parent)
|
|
20
|
-
fragment_parent =
|
|
20
|
+
fragment_parent = @current_object_type
|
|
21
21
|
@spreads_to_validate << FragmentSpread.new(node: node, parent_type: fragment_parent, path: context.path)
|
|
22
22
|
super
|
|
23
23
|
end
|
|
@@ -23,18 +23,21 @@ module GraphQL
|
|
|
23
23
|
type_name = fragment_node.type.name
|
|
24
24
|
type = @types.type(type_name)
|
|
25
25
|
if type.nil?
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
suggestion = if @schema.did_you_mean
|
|
27
|
+
@all_possible_fragment_type_names ||= begin
|
|
28
|
+
names = []
|
|
29
|
+
context.types.all_types.each do |type|
|
|
30
|
+
if type.kind.fields?
|
|
31
|
+
names << type.graphql_name
|
|
32
|
+
end
|
|
31
33
|
end
|
|
34
|
+
names
|
|
32
35
|
end
|
|
33
|
-
|
|
36
|
+
context.did_you_mean_suggestion(type_name, @all_possible_fragment_type_names)
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
add_error(GraphQL::StaticValidation::FragmentTypesExistError.new(
|
|
37
|
-
"No such type #{type_name}, so it can't be a fragment condition#{
|
|
40
|
+
"No such type #{type_name}, so it can't be a fragment condition#{suggestion}",
|
|
38
41
|
nodes: fragment_node,
|
|
39
42
|
type: type_name
|
|
40
43
|
))
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
module GraphQL
|
|
3
3
|
module StaticValidation
|
|
4
4
|
module RequiredArgumentsArePresent
|
|
5
|
+
def initialize(*)
|
|
6
|
+
super
|
|
7
|
+
@required_args_cache = {}.compare_by_identity
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
def on_field(node, _parent)
|
|
6
|
-
assert_required_args(node,
|
|
11
|
+
assert_required_args(node, @current_field_definition)
|
|
7
12
|
super
|
|
8
13
|
end
|
|
9
14
|
|
|
@@ -16,13 +21,28 @@ module GraphQL
|
|
|
16
21
|
private
|
|
17
22
|
|
|
18
23
|
def assert_required_args(ast_node, defn)
|
|
19
|
-
|
|
20
|
-
return if args.empty?
|
|
21
|
-
present_argument_names = ast_node.arguments.map(&:name)
|
|
22
|
-
required_argument_names = context.query.types.arguments(defn)
|
|
23
|
-
.select { |a| a.type.kind.non_null? && !a.default_value? && context.query.types.argument(defn, a.name) }
|
|
24
|
-
.map!(&:name)
|
|
24
|
+
return unless defn
|
|
25
25
|
|
|
26
|
+
# Cache required argument names per definition to avoid re-iterating
|
|
27
|
+
# arguments for the same definition across field instances
|
|
28
|
+
if @required_args_cache.key?(defn)
|
|
29
|
+
required_argument_names = @required_args_cache[defn]
|
|
30
|
+
else
|
|
31
|
+
args = @types.arguments(defn)
|
|
32
|
+
required_argument_names = nil
|
|
33
|
+
if !args.empty?
|
|
34
|
+
args.each do |a|
|
|
35
|
+
if a.type.kind.non_null? && !a.default_value? && @types.argument(defn, a.name)
|
|
36
|
+
(required_argument_names ||= []) << a.graphql_name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
@required_args_cache[defn] = required_argument_names
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return if required_argument_names.nil?
|
|
44
|
+
|
|
45
|
+
present_argument_names = ast_node.arguments.map(&:name)
|
|
26
46
|
missing_names = required_argument_names - present_argument_names
|
|
27
47
|
if !missing_names.empty?
|
|
28
48
|
add_error(GraphQL::StaticValidation::RequiredArgumentsArePresentError.new(
|
|
@@ -7,17 +7,20 @@ module GraphQL
|
|
|
7
7
|
type = context.query.types.type(type_name)
|
|
8
8
|
|
|
9
9
|
if type.nil?
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
suggestion = if @schema.did_you_mean
|
|
11
|
+
@all_possible_input_type_names ||= begin
|
|
12
|
+
names = []
|
|
13
|
+
context.types.all_types.each { |(t)|
|
|
14
|
+
if t.kind.input?
|
|
15
|
+
names << t.graphql_name
|
|
16
|
+
end
|
|
17
|
+
}
|
|
18
|
+
names
|
|
19
|
+
end
|
|
20
|
+
context.did_you_mean_suggestion(type_name, @all_possible_input_type_names)
|
|
18
21
|
end
|
|
19
22
|
add_error(GraphQL::StaticValidation::VariablesAreInputTypesError.new(
|
|
20
|
-
"#{type_name} isn't a defined input type (on $#{node.name})#{
|
|
23
|
+
"#{type_name} isn't a defined input type (on $#{node.name})#{suggestion}",
|
|
21
24
|
nodes: node,
|
|
22
25
|
name: node.name,
|
|
23
26
|
type: type_name
|
|
@@ -32,7 +32,7 @@ module GraphQL
|
|
|
32
32
|
# TODO stop using def_delegators because of Array allocations
|
|
33
33
|
def_delegators :@visitor,
|
|
34
34
|
:path, :type_definition, :field_definition, :argument_definition,
|
|
35
|
-
:parent_type_definition, :directive_definition, :
|
|
35
|
+
:parent_type_definition, :directive_definition, :dependencies
|
|
36
36
|
|
|
37
37
|
def on_dependency_resolve(&handler)
|
|
38
38
|
@on_dependency_resolve_handlers << handler
|
|
@@ -17,10 +17,34 @@ module GraphQL
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def resolve_next(context:, objects:, arguments:)
|
|
21
|
+
has_override_implementation = @field.execution_next_mode != :direct_send
|
|
22
|
+
|
|
23
|
+
if !has_override_implementation
|
|
24
|
+
if context.query.subscription_update?
|
|
25
|
+
objects
|
|
26
|
+
else
|
|
27
|
+
objects.map { |o| context.skip }
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
yield(objects, arguments)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
20
34
|
def after_resolve(value:, context:, object:, arguments:, **rest)
|
|
35
|
+
self.class.write_subscription(@field, value, arguments, context)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def after_resolve_next(values:, context:, objects:, arguments:, **rest)
|
|
39
|
+
values.map do |value|
|
|
40
|
+
self.class.write_subscription(@field, value, arguments, context)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.write_subscription(field, value, arguments, context)
|
|
21
45
|
if value.is_a?(GraphQL::ExecutionError)
|
|
22
46
|
value
|
|
23
|
-
elsif
|
|
47
|
+
elsif field.resolver&.method_defined?(:subscription_written?) &&
|
|
24
48
|
(subscription_namespace = context.namespace(:subscriptions)) &&
|
|
25
49
|
(subscriptions_by_path = subscription_namespace[:subscriptions])
|
|
26
50
|
(subscription_instance = subscriptions_by_path[context.current_path])
|
|
@@ -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,
|
|
@@ -239,6 +239,26 @@ module GraphQL
|
|
|
239
239
|
query.context.namespace(:subscriptions)[:subscription_broadcastable]
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
+
# Called during execution when a new `subscription ...` operation is received
|
|
243
|
+
# @param query [GraphQL::Query]
|
|
244
|
+
# @return [void]
|
|
245
|
+
def initialize_subscriptions(query)
|
|
246
|
+
subs_namespace = query.context.namespace(:subscriptions)
|
|
247
|
+
subs_namespace[:events] = []
|
|
248
|
+
subs_namespace[:subscriptions] = {}
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Called during execution when a subscription operation has finished
|
|
253
|
+
# @param query [GraphQL::Query]
|
|
254
|
+
# @return [void]
|
|
255
|
+
def finish_subscriptions(query)
|
|
256
|
+
if (events = query.context.namespace(:subscriptions)[:events]) && !events.empty?
|
|
257
|
+
write_subscription(query, events)
|
|
258
|
+
end
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
|
|
242
262
|
private
|
|
243
263
|
|
|
244
264
|
# Recursively normalize `args` as belonging to `arg_owner`:
|
|
@@ -39,9 +39,9 @@ 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
46
|
dataloader = query_context.dataloader
|
|
47
47
|
object_type = dummy_query.types.type(type_name) # rubocop:disable Development/ContextIsPassedCop
|
|
@@ -104,32 +104,35 @@ module GraphQL
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
def with_resolution_context(schema, type:, object:, context:{})
|
|
107
|
+
def with_resolution_context(schema, type:, object:, context:{}, visibility_profile: nil)
|
|
108
108
|
resolution_context = ResolutionAssertionContext.new(
|
|
109
109
|
self,
|
|
110
110
|
schema: schema,
|
|
111
111
|
type_name: type,
|
|
112
112
|
object: object,
|
|
113
|
-
context: context
|
|
113
|
+
context: context,
|
|
114
|
+
visibility_profile: visibility_profile,
|
|
114
115
|
)
|
|
115
116
|
yield(resolution_context)
|
|
116
117
|
end
|
|
117
118
|
|
|
118
119
|
class ResolutionAssertionContext
|
|
119
|
-
def initialize(test, type_name:, object:, schema:, context:)
|
|
120
|
+
def initialize(test, type_name:, object:, schema:, context:, visibility_profile:)
|
|
120
121
|
@test = test
|
|
121
122
|
@type_name = type_name
|
|
122
123
|
@object = object
|
|
123
124
|
@schema = schema
|
|
124
125
|
@context = context
|
|
126
|
+
@visibility_profile = visibility_profile
|
|
125
127
|
end
|
|
126
128
|
|
|
129
|
+
attr_reader :visibility_profile
|
|
127
130
|
|
|
128
131
|
def run_graphql_field(field_name, arguments: {})
|
|
129
132
|
if @schema
|
|
130
|
-
@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)
|
|
131
134
|
else
|
|
132
|
-
@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)
|
|
133
136
|
end
|
|
134
137
|
end
|
|
135
138
|
end
|
|
@@ -137,8 +140,8 @@ module GraphQL
|
|
|
137
140
|
module SchemaHelpers
|
|
138
141
|
include Helpers
|
|
139
142
|
|
|
140
|
-
def run_graphql_field(field_path, object, arguments: {}, context: {})
|
|
141
|
-
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)
|
|
142
145
|
end
|
|
143
146
|
|
|
144
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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphQL
|
|
4
|
+
module Tracing
|
|
5
|
+
class DetailedTrace
|
|
6
|
+
class ActiveRecordBackend
|
|
7
|
+
class GraphqlDetailedTrace < ActiveRecord::Base
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(limit: nil, model_class: nil)
|
|
11
|
+
@limit = limit
|
|
12
|
+
@model_class = model_class || GraphqlDetailedTrace
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def traces(last:, before:)
|
|
16
|
+
gdts = @model_class.all.order("begin_ms DESC")
|
|
17
|
+
if before
|
|
18
|
+
gdts = gdts.where("begin_ms < ?", before)
|
|
19
|
+
end
|
|
20
|
+
if last
|
|
21
|
+
gdts = gdts.limit(last)
|
|
22
|
+
end
|
|
23
|
+
gdts.map { |gdt| record_to_stored_trace(gdt) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def delete_trace(id)
|
|
27
|
+
@model_class.where(id: id).destroy_all
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete_all_traces
|
|
32
|
+
@model_class.all.destroy_all
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def find_trace(id)
|
|
36
|
+
gdt = @model_class.find_by(id: id)
|
|
37
|
+
if gdt
|
|
38
|
+
record_to_stored_trace(gdt)
|
|
39
|
+
else
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
|
45
|
+
gdt = @model_class.create!(
|
|
46
|
+
begin_ms: begin_ms,
|
|
47
|
+
operation_name: operation_name,
|
|
48
|
+
duration_ms: duration_ms,
|
|
49
|
+
trace_data: trace_data,
|
|
50
|
+
)
|
|
51
|
+
if @limit
|
|
52
|
+
@model_class
|
|
53
|
+
.where("id NOT IN(SELECT id FROM graphql_detailed_traces ORDER BY begin_ms DESC LIMIT ?)", @limit)
|
|
54
|
+
.delete_all
|
|
55
|
+
end
|
|
56
|
+
gdt.id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def record_to_stored_trace(gdt)
|
|
62
|
+
StoredTrace.new(
|
|
63
|
+
id: gdt.id,
|
|
64
|
+
begin_ms: gdt.begin_ms,
|
|
65
|
+
operation_name: gdt.operation_name,
|
|
66
|
+
duration_ms: gdt.duration_ms,
|
|
67
|
+
trace_data: gdt.trace_data
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
if defined?(ActiveRecord)
|
|
3
|
+
require "graphql/tracing/detailed_trace/active_record_backend"
|
|
4
|
+
end
|
|
2
5
|
require "graphql/tracing/detailed_trace/memory_backend"
|
|
3
6
|
require "graphql/tracing/detailed_trace/redis_backend"
|
|
4
7
|
|
|
5
8
|
module GraphQL
|
|
6
9
|
module Tracing
|
|
7
|
-
# `DetailedTrace` can make detailed profiles for a subset of production traffic.
|
|
10
|
+
# `DetailedTrace` can make detailed profiles for a subset of production traffic. Install it in Rails with `rails generate graphql:detailed_trace`.
|
|
8
11
|
#
|
|
9
12
|
# When `MySchema.detailed_trace?(query)` returns `true`, a profiler-specific `trace_mode: ...` will be used for the query,
|
|
10
13
|
# overriding the one in `context[:trace_mode]`.
|
|
11
14
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
15
|
+
# By default, the detailed tracer calls `.inspect` on application objects returned from fields. You can customize
|
|
16
|
+
# this behavior by extending {DetailedTrace} and overriding {#inspect_object}. You can opt out of debug annotations
|
|
17
|
+
# entirely with `use ..., debug: false` or for a single query with `context: { detailed_trace_debug: false }`.
|
|
18
|
+
#
|
|
19
|
+
# You can store saved traces in two ways:
|
|
20
|
+
#
|
|
21
|
+
# - __ActiveRecord__: With `rails generate graphql:detailed_trace`, a new migration will be added to your app.
|
|
22
|
+
# That table will be used to store trace data.
|
|
23
|
+
#
|
|
24
|
+
# - __Redis__: Pass `redis: ...` to save trace data to a Redis database. Depending on your needs,
|
|
25
|
+
# you can configure this database to retain all data (persistent) or to expire data according to your rules.
|
|
26
|
+
#
|
|
14
27
|
# If you need to save traces indefinitely, you can download them from Perfetto after opening them there.
|
|
15
28
|
#
|
|
29
|
+
# @example Installing with Rails
|
|
30
|
+
# rails generate graphql:detailed_trace # optional: --redis
|
|
31
|
+
#
|
|
16
32
|
# @example Adding the sampler to your schema
|
|
17
33
|
# class MySchema < GraphQL::Schema
|
|
18
34
|
# # Add the sampler:
|
|
@@ -27,24 +43,48 @@ module GraphQL
|
|
|
27
43
|
# end
|
|
28
44
|
#
|
|
29
45
|
# @see Graphql::Dashboard GraphQL::Dashboard for viewing stored results
|
|
46
|
+
#
|
|
47
|
+
# @example Customizing debug output in traces
|
|
48
|
+
# class CustomDetailedTrace < GraphQL::Tracing::DetailedTrace
|
|
49
|
+
# def inspect_object(object)
|
|
50
|
+
# if object.is_a?(SomeThing)
|
|
51
|
+
# # handle it specially ...
|
|
52
|
+
# else
|
|
53
|
+
# super
|
|
54
|
+
# end
|
|
55
|
+
# end
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# @example disabling debug annotations completely
|
|
59
|
+
# use DetailedTrace, debug: false, ...
|
|
60
|
+
#
|
|
61
|
+
# @example disabling debug annotations for one query
|
|
62
|
+
# MySchema.execute(query_str, context: { detailed_trace_debug: false })
|
|
63
|
+
#
|
|
30
64
|
class DetailedTrace
|
|
31
65
|
# @param redis [Redis] If provided, profiles will be stored in Redis for later review
|
|
32
66
|
# @param limit [Integer] A maximum number of profiles to store
|
|
33
|
-
|
|
67
|
+
# @param debug [Boolean] if `false`, it won't create `debug` annotations in Perfetto traces (reduces overhead)
|
|
68
|
+
# @param model_class [Class<ActiveRecord::Base>] Overrides {ActiveRecordBackend::GraphqlDetailedTrace} if present
|
|
69
|
+
def self.use(schema, trace_mode: :profile_sample, memory: false, debug: debug?, redis: nil, limit: nil, model_class: nil)
|
|
34
70
|
storage = if redis
|
|
35
71
|
RedisBackend.new(redis: redis, limit: limit)
|
|
36
72
|
elsif memory
|
|
37
73
|
MemoryBackend.new(limit: limit)
|
|
74
|
+
elsif defined?(ActiveRecord)
|
|
75
|
+
ActiveRecordBackend.new(limit: limit, model_class: model_class)
|
|
38
76
|
else
|
|
39
|
-
raise ArgumentError, "
|
|
77
|
+
raise ArgumentError, "To store traces, install ActiveRecord or provide `redis: ...`"
|
|
40
78
|
end
|
|
41
|
-
|
|
79
|
+
detailed_trace = self.new(storage: storage, trace_mode: trace_mode, debug: debug)
|
|
80
|
+
schema.detailed_trace = detailed_trace
|
|
42
81
|
schema.trace_with(PerfettoTrace, mode: trace_mode, save_profile: true)
|
|
43
82
|
end
|
|
44
83
|
|
|
45
|
-
def initialize(storage:, trace_mode:)
|
|
84
|
+
def initialize(storage:, trace_mode:, debug:)
|
|
46
85
|
@storage = storage
|
|
47
86
|
@trace_mode = trace_mode
|
|
87
|
+
@debug = debug
|
|
48
88
|
end
|
|
49
89
|
|
|
50
90
|
# @return [Symbol] The trace mode to use when {Schema.detailed_trace?} returns `true`
|
|
@@ -55,6 +95,11 @@ module GraphQL
|
|
|
55
95
|
@storage.save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
|
56
96
|
end
|
|
57
97
|
|
|
98
|
+
# @return [Boolean]
|
|
99
|
+
def debug?
|
|
100
|
+
@debug
|
|
101
|
+
end
|
|
102
|
+
|
|
58
103
|
# @param last [Integer]
|
|
59
104
|
# @param before [Integer] Timestamp in milliseconds since epoch
|
|
60
105
|
# @return [Enumerable<StoredTrace>]
|
|
@@ -77,6 +122,24 @@ module GraphQL
|
|
|
77
122
|
@storage.delete_all_traces
|
|
78
123
|
end
|
|
79
124
|
|
|
125
|
+
def inspect_object(object)
|
|
126
|
+
self.class.inspect_object(object)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.inspect_object(object)
|
|
130
|
+
if defined?(ActiveRecord::Relation) && object.is_a?(ActiveRecord::Relation)
|
|
131
|
+
"#{object.class}, .to_sql=#{object.to_sql.inspect}"
|
|
132
|
+
else
|
|
133
|
+
object.inspect
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Default debug setting
|
|
138
|
+
# @return [true]
|
|
139
|
+
def self.debug?
|
|
140
|
+
true
|
|
141
|
+
end
|
|
142
|
+
|
|
80
143
|
class StoredTrace
|
|
81
144
|
def initialize(id:, operation_name:, duration_ms:, begin_ms:, trace_data:)
|
|
82
145
|
@id = id
|