graphql 1.6.8 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql.rb +5 -0
- data/lib/graphql/analysis/analyze_query.rb +21 -17
- data/lib/graphql/argument.rb +6 -2
- data/lib/graphql/backtrace.rb +50 -0
- data/lib/graphql/backtrace/inspect_result.rb +51 -0
- data/lib/graphql/backtrace/table.rb +120 -0
- data/lib/graphql/backtrace/traced_error.rb +55 -0
- data/lib/graphql/backtrace/tracer.rb +50 -0
- data/lib/graphql/enum_type.rb +1 -10
- data/lib/graphql/execution.rb +1 -2
- data/lib/graphql/execution/execute.rb +98 -89
- data/lib/graphql/execution/flatten.rb +40 -0
- data/lib/graphql/execution/lazy/resolve.rb +7 -7
- data/lib/graphql/execution/multiplex.rb +29 -29
- data/lib/graphql/field.rb +5 -1
- data/lib/graphql/internal_representation/node.rb +16 -0
- data/lib/graphql/invalid_name_error.rb +11 -0
- data/lib/graphql/language/parser.rb +11 -5
- data/lib/graphql/language/parser.y +11 -5
- data/lib/graphql/name_validator.rb +16 -0
- data/lib/graphql/object_type.rb +5 -0
- data/lib/graphql/query.rb +28 -7
- data/lib/graphql/query/context.rb +155 -52
- data/lib/graphql/query/literal_input.rb +36 -9
- data/lib/graphql/query/null_context.rb +7 -1
- data/lib/graphql/query/result.rb +63 -0
- data/lib/graphql/query/serial_execution/field_resolution.rb +3 -4
- data/lib/graphql/query/serial_execution/value_resolution.rb +3 -4
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/schema.rb +31 -0
- data/lib/graphql/schema/traversal.rb +16 -1
- data/lib/graphql/schema/warden.rb +40 -4
- data/lib/graphql/static_validation/validator.rb +20 -18
- data/lib/graphql/subscriptions.rb +129 -0
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +122 -0
- data/lib/graphql/subscriptions/event.rb +52 -0
- data/lib/graphql/subscriptions/instrumentation.rb +68 -0
- data/lib/graphql/tracing.rb +80 -0
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +31 -0
- data/lib/graphql/version.rb +1 -1
- data/readme.md +1 -1
- data/spec/graphql/analysis/analyze_query_spec.rb +19 -0
- data/spec/graphql/argument_spec.rb +28 -0
- data/spec/graphql/backtrace_spec.rb +144 -0
- data/spec/graphql/define/assign_argument_spec.rb +12 -0
- data/spec/graphql/enum_type_spec.rb +1 -1
- data/spec/graphql/execution/execute_spec.rb +66 -0
- data/spec/graphql/execution/lazy_spec.rb +4 -3
- data/spec/graphql/language/parser_spec.rb +16 -0
- data/spec/graphql/object_type_spec.rb +14 -0
- data/spec/graphql/query/context_spec.rb +134 -27
- data/spec/graphql/query/result_spec.rb +29 -0
- data/spec/graphql/query/variables_spec.rb +13 -0
- data/spec/graphql/query_spec.rb +22 -0
- data/spec/graphql/schema/build_from_definition_spec.rb +2 -0
- data/spec/graphql/schema/traversal_spec.rb +70 -12
- data/spec/graphql/schema/warden_spec.rb +67 -1
- data/spec/graphql/schema_spec.rb +29 -0
- data/spec/graphql/static_validation/validator_spec.rb +16 -0
- data/spec/graphql/subscriptions_spec.rb +331 -0
- data/spec/graphql/tracing/active_support_notifications_tracing_spec.rb +57 -0
- data/spec/graphql/tracing_spec.rb +47 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/star_wars/schema.rb +39 -0
- metadata +27 -4
- data/lib/graphql/execution/field_result.rb +0 -54
- 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
|
data/lib/graphql/version.rb
CHANGED
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 [
|
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
|