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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  4. data/lib/graphql/dashboard/application_controller.rb +41 -0
  5. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  6. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  7. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  8. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  9. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  10. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  11. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  12. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  13. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  15. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  16. data/lib/graphql/dashboard.rb +11 -73
  17. data/lib/graphql/dataloader/async_dataloader.rb +22 -11
  18. data/lib/graphql/dataloader/null_dataloader.rb +48 -10
  19. data/lib/graphql/dataloader.rb +75 -23
  20. data/lib/graphql/date_encoding_error.rb +1 -1
  21. data/lib/graphql/execution/interpreter/resolve.rb +7 -13
  22. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  23. data/lib/graphql/execution/interpreter/runtime.rb +24 -18
  24. data/lib/graphql/execution/interpreter.rb +8 -22
  25. data/lib/graphql/execution/lazy.rb +1 -1
  26. data/lib/graphql/execution/multiplex.rb +1 -1
  27. data/lib/graphql/execution/next/field_resolve_step.rb +743 -0
  28. data/lib/graphql/execution/next/load_argument_step.rb +64 -0
  29. data/lib/graphql/execution/next/prepare_object_step.rb +129 -0
  30. data/lib/graphql/execution/next/runner.rb +411 -0
  31. data/lib/graphql/execution/next/selections_step.rb +37 -0
  32. data/lib/graphql/execution/next.rb +72 -0
  33. data/lib/graphql/execution.rb +8 -4
  34. data/lib/graphql/execution_error.rb +17 -10
  35. data/lib/graphql/introspection/directive_type.rb +7 -3
  36. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  37. data/lib/graphql/introspection/entry_points.rb +11 -3
  38. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  39. data/lib/graphql/introspection/field_type.rb +13 -5
  40. data/lib/graphql/introspection/input_value_type.rb +21 -13
  41. data/lib/graphql/introspection/type_type.rb +64 -28
  42. data/lib/graphql/invalid_null_error.rb +11 -5
  43. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  44. data/lib/graphql/language.rb +21 -12
  45. data/lib/graphql/pagination/connection.rb +2 -0
  46. data/lib/graphql/pagination/connections.rb +32 -0
  47. data/lib/graphql/query/context.rb +4 -3
  48. data/lib/graphql/query/null_context.rb +9 -3
  49. data/lib/graphql/schema/argument.rb +12 -0
  50. data/lib/graphql/schema/build_from_definition.rb +10 -1
  51. data/lib/graphql/schema/directive.rb +22 -4
  52. data/lib/graphql/schema/field/connection_extension.rb +15 -35
  53. data/lib/graphql/schema/field/scope_extension.rb +22 -13
  54. data/lib/graphql/schema/field.rb +79 -48
  55. data/lib/graphql/schema/field_extension.rb +33 -0
  56. data/lib/graphql/schema/list.rb +1 -1
  57. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -1
  58. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  59. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  60. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  61. data/lib/graphql/schema/member/has_fields.rb +86 -5
  62. data/lib/graphql/schema/member.rb +5 -0
  63. data/lib/graphql/schema/non_null.rb +1 -1
  64. data/lib/graphql/schema/object.rb +1 -0
  65. data/lib/graphql/schema/resolver.rb +60 -1
  66. data/lib/graphql/schema/subscription.rb +0 -2
  67. data/lib/graphql/schema/validator/required_validator.rb +33 -2
  68. data/lib/graphql/schema/visibility/profile.rb +68 -49
  69. data/lib/graphql/schema/visibility.rb +3 -3
  70. data/lib/graphql/schema/wrapper.rb +7 -1
  71. data/lib/graphql/schema.rb +53 -10
  72. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  73. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  74. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  75. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  76. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  77. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  78. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  79. data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
  80. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  81. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  82. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  83. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  84. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  85. data/lib/graphql/static_validation/validation_context.rb +1 -1
  86. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  87. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +25 -1
  88. data/lib/graphql/subscriptions/event.rb +1 -0
  89. data/lib/graphql/subscriptions.rb +21 -1
  90. data/lib/graphql/testing/helpers.rb +12 -9
  91. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  92. data/lib/graphql/testing.rb +1 -0
  93. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  94. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  95. data/lib/graphql/tracing/perfetto_trace.rb +209 -79
  96. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  97. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  98. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  99. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  100. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  101. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  102. data/lib/graphql/unauthorized_error.rb +9 -1
  103. data/lib/graphql/version.rb +1 -1
  104. data/lib/graphql.rb +8 -2
  105. metadata +17 -3
@@ -28,10 +28,10 @@ module GraphQL
28
28
  end
29
29
 
30
30
  def add_conflict(node, conflict_str)
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 }
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 = context.object_types[-2]
12
- fragment_child = context.object_types.last
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 = context.object_types.last
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
- @all_possible_fragment_type_names ||= begin
27
- names = []
28
- context.types.all_types.each do |type|
29
- if type.kind.fields?
30
- names << type.graphql_name
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
- names
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#{context.did_you_mean_suggestion(type_name, @all_possible_fragment_type_names)}",
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, field_definition)
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
- args = @context.query.types.arguments(defn)
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
- @all_possible_input_type_names ||= begin
11
- names = []
12
- context.types.all_types.each { |(t)|
13
- if t.kind.input?
14
- names << t.graphql_name
15
- end
16
- }
17
- names
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})#{context.did_you_mean_suggestion(type_name, @all_possible_input_type_names)}",
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, :object_types, :dependencies
35
+ :parent_type_definition, :directive_definition, :dependencies
36
36
 
37
37
  def on_dependency_resolve(&handler)
38
38
  @on_dependency_resolve_handlers << handler
@@ -81,6 +81,7 @@ module GraphQL
81
81
  # end
82
82
  # end
83
83
  #
84
+ # @see GraphQL::Testing::MockActionCable for test helpers
84
85
  class ActionCableSubscriptions < GraphQL::Subscriptions
85
86
  SUBSCRIPTION_PREFIX = "graphql-subscription:"
86
87
  EVENT_PREFIX = "graphql-event:"
@@ -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 @field.resolver&.method_defined?(:subscription_written?) &&
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])
@@ -104,6 +104,7 @@ module GraphQL
104
104
 
105
105
  def stringify_args(arg_owner, args, context)
106
106
  arg_owner = arg_owner.respond_to?(:unwrap) ? arg_owner.unwrap : arg_owner # remove list and non-null wrappers
107
+
107
108
  case args
108
109
  when Hash
109
110
  next_args = {}
@@ -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, GraphQL::Query::NullContext.instance)
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
@@ -1,2 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/testing/helpers"
3
+ require "graphql/testing/mock_action_cable"
@@ -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
- # __Redis__: The sampler stores its results in a provided Redis database. Depending on your needs,
13
- # You can configure this database to retail all data (persistent) or to expire data according to your rules.
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
- def self.use(schema, trace_mode: :profile_sample, memory: false, redis: nil, limit: nil)
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, "Pass `redis: ...` to store traces in Redis for later review"
77
+ raise ArgumentError, "To store traces, install ActiveRecord or provide `redis: ...`"
40
78
  end
41
- schema.detailed_trace = self.new(storage: storage, trace_mode: trace_mode)
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