graphql 1.6.8 → 1.7.0

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +5 -0
  3. data/lib/graphql/analysis/analyze_query.rb +21 -17
  4. data/lib/graphql/argument.rb +6 -2
  5. data/lib/graphql/backtrace.rb +50 -0
  6. data/lib/graphql/backtrace/inspect_result.rb +51 -0
  7. data/lib/graphql/backtrace/table.rb +120 -0
  8. data/lib/graphql/backtrace/traced_error.rb +55 -0
  9. data/lib/graphql/backtrace/tracer.rb +50 -0
  10. data/lib/graphql/enum_type.rb +1 -10
  11. data/lib/graphql/execution.rb +1 -2
  12. data/lib/graphql/execution/execute.rb +98 -89
  13. data/lib/graphql/execution/flatten.rb +40 -0
  14. data/lib/graphql/execution/lazy/resolve.rb +7 -7
  15. data/lib/graphql/execution/multiplex.rb +29 -29
  16. data/lib/graphql/field.rb +5 -1
  17. data/lib/graphql/internal_representation/node.rb +16 -0
  18. data/lib/graphql/invalid_name_error.rb +11 -0
  19. data/lib/graphql/language/parser.rb +11 -5
  20. data/lib/graphql/language/parser.y +11 -5
  21. data/lib/graphql/name_validator.rb +16 -0
  22. data/lib/graphql/object_type.rb +5 -0
  23. data/lib/graphql/query.rb +28 -7
  24. data/lib/graphql/query/context.rb +155 -52
  25. data/lib/graphql/query/literal_input.rb +36 -9
  26. data/lib/graphql/query/null_context.rb +7 -1
  27. data/lib/graphql/query/result.rb +63 -0
  28. data/lib/graphql/query/serial_execution/field_resolution.rb +3 -4
  29. data/lib/graphql/query/serial_execution/value_resolution.rb +3 -4
  30. data/lib/graphql/query/variables.rb +1 -1
  31. data/lib/graphql/schema.rb +31 -0
  32. data/lib/graphql/schema/traversal.rb +16 -1
  33. data/lib/graphql/schema/warden.rb +40 -4
  34. data/lib/graphql/static_validation/validator.rb +20 -18
  35. data/lib/graphql/subscriptions.rb +129 -0
  36. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +122 -0
  37. data/lib/graphql/subscriptions/event.rb +52 -0
  38. data/lib/graphql/subscriptions/instrumentation.rb +68 -0
  39. data/lib/graphql/tracing.rb +80 -0
  40. data/lib/graphql/tracing/active_support_notifications_tracing.rb +31 -0
  41. data/lib/graphql/version.rb +1 -1
  42. data/readme.md +1 -1
  43. data/spec/graphql/analysis/analyze_query_spec.rb +19 -0
  44. data/spec/graphql/argument_spec.rb +28 -0
  45. data/spec/graphql/backtrace_spec.rb +144 -0
  46. data/spec/graphql/define/assign_argument_spec.rb +12 -0
  47. data/spec/graphql/enum_type_spec.rb +1 -1
  48. data/spec/graphql/execution/execute_spec.rb +66 -0
  49. data/spec/graphql/execution/lazy_spec.rb +4 -3
  50. data/spec/graphql/language/parser_spec.rb +16 -0
  51. data/spec/graphql/object_type_spec.rb +14 -0
  52. data/spec/graphql/query/context_spec.rb +134 -27
  53. data/spec/graphql/query/result_spec.rb +29 -0
  54. data/spec/graphql/query/variables_spec.rb +13 -0
  55. data/spec/graphql/query_spec.rb +22 -0
  56. data/spec/graphql/schema/build_from_definition_spec.rb +2 -0
  57. data/spec/graphql/schema/traversal_spec.rb +70 -12
  58. data/spec/graphql/schema/warden_spec.rb +67 -1
  59. data/spec/graphql/schema_spec.rb +29 -0
  60. data/spec/graphql/static_validation/validator_spec.rb +16 -0
  61. data/spec/graphql/subscriptions_spec.rb +331 -0
  62. data/spec/graphql/tracing/active_support_notifications_tracing_spec.rb +57 -0
  63. data/spec/graphql/tracing_spec.rb +47 -0
  64. data/spec/spec_helper.rb +32 -0
  65. data/spec/support/star_wars/schema.rb +39 -0
  66. metadata +27 -4
  67. data/lib/graphql/execution/field_result.rb +0 -54
  68. data/lib/graphql/execution/selection_result.rb +0 -90
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+ require "securerandom"
3
+
4
+ module GraphQL
5
+ class Subscriptions
6
+ # A subscriptions implementation that sends data
7
+ # as ActionCable broadcastings.
8
+ #
9
+ # Experimental, some things to keep in mind:
10
+ #
11
+ # - No queueing system; ActiveJob should be added
12
+ # - Take care to reload context when re-delivering the subscription. (see {Query#subscription_update?})
13
+ #
14
+ # @example Adding ActionCableSubscriptions to your schema
15
+ # MySchema = GraphQL::Schema.define do
16
+ # # ...
17
+ # use GraphQL::Subscriptions::ActionCableSubscriptions
18
+ # end
19
+ #
20
+ # @example Implementing a channel for GraphQL Subscriptions
21
+ # class GraphqlChannel < ApplicationCable::Channel
22
+ # def subscribed
23
+ # @subscription_ids = []
24
+ # end
25
+ #
26
+ # def execute(data)
27
+ # query = data["query"]
28
+ # variables = ensure_hash(data["variables"])
29
+ # operation_name = data["operationName"]
30
+ # context = {
31
+ # current_user: current_user,
32
+ # # Make sure the channel is in the context
33
+ # channel: self,
34
+ # }
35
+ #
36
+ # result = MySchema.execute({
37
+ # query: query,
38
+ # context: context,
39
+ # variables: variables,
40
+ # operation_name: operation_name
41
+ # })
42
+ #
43
+ # payload = {
44
+ # result: result.to_h,
45
+ # more: result.subscription?,
46
+ # }
47
+ #
48
+ # # Track the subscription here so we can remove it
49
+ # # on unsubscribe.
50
+ # if result.context[:subscription_id]
51
+ # @subscription_ids << context[:subscription_id]
52
+ # end
53
+ #
54
+ # transmit(payload)
55
+ # end
56
+ #
57
+ # def unsubscribed
58
+ # @subscription_ids.each { |sid|
59
+ # CardsSchema.subscriptions.delete_subscription(sid)
60
+ # }
61
+ # end
62
+ # end
63
+ #
64
+ class ActionCableSubscriptions < GraphQL::Subscriptions
65
+ SUBSCRIPTION_PREFIX = "graphql-subscription:"
66
+ EVENT_PREFIX = "graphql-event:"
67
+ def initialize(**rest)
68
+ # A per-process map of subscriptions to deliver.
69
+ # This is provided by Rails, so let's use it
70
+ @subscriptions = Concurrent::Map.new
71
+ super
72
+ end
73
+
74
+ # An event was triggered; Push the data over ActionCable.
75
+ # Subscribers will re-evaluate locally.
76
+ # TODO: this method name is a smell
77
+ def execute_all(event, object)
78
+ ActionCable.server.broadcast(EVENT_PREFIX + event.topic, object)
79
+ end
80
+
81
+ # This subscription was re-evaluated.
82
+ # Send it to the specific stream where this client was waiting.
83
+ def deliver(subscription_id, result)
84
+ payload = { result: result.to_h, more: true }
85
+ ActionCable.server.broadcast(SUBSCRIPTION_PREFIX + subscription_id, payload)
86
+ end
87
+
88
+ # A query was run where these events were subscribed to.
89
+ # Store them in memory in _this_ ActionCable frontend.
90
+ # It will receive notifications when events come in
91
+ # and re-evaluate the query locally.
92
+ def write_subscription(query, events)
93
+ channel = query.context[:channel]
94
+ subscription_id = query.context[:subscription_id] ||= SecureRandom.uuid
95
+ channel.stream_from(SUBSCRIPTION_PREFIX + subscription_id)
96
+ @subscriptions[subscription_id] = query
97
+ events.each do |event|
98
+ channel.stream_from(EVENT_PREFIX + event.topic, coder: ActiveSupport::JSON) do |message|
99
+ execute(subscription_id, event, message)
100
+ nil
101
+ end
102
+ end
103
+ end
104
+
105
+ # Return the query from "storage" (in memory)
106
+ def read_subscription(subscription_id)
107
+ query = @subscriptions[subscription_id]
108
+ {
109
+ query_string: query.query_string,
110
+ variables: query.provided_variables,
111
+ context: query.context.to_h,
112
+ operation_name: query.operation_name,
113
+ }
114
+ end
115
+
116
+ # The channel was closed, forget about it.
117
+ def delete_subscription(subscription_id)
118
+ @subscriptions.delete(subscription_id)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Subscriptions
4
+ # This thing can be:
5
+ # - Subscribed to by `subscription { ... }`
6
+ # - Triggered by `MySchema.subscriber.trigger(name, arguments, obj)`
7
+ #
8
+ # An array of `Event`s are passed to `store.register(query, events)`.
9
+ class Event
10
+ # @return [String] Corresponds to the Subscription root field name
11
+ attr_reader :name
12
+
13
+ # @return [GraphQL::Query::Arguments]
14
+ attr_reader :arguments
15
+
16
+ # @return [GraphQL::Query::Context]
17
+ attr_reader :context
18
+
19
+ # @return [String] An opaque string which identifies this event, derived from `name` and `arguments`
20
+ attr_reader :topic
21
+
22
+ def initialize(name:, arguments:, field: nil, context: nil, scope: nil)
23
+ @name = name
24
+ @arguments = arguments
25
+ @context = context
26
+ field ||= context.field
27
+ scope_val = scope || (context && field.subscription_scope && context[field.subscription_scope])
28
+
29
+ @topic = self.class.serialize(name, arguments, field, scope: scope_val)
30
+ end
31
+
32
+ # @return [String] an identifier for this unit of subscription
33
+ def self.serialize(name, arguments, field, scope:)
34
+ normalized_args = case arguments
35
+ when GraphQL::Query::Arguments
36
+ arguments
37
+ when Hash
38
+ GraphQL::Query::LiteralInput.from_arguments(
39
+ arguments,
40
+ field.arguments,
41
+ nil,
42
+ )
43
+ else
44
+ raise ArgumentError, "Unexpected arguments: #{arguments}, must be Hash or GraphQL::Arguments"
45
+ end
46
+
47
+ sorted_h = normalized_args.to_h.sort.to_h
48
+ JSON.dump([scope, name, sorted_h])
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+ # test_via: ../subscriptions.rb
3
+ module GraphQL
4
+ class Subscriptions
5
+ # Wrap the root fields of the subscription type with special logic for:
6
+ # - Registering the subscription during the first execution
7
+ # - Evaluating the triggered portion(s) of the subscription during later execution
8
+ class Instrumentation
9
+ def initialize(schema:)
10
+ @schema = schema
11
+ end
12
+
13
+ def instrument(type, field)
14
+ if type == @schema.subscription
15
+ # This is a root field of `subscription`
16
+ subscribing_resolve_proc = SubscriptionRegistrationResolve.new(field.resolve_proc)
17
+ field.redefine(resolve: subscribing_resolve_proc)
18
+ else
19
+ field
20
+ end
21
+ end
22
+
23
+ # If needed, prepare to gather events which this query subscribes to
24
+ def before_query(query)
25
+ if query.subscription? && !query.subscription_update?
26
+ query.context.namespace(:subscriptions)[:events] = []
27
+ end
28
+ end
29
+
30
+ # After checking the root fields, pass the gathered events to the store
31
+ def after_query(query)
32
+ events = query.context.namespace(:subscriptions)[:events]
33
+ if events && events.any?
34
+ @schema.subscriptions.write_subscription(query, events)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ class SubscriptionRegistrationResolve
41
+ def initialize(inner_proc)
42
+ @inner_proc = inner_proc
43
+ end
44
+
45
+ # Wrap the proc with subscription registration logic
46
+ def call(obj, args, ctx)
47
+ events = ctx.namespace(:subscriptions)[:events]
48
+ if events
49
+ # This is the first execution, so gather an Event
50
+ # for the backend to register:
51
+ events << Subscriptions::Event.new(
52
+ name: ctx.field.name,
53
+ arguments: args,
54
+ context: ctx,
55
+ )
56
+ ctx.skip
57
+ elsif ctx.irep_node.subscription_topic == ctx.query.subscription_topic
58
+ # The root object is _already_ the subscription update:
59
+ @inner_proc.call(obj, args, ctx)
60
+ else
61
+ # This is a subscription update, but this event wasn't triggered.
62
+ ctx.skip
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+ require "graphql/tracing/active_support_notifications_tracing"
3
+ module GraphQL
4
+ # Library entry point for performance metric reporting.
5
+ #
6
+ # {ActiveSupportNotificationsTracing} is imported by default
7
+ # when `ActiveSupport::Notifications` is found.
8
+ #
9
+ # You can remove it with `GraphQL::Tracing.uninstall(GraphQL::Tracing::ActiveSupportNotificationsTracing)`.
10
+ #
11
+ # __Warning:__ Installing/uninstalling tracers is not thread-safe. Do it during application boot only.
12
+ #
13
+ # @example Sending custom events
14
+ # GraphQL::Tracing.trace("my_custom_event", { ... }) do
15
+ # # do stuff ...
16
+ # end
17
+ #
18
+ # Events:
19
+ #
20
+ # Key | Metadata
21
+ # ----|---------
22
+ # lex | `{ query_string: String }`
23
+ # parse | `{ query_string: String }`
24
+ # validate | `{ query: GraphQL::Query, validate: Boolean }`
25
+ # analyze_multiplex | `{ multiplex: GraphQL::Execution::Multiplex }`
26
+ # analyze_query | `{ query: GraphQL::Query }`
27
+ # execute_multiplex | `{ query: GraphQL::Execution::Multiplex }`
28
+ # execute_query | `{ query: GraphQL::Query }`
29
+ # execute_query_lazy | `{ query: GraphQL::Query?, queries: Array<GraphQL::Query>? }`
30
+ # execute_field | `{ context: GraphQL::Query::Context::FieldResolutionContext }`
31
+ # execute_field_lazy | `{ context: GraphQL::Query::Context::FieldResolutionContext }`
32
+ #
33
+ module Tracing
34
+ class << self
35
+ # Override this method to do stuff
36
+ # @param key [String] The name of the event in GraphQL internals
37
+ # @param metadata [Hash] Event-related metadata (can be anything)
38
+ # @return [Object] Must return the value of the block
39
+ def trace(key, metadata)
40
+ call_tracers(0, key, metadata) { yield }
41
+ end
42
+
43
+ # Install a tracer to receive events.
44
+ # @param tracer [<#trace(key, metadata)>]
45
+ # @return [void]
46
+ def install(tracer)
47
+ if !tracers.include?(tracer)
48
+ @tracers << tracer
49
+ end
50
+ end
51
+
52
+ def uninstall(tracer)
53
+ @tracers.delete(tracer)
54
+ end
55
+
56
+ def tracers
57
+ @tracers ||= []
58
+ end
59
+
60
+ private
61
+
62
+ # If there's a tracer at `idx`, call it and then increment `idx`.
63
+ # Otherwise, yield.
64
+ #
65
+ # @param idx [Integer] Which tracer to call
66
+ # @param key [String] The current event name
67
+ # @param metadata [Object] The current event object
68
+ # @return Whatever the block returns
69
+ def call_tracers(idx, key, metadata)
70
+ if idx == @tracers.length
71
+ yield
72
+ else
73
+ @tracers[idx].trace(key, metadata) { call_tracers(idx + 1, key, metadata) { yield } }
74
+ end
75
+ end
76
+ end
77
+ # Initialize the array
78
+ tracers
79
+ end
80
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ # This implementation forwards events to ActiveSupport::Notifications
6
+ # with a `graphql.` prefix.
7
+ #
8
+ # Installed automatically when `ActiveSupport::Notifications` is discovered.
9
+ module ActiveSupportNotificationsTracing
10
+ # A cache of frequently-used keys to avoid needless string allocations
11
+ KEYS = {
12
+ "lex" => "graphql.lex",
13
+ "parse" => "graphql.parse",
14
+ "validate" => "graphql.validate",
15
+ "analyze_multiplex" => "graphql.analyze_multiplex",
16
+ "analyze_query" => "graphql.analyze_query",
17
+ "execute_query" => "graphql.execute_query",
18
+ "execute_query_lazy" => "graphql.execute_query_lazy",
19
+ "execute_field" => "graphql.execute_field",
20
+ "execute_field_lazy" => "graphql.execute_field_lazy",
21
+ }
22
+
23
+ def self.trace(key, metadata)
24
+ prefixed_key = KEYS[key] || "graphql.#{key}"
25
+ ActiveSupport::Notifications.instrument(prefixed_key, metadata) do
26
+ yield
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.6.8"
3
+ VERSION = "1.7.0"
4
4
  end
