graphql 1.12.3 → 1.12.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/graphql/install_generator.rb +4 -1
- data/lib/generators/graphql/loader_generator.rb +1 -0
- data/lib/generators/graphql/mutation_generator.rb +1 -0
- data/lib/generators/graphql/relay.rb +55 -0
- data/lib/generators/graphql/relay_generator.rb +4 -46
- data/lib/generators/graphql/type_generator.rb +1 -0
- data/lib/graphql.rb +4 -2
- data/lib/graphql/backtrace/inspect_result.rb +0 -1
- data/lib/graphql/backtrace/table.rb +0 -1
- data/lib/graphql/backtrace/traced_error.rb +0 -1
- data/lib/graphql/backtrace/tracer.rb +4 -8
- data/lib/graphql/dataloader.rb +102 -92
- data/lib/graphql/dataloader/null_dataloader.rb +5 -5
- data/lib/graphql/dataloader/request.rb +1 -6
- data/lib/graphql/dataloader/request_all.rb +1 -4
- data/lib/graphql/dataloader/source.rb +20 -6
- data/lib/graphql/execution/errors.rb +109 -11
- data/lib/graphql/execution/interpreter.rb +2 -2
- data/lib/graphql/execution/interpreter/arguments_cache.rb +37 -14
- data/lib/graphql/execution/interpreter/resolve.rb +33 -25
- data/lib/graphql/execution/interpreter/runtime.rb +41 -78
- data/lib/graphql/execution/multiplex.rb +21 -22
- data/lib/graphql/introspection.rb +1 -1
- data/lib/graphql/introspection/directive_type.rb +7 -3
- data/lib/graphql/language.rb +1 -0
- data/lib/graphql/language/cache.rb +37 -0
- data/lib/graphql/language/parser.rb +15 -5
- data/lib/graphql/language/parser.y +15 -5
- data/lib/graphql/object_type.rb +0 -2
- data/lib/graphql/pagination/active_record_relation_connection.rb +7 -0
- data/lib/graphql/pagination/connection.rb +15 -1
- data/lib/graphql/pagination/connections.rb +1 -0
- data/lib/graphql/pagination/relation_connection.rb +12 -1
- data/lib/graphql/parse_error.rb +0 -1
- data/lib/graphql/query.rb +9 -5
- data/lib/graphql/query/arguments_cache.rb +0 -1
- data/lib/graphql/query/context.rb +1 -3
- data/lib/graphql/query/executor.rb +0 -1
- data/lib/graphql/query/null_context.rb +3 -2
- data/lib/graphql/query/validation_pipeline.rb +1 -1
- data/lib/graphql/query/variable_validation_error.rb +1 -1
- data/lib/graphql/railtie.rb +9 -1
- data/lib/graphql/relay/range_add.rb +10 -5
- data/lib/graphql/schema.rb +14 -27
- data/lib/graphql/schema/argument.rb +61 -0
- data/lib/graphql/schema/field.rb +10 -5
- data/lib/graphql/schema/field/connection_extension.rb +1 -0
- data/lib/graphql/schema/find_inherited_value.rb +3 -1
- data/lib/graphql/schema/input_object.rb +6 -2
- data/lib/graphql/schema/member/has_arguments.rb +43 -56
- data/lib/graphql/schema/member/has_fields.rb +1 -4
- data/lib/graphql/schema/member/instrumentation.rb +0 -1
- data/lib/graphql/schema/resolver.rb +28 -1
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +3 -1
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +6 -2
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
- data/lib/graphql/static_validation/rules/arguments_are_defined_error.rb +4 -2
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +0 -3
- data/lib/graphql/subscriptions/event.rb +0 -1
- data/lib/graphql/subscriptions/instrumentation.rb +0 -1
- data/lib/graphql/subscriptions/serialize.rb +3 -1
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +2 -1
- data/lib/graphql/types/relay/base_connection.rb +4 -0
- data/lib/graphql/types/relay/connection_behaviors.rb +38 -5
- data/lib/graphql/types/relay/edge_behaviors.rb +12 -1
- data/lib/graphql/version.rb +1 -1
- data/readme.md +1 -1
- metadata +8 -90
@@ -7,15 +7,15 @@ module GraphQL
|
|
7
7
|
# The Dataloader interface isn't public, but it enables
|
8
8
|
# simple internal code while adding the option to add Dataloader.
|
9
9
|
class NullDataloader < Dataloader
|
10
|
-
def enqueue
|
11
|
-
yield
|
12
|
-
end
|
13
|
-
|
14
10
|
# These are all no-ops because code was
|
15
11
|
# executed sychronously.
|
16
12
|
def run; end
|
17
13
|
def yield; end
|
18
|
-
|
14
|
+
|
15
|
+
def append_job
|
16
|
+
yield
|
17
|
+
nil
|
18
|
+
end
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
@@ -3,9 +3,6 @@
|
|
3
3
|
module GraphQL
|
4
4
|
class Dataloader
|
5
5
|
class Source
|
6
|
-
# @api private
|
7
|
-
attr_reader :results
|
8
|
-
|
9
6
|
# Called by {Dataloader} to prepare the {Source}'s internal state
|
10
7
|
# @api private
|
11
8
|
def setup(dataloader)
|
@@ -35,11 +32,11 @@ module GraphQL
|
|
35
32
|
# @return [Object] The result from {#fetch} for `key`. If `key` hasn't been loaded yet, the Fiber will yield until it's loaded.
|
36
33
|
def load(key)
|
37
34
|
if @results.key?(key)
|
38
|
-
|
35
|
+
result_for(key)
|
39
36
|
else
|
40
37
|
@pending_keys << key
|
41
38
|
sync
|
42
|
-
|
39
|
+
result_for(key)
|
43
40
|
end
|
44
41
|
end
|
45
42
|
|
@@ -52,7 +49,7 @@ module GraphQL
|
|
52
49
|
sync
|
53
50
|
end
|
54
51
|
|
55
|
-
keys.map { |k|
|
52
|
+
keys.map { |k| result_for(k) }
|
56
53
|
end
|
57
54
|
|
58
55
|
# Subclasses must implement this method to return a value for each of `keys`
|
@@ -86,8 +83,25 @@ module GraphQL
|
|
86
83
|
fetch_keys.each_with_index do |key, idx|
|
87
84
|
@results[key] = results[idx]
|
88
85
|
end
|
86
|
+
rescue StandardError => error
|
87
|
+
fetch_keys.each { |key| @results[key] = error }
|
88
|
+
ensure
|
89
89
|
nil
|
90
90
|
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
|
95
|
+
# @param key [Object] key passed to {#load} or {#load_all}
|
96
|
+
# @return [Object] The result from {#fetch} for `key`.
|
97
|
+
# @api private
|
98
|
+
def result_for(key)
|
99
|
+
result = @results[key]
|
100
|
+
|
101
|
+
raise result if result.class <= StandardError
|
102
|
+
|
103
|
+
result
|
104
|
+
end
|
91
105
|
end
|
92
106
|
end
|
93
107
|
end
|
@@ -18,21 +18,83 @@ module GraphQL
|
|
18
18
|
#
|
19
19
|
class Errors
|
20
20
|
def self.use(schema)
|
21
|
-
|
22
|
-
|
23
|
-
GraphQL::Deprecation.warn("GraphQL::Execution::Errors is now installed by default, remove `use GraphQL::Execution::Errors` from #{definition_line}")
|
24
|
-
end
|
25
|
-
schema.error_handler = self.new(schema)
|
21
|
+
definition_line = caller(2, 1).first
|
22
|
+
GraphQL::Deprecation.warn("GraphQL::Execution::Errors is now installed by default, remove `use GraphQL::Execution::Errors` from #{definition_line}")
|
26
23
|
end
|
27
24
|
|
25
|
+
NEW_HANDLER_HASH = ->(h, k) {
|
26
|
+
h[k] = {
|
27
|
+
class: k,
|
28
|
+
handler: nil,
|
29
|
+
subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
28
33
|
def initialize(schema)
|
29
34
|
@schema = schema
|
35
|
+
@handlers = {
|
36
|
+
class: nil,
|
37
|
+
handler: nil,
|
38
|
+
subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# @api private
|
43
|
+
def each_rescue
|
44
|
+
handlers = @handlers.values
|
45
|
+
while (handler = handlers.shift) do
|
46
|
+
yield(handler[:class], handler[:handler])
|
47
|
+
handlers.concat(handler[:subclass_handlers].values)
|
48
|
+
end
|
30
49
|
end
|
31
50
|
|
32
|
-
|
33
|
-
|
34
|
-
|
51
|
+
# Register this handler, updating the
|
52
|
+
# internal handler index to maintain least-to-most specific.
|
53
|
+
#
|
54
|
+
# @param error_class [Class<Exception>]
|
55
|
+
# @param error_handler [Proc]
|
56
|
+
# @return [void]
|
57
|
+
def rescue_from(error_class, error_handler)
|
58
|
+
subclasses_handlers = {}
|
59
|
+
this_level_subclasses = []
|
60
|
+
# During this traversal, do two things:
|
61
|
+
# - Identify any already-registered subclasses of this error class
|
62
|
+
# and gather them up to be inserted _under_ this class
|
63
|
+
# - Find the point in the index where this handler should be inserted
|
64
|
+
# (That is, _under_ any superclasses, or at top-level, if there are no superclasses registered)
|
65
|
+
handlers = @handlers[:subclass_handlers]
|
66
|
+
while (handlers) do
|
67
|
+
this_level_subclasses.clear
|
68
|
+
# First, identify already-loaded handlers that belong
|
69
|
+
# _under_ this one. (That is, they're handlers
|
70
|
+
# for subclasses of `error_class`.)
|
71
|
+
handlers.each do |err_class, handler|
|
72
|
+
if err_class < error_class
|
73
|
+
subclasses_handlers[err_class] = handler
|
74
|
+
this_level_subclasses << err_class
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# Any handlers that we'll be moving, delete them from this point in the index
|
78
|
+
this_level_subclasses.each do |err_class|
|
79
|
+
handlers.delete(err_class)
|
80
|
+
end
|
81
|
+
|
82
|
+
# See if any keys in this hash are superclasses of this new class:
|
83
|
+
next_index_point = handlers.find { |err_class, handler| error_class < err_class }
|
84
|
+
if next_index_point
|
85
|
+
handlers = next_index_point[1][:subclass_handlers]
|
86
|
+
else
|
87
|
+
# this new handler doesn't belong to any sub-handlers,
|
88
|
+
# so insert it in the current set of `handlers`
|
89
|
+
break
|
90
|
+
end
|
35
91
|
end
|
92
|
+
# Having found the point at which to insert this handler,
|
93
|
+
# register it and merge any subclass handlers back in at this point.
|
94
|
+
this_class_handlers = handlers[error_class]
|
95
|
+
this_class_handlers[:handler] = error_handler
|
96
|
+
this_class_handlers[:subclass_handlers].merge!(subclasses_handlers)
|
97
|
+
nil
|
36
98
|
end
|
37
99
|
|
38
100
|
# Call the given block with the schema's configured error handlers.
|
@@ -44,8 +106,7 @@ module GraphQL
|
|
44
106
|
def with_error_handling(ctx)
|
45
107
|
yield
|
46
108
|
rescue StandardError => err
|
47
|
-
|
48
|
-
_err_class, handler = rescues.find { |err_class, handler| err.is_a?(err_class) }
|
109
|
+
handler = find_handler_for(err.class)
|
49
110
|
if handler
|
50
111
|
runtime_info = ctx.namespace(:interpreter) || {}
|
51
112
|
obj = runtime_info[:current_object]
|
@@ -54,11 +115,48 @@ module GraphQL
|
|
54
115
|
if obj.is_a?(GraphQL::Schema::Object)
|
55
116
|
obj = obj.object
|
56
117
|
end
|
57
|
-
handler.call(err, obj, args, ctx, field)
|
118
|
+
handler[:handler].call(err, obj, args, ctx, field)
|
58
119
|
else
|
59
120
|
raise err
|
60
121
|
end
|
61
122
|
end
|
123
|
+
|
124
|
+
# @return [Proc, nil] The handler for `error_class`, if one was registered on this schema or inherited
|
125
|
+
def find_handler_for(error_class)
|
126
|
+
handlers = @handlers[:subclass_handlers]
|
127
|
+
handler = nil
|
128
|
+
while (handlers) do
|
129
|
+
_err_class, next_handler = handlers.find { |err_class, handler| error_class <= err_class }
|
130
|
+
if next_handler
|
131
|
+
handlers = next_handler[:subclass_handlers]
|
132
|
+
handler = next_handler
|
133
|
+
else
|
134
|
+
# Don't reassign `handler` --
|
135
|
+
# let the previous assignment carry over outside this block.
|
136
|
+
break
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# check for a handler from a parent class:
|
141
|
+
if @schema.superclass.respond_to?(:error_handler) && (parent_errors = @schema.superclass.error_handler)
|
142
|
+
parent_handler = parent_errors.find_handler_for(error_class)
|
143
|
+
end
|
144
|
+
|
145
|
+
# If the inherited handler is more specific than the one defined here,
|
146
|
+
# use it.
|
147
|
+
# If it's a tie (or there is no parent handler), use the one defined here.
|
148
|
+
# If there's an inherited one, but not one defined here, use the inherited one.
|
149
|
+
# Otherwise, there's no handler for this error, return `nil`.
|
150
|
+
if parent_handler && handler && parent_handler[:class] < handler[:class]
|
151
|
+
parent_handler
|
152
|
+
elsif handler
|
153
|
+
handler
|
154
|
+
elsif parent_handler
|
155
|
+
parent_handler
|
156
|
+
else
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
end
|
62
160
|
end
|
63
161
|
end
|
64
162
|
end
|
@@ -95,7 +95,7 @@ module GraphQL
|
|
95
95
|
end
|
96
96
|
final_values.compact!
|
97
97
|
tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
|
98
|
-
Interpreter::Resolve.resolve_all(final_values)
|
98
|
+
Interpreter::Resolve.resolve_all(final_values, multiplex.dataloader)
|
99
99
|
end
|
100
100
|
queries.each do |query|
|
101
101
|
runtime = query.context.namespace(:interpreter)[:runtime]
|
@@ -113,7 +113,7 @@ module GraphQL
|
|
113
113
|
def initialize(value:, path:, field:)
|
114
114
|
message = "Failed to build a GraphQL list result for field `#{field.path}` at path `#{path.join(".")}`.\n".dup
|
115
115
|
|
116
|
-
message << "Expected `#{value.inspect}` to implement `.each` to satisfy the GraphQL return type `#{field.type.to_type_signature}`.\n"
|
116
|
+
message << "Expected `#{value.inspect}` (#{value.class}) to implement `.each` to satisfy the GraphQL return type `#{field.type.to_type_signature}`.\n"
|
117
117
|
|
118
118
|
if field.connection?
|
119
119
|
message << "\nThis field was treated as a Relay-style connection; add `connection: false` to the `field(...)` to disable this behavior."
|
@@ -6,17 +6,21 @@ module GraphQL
|
|
6
6
|
class ArgumentsCache
|
7
7
|
def initialize(query)
|
8
8
|
@query = query
|
9
|
+
@dataloader = query.context.dataloader
|
9
10
|
@storage = Hash.new do |h, ast_node|
|
10
11
|
h[ast_node] = Hash.new do |h2, arg_owner|
|
11
12
|
h2[arg_owner] = Hash.new do |h3, parent_object|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
dataload_for(ast_node, arg_owner, parent_object) do |kwarg_arguments|
|
14
|
+
h3[parent_object] = @query.schema.after_lazy(kwarg_arguments) do |resolved_args|
|
15
|
+
h3[parent_object] = resolved_args
|
16
|
+
end
|
17
|
+
end
|
16
18
|
|
17
|
-
h3
|
18
|
-
#
|
19
|
-
h3[parent_object] =
|
19
|
+
if !h3.key?(parent_object)
|
20
|
+
# TODO should i bother putting anything here?
|
21
|
+
h3[parent_object] = NO_ARGUMENTS
|
22
|
+
else
|
23
|
+
h3[parent_object]
|
20
24
|
end
|
21
25
|
end
|
22
26
|
end
|
@@ -25,6 +29,25 @@ module GraphQL
|
|
25
29
|
|
26
30
|
def fetch(ast_node, argument_owner, parent_object)
|
27
31
|
@storage[ast_node][argument_owner][parent_object]
|
32
|
+
# If any jobs were enqueued, run them now,
|
33
|
+
# since this might have been called outside of execution.
|
34
|
+
# (The jobs are responsible for updating `result` in-place.)
|
35
|
+
@dataloader.run
|
36
|
+
# Ack, the _hash_ is updated, but the key is eventually
|
37
|
+
# overridden with an immutable arguments instance.
|
38
|
+
# The first call queues up the job,
|
39
|
+
# then this call fetches the result.
|
40
|
+
# TODO this should be better, find a solution
|
41
|
+
# that works with merging the runtime.rb code
|
42
|
+
@storage[ast_node][argument_owner][parent_object]
|
43
|
+
end
|
44
|
+
|
45
|
+
# @yield [Interpreter::Arguments, Lazy<Interpreter::Arguments>] The finally-loaded arguments
|
46
|
+
def dataload_for(ast_node, argument_owner, parent_object, &block)
|
47
|
+
# First, normalize all AST or Ruby values to a plain Ruby hash
|
48
|
+
args_hash = self.class.prepare_args_hash(@query, ast_node)
|
49
|
+
argument_owner.coerce_arguments(parent_object, args_hash, @query.context, &block)
|
50
|
+
nil
|
28
51
|
end
|
29
52
|
|
30
53
|
private
|
@@ -33,7 +56,7 @@ module GraphQL
|
|
33
56
|
|
34
57
|
NO_VALUE_GIVEN = Object.new
|
35
58
|
|
36
|
-
def prepare_args_hash(ast_arg_or_hash_or_value)
|
59
|
+
def self.prepare_args_hash(query, ast_arg_or_hash_or_value)
|
37
60
|
case ast_arg_or_hash_or_value
|
38
61
|
when Hash
|
39
62
|
if ast_arg_or_hash_or_value.empty?
|
@@ -41,27 +64,27 @@ module GraphQL
|
|
41
64
|
end
|
42
65
|
args_hash = {}
|
43
66
|
ast_arg_or_hash_or_value.each do |k, v|
|
44
|
-
args_hash[k] = prepare_args_hash(v)
|
67
|
+
args_hash[k] = prepare_args_hash(query, v)
|
45
68
|
end
|
46
69
|
args_hash
|
47
70
|
when Array
|
48
|
-
ast_arg_or_hash_or_value.map { |v| prepare_args_hash(v) }
|
71
|
+
ast_arg_or_hash_or_value.map { |v| prepare_args_hash(query, v) }
|
49
72
|
when GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::InputObject, GraphQL::Language::Nodes::Directive
|
50
73
|
if ast_arg_or_hash_or_value.arguments.empty?
|
51
74
|
return NO_ARGUMENTS
|
52
75
|
end
|
53
76
|
args_hash = {}
|
54
77
|
ast_arg_or_hash_or_value.arguments.each do |arg|
|
55
|
-
v = prepare_args_hash(arg.value)
|
78
|
+
v = prepare_args_hash(query, arg.value)
|
56
79
|
if v != NO_VALUE_GIVEN
|
57
80
|
args_hash[arg.name] = v
|
58
81
|
end
|
59
82
|
end
|
60
83
|
args_hash
|
61
84
|
when GraphQL::Language::Nodes::VariableIdentifier
|
62
|
-
if
|
63
|
-
variable_value =
|
64
|
-
prepare_args_hash(variable_value)
|
85
|
+
if query.variables.key?(ast_arg_or_hash_or_value.name)
|
86
|
+
variable_value = query.variables[ast_arg_or_hash_or_value.name]
|
87
|
+
prepare_args_hash(query, variable_value)
|
65
88
|
else
|
66
89
|
NO_VALUE_GIVEN
|
67
90
|
end
|
@@ -6,10 +6,9 @@ module GraphQL
|
|
6
6
|
module Resolve
|
7
7
|
# Continue field results in `results` until there's nothing else to continue.
|
8
8
|
# @return [void]
|
9
|
-
def self.resolve_all(results)
|
10
|
-
|
11
|
-
|
12
|
-
end
|
9
|
+
def self.resolve_all(results, dataloader)
|
10
|
+
dataloader.append_job { resolve(results, dataloader) }
|
11
|
+
nil
|
13
12
|
end
|
14
13
|
|
15
14
|
# After getting `results` back from an interpreter evaluation,
|
@@ -24,33 +23,42 @@ module GraphQL
|
|
24
23
|
# return {Lazy} instances if there's more work to be done,
|
25
24
|
# or return {Hash}/{Array} if the query should be continued.
|
26
25
|
#
|
27
|
-
# @
|
28
|
-
|
29
|
-
|
26
|
+
# @return [void]
|
27
|
+
def self.resolve(results, dataloader)
|
28
|
+
# There might be pending jobs here that _will_ write lazies
|
29
|
+
# into the result hash. We should run them out, so we
|
30
|
+
# can be sure that all lazies will be present in the result hashes.
|
31
|
+
# A better implementation would somehow interleave (or unify)
|
32
|
+
# these approaches.
|
33
|
+
dataloader.run
|
30
34
|
next_results = []
|
31
|
-
|
32
|
-
# Work through the queue until it's empty
|
33
|
-
while results.size > 0
|
35
|
+
while results.any?
|
34
36
|
result_value = results.shift
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
if result_value.is_a?(Lazy)
|
41
|
-
# Since this field returned another lazy,
|
42
|
-
# add it to the same queue
|
43
|
-
results << result_value
|
44
|
-
elsif result_value.is_a?(Hash)
|
45
|
-
# This is part of the next level, add it
|
46
|
-
next_results.concat(result_value.values)
|
37
|
+
if result_value.is_a?(Hash)
|
38
|
+
results.concat(result_value.values)
|
39
|
+
next
|
47
40
|
elsif result_value.is_a?(Array)
|
48
|
-
|
49
|
-
|
41
|
+
results.concat(result_value)
|
42
|
+
next
|
43
|
+
elsif result_value.is_a?(Lazy)
|
44
|
+
loaded_value = result_value.value
|
45
|
+
if loaded_value.is_a?(Lazy)
|
46
|
+
# Since this field returned another lazy,
|
47
|
+
# add it to the same queue
|
48
|
+
results << loaded_value
|
49
|
+
elsif loaded_value.is_a?(Hash) || loaded_value.is_a?(Array)
|
50
|
+
# Add these values in wholesale --
|
51
|
+
# they might be modified by later work in the dataloader.
|
52
|
+
next_results << loaded_value
|
53
|
+
end
|
50
54
|
end
|
51
55
|
end
|
52
56
|
|
53
|
-
next_results
|
57
|
+
if next_results.any?
|
58
|
+
dataloader.append_job { resolve(next_results, dataloader) }
|
59
|
+
end
|
60
|
+
|
61
|
+
nil
|
54
62
|
end
|
55
63
|
end
|
56
64
|
end
|