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.
- 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
|