data/readme.md CHANGED
@@ -37,7 +37,7 @@ Or, see ["Getting Started"](https://rmosolgo.github.io/graphql-ruby/).
37
37
 
38
38
  ## Upgrade
39
39
 
40
- I also sell [GraphQL::Pro](http://graphql.pro) which provides several features on top of the GraphQL runtime, including [authorization](http://rmosolgo.github.io/graphql-ruby/pro/authorization), [monitoring](http://rmosolgo.github.io/graphql-ruby/pro/monitoring) plugins and [static queries](http://rmosolgo.github.io/graphql-ruby/pro/persisted_queries). Besides that, Pro customers get email support and an opportunity to support graphql-ruby's development!
40
+ I also sell [GraphQL::Pro](http://graphql.pro) which provides several features on top of the GraphQL runtime, including [authorization](http://rmosolgo.github.io/graphql-ruby/pro/authorization), [monitoring](http://rmosolgo.github.io/graphql-ruby/pro/monitoring) plugins and [persisted queries](http://rmosolgo.github.io/graphql-ruby/operation_store/overview). Besides that, Pro customers get email support and an opportunity to support graphql-ruby's development!
41
41
 
42
42
  ## Goals
43
43
 
@@ -49,6 +49,25 @@ describe GraphQL::Analysis do
49
49
  assert_equal expected_node_counts, node_counts
50
50
  end
51
51
 
52
+ describe "tracing" do
53
+ let(:query_string) { "{ t: __typename }"}
54
+
55
+ it "emits traces" do
56
+ traces = TestTracing.with_trace do
57
+ Dummy::Schema.execute(document: GraphQL.parse(query_string))
58
+ end
59
+
60
+ # The query_trace is on the list _first_ because it finished first
61
+ _lex, _parse, _validate, query_trace, multiplex_trace, *_rest = traces
62
+
63
+ assert_equal "analyze_multiplex", multiplex_trace[:key]
64
+ assert_instance_of GraphQL::Execution::Multiplex, multiplex_trace[:multiplex]
65
+
66
+ assert_equal "analyze_query", query_trace[:key]
67
+ assert_instance_of GraphQL::Query, query_trace[:query]
68
+ end
69
+ end
70
+
52
71
  describe "when a variable is missing" do
53
72
  let(:query_string) {%|
54
73
  query something($cheeseId: Int!){
@@ -19,6 +19,34 @@ describe GraphQL::Argument do
19
19
  assert_includes err.message, expected_error
20
20
  end
21
21
 
22
+ describe ".from_dsl" do
23
+ it "accepts an existing argument" do
24
+ existing = GraphQL::Argument.define do
25
+ name "bar"
26
+ type GraphQL::STRING_TYPE
27
+ end
28
+
29
+ arg = GraphQL::Argument.from_dsl(:foo, existing)
30
+
31
+ assert_equal "foo", arg.name
32
+ assert_equal GraphQL::STRING_TYPE, arg.type
33
+ end
34
+
35
+ it "creates an argument from dsl arguments" do
36
+ arg = GraphQL::Argument.from_dsl(
37
+ :foo,
38
+ GraphQL::STRING_TYPE,
39
+ "A Description",
40
+ default_value: "Bar"
41
+ )
42
+
43
+ assert_equal "foo", arg.name
44
+ assert_equal GraphQL::STRING_TYPE, arg.type
45
+ assert_equal "A Description", arg.description
46
+ assert_equal "Bar", arg.default_value
47
+ end
48
+ end
49
+
22
50
  it "accepts custom keywords" do
23
51
  type = GraphQL::ObjectType.define do
24
52
  name "Something"
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe GraphQL::Backtrace do
5
+ before {
6
+ GraphQL::Backtrace.enable
7
+ }
8
+
9
+ after {
10
+ GraphQL::Backtrace.disable
11
+ }
12
+
13
+ class LazyError
14
+ def raise_err
15
+ raise "Lazy Boom"
16
+ end
17
+ end
18
+
19
+ let(:resolvers) {
20
+ {
21
+ "Query" => {
22
+ "field1" => Proc.new { :something },
23
+ "field2" => Proc.new { :something },
24
+ },
25
+ "Thing" => {
26
+ "listField" => Proc.new { :not_a_list },
27
+ "raiseField" => Proc.new { |o, a| raise("This is broken: #{a[:message]}") },
28
+ },
29
+ "OtherThing" => {
30
+ "strField" => Proc.new { LazyError.new },
31
+ },
32
+ }
33
+ }
34
+ let(:schema) {
35
+ defn = <<-GRAPHQL
36
+ type Query {
37
+ field1: Thing
38
+ field2: OtherThing
39
+ }
40
+
41
+ type Thing {
42
+ listField: [OtherThing]
43
+ raiseField(message: String!): Int
44
+ }
45
+
46
+ type OtherThing {
47
+ strField: String
48
+ }
49
+ GRAPHQL
50
+ GraphQL::Schema.from_definition(defn, default_resolve: resolvers).redefine {
51
+ lazy_resolve(LazyError, :raise_err)
52
+ }
53
+ }
54
+
55
+ describe "GraphQL backtrace helpers" do
56
+ it "raises a TracedError when enabled" do
57
+ assert_raises(GraphQL::Backtrace::TracedError) {
58
+ schema.execute("query BrokenList { field1 { listField { strField } } }")
59
+ }
60
+
61
+ GraphQL::Backtrace.disable
62
+
63
+ assert_raises(NoMethodError) {
64
+ schema.execute("query BrokenList { field1 { listField { strField } } }")
65
+ }
66
+ end
67
+
68
+ it "annotates crashes from user code" do
69
+ err = assert_raises(GraphQL::Backtrace::TracedError) {
70
+ schema.execute <<-GRAPHQL, root_value: "Root"
71
+ query($msg: String = \"Boom\") {
72
+ field1 {
73
+ boomError: raiseField(message: $msg)
74
+ }
75
+ }
76
+ GRAPHQL
77
+ }
78
+
79
+ # The original error info is present
80
+ assert_instance_of RuntimeError, err.cause
81
+ b = err.cause.backtrace
82
+ assert_backtrace_includes(b, file: "backtrace_spec.rb", method: "block")
83
+ assert_backtrace_includes(b, file: "execute.rb", method: "resolve_field")
84
+ assert_backtrace_includes(b, file: "execute.rb", method: "resolve_field")
85
+ assert_backtrace_includes(b, file: "execute.rb", method: "resolve_root_selection")
86
+
87
+ # GraphQL backtrace is present
88
+ expected_graphql_backtrace = [
89
+ "3:13: Thing.raiseField as boomError",
90
+ "2:11: Query.field1",
91
+ "1:9: query",
92
+ ]
93
+ assert_equal expected_graphql_backtrace, err.graphql_backtrace
94
+
95
+ # The message includes the GraphQL context
96
+ rendered_table = [
97
+ 'Loc | Field | Object | Arguments | Result',
98
+ '3:13 | Thing.raiseField as boomError | :something | {"message"=>"Boom"} | #<RuntimeError: This is broken: Boom>',
99
+ '2:11 | Query.field1 | "Root" | {} | {}',
100
+ '1:9 | query | "Root" | {"msg"=>"Boom"} | ',
101
+ ].join("\n")
102
+
103
+ assert_includes err.message, rendered_table
104
+ # The message includes the original error message
105
+ assert_includes err.message, "This is broken: Boom"
106
+ assert_includes err.message, "spec/graphql/backtrace_spec.rb:27", "It includes the original backtrace"
107
+ assert_includes err.message, "more lines"
108
+ end
109
+
110
+ it "annotates errors inside lazy resolution" do
111
+ err = assert_raises(GraphQL::Backtrace::TracedError) {
112
+ schema.execute("query StrField { field2 { strField } __typename }")
113
+ }
114
+ assert_instance_of RuntimeError, err.cause
115
+ b = err.cause.backtrace
116
+ assert_backtrace_includes(b, file: "backtrace_spec.rb", method: "raise_err")
117
+ assert_backtrace_includes(b, file: "field.rb", method: "lazy_resolve")
118
+ assert_backtrace_includes(b, file: "lazy/resolve.rb", method: "block")
119
+
120
+ expected_graphql_backtrace = [
121
+ "1:27: OtherThing.strField",
122
+ "1:18: Query.field2",
123
+ "1:1: query StrField",
124
+ ]
125
+
126
+ assert_equal(expected_graphql_backtrace, err.graphql_backtrace)
127
+
128
+ rendered_table = [
129
+ 'Loc | Field | Object | Arguments | Result',
130
+ '1:27 | OtherThing.strField | :something | {} | #<RuntimeError: Lazy Boom>',
131
+ '1:18 | Query.field2 | nil | {} | {strField: (unresolved)}',
132
+ '1:1 | query StrField | nil | {} | {field2: {...}, __typename: "Query"}',
133
+ ].join("\n")
134
+ assert_includes err.message, rendered_table
135
+ end
136
+ end
137
+
138
+ # This will get brittle when execution code moves between files
139
+ # but I'm not sure how to be sure that the backtrace contains the right stuff!
140
+ def assert_backtrace_includes(backtrace, file:, method:)
141
+ includes_tag = backtrace.any? { |s| s.include?(file) && s.include?("`" + method) }
142
+ assert includes_tag, "Backtrace should include #{file} inside method #{method}"
143
+ end
144
+ end