graphql 2.0.13 → 2.0.14
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.
Potentially problematic release.
This version of graphql might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/generators/graphql/templates/schema.erb +3 -0
- data/lib/graphql/dataloader/source.rb +9 -0
- data/lib/graphql/execution/interpreter.rb +185 -59
- data/lib/graphql/execution/lookahead.rb +26 -26
- data/lib/graphql/execution/multiplex.rb +1 -116
- data/lib/graphql/execution.rb +0 -1
- data/lib/graphql/introspection/type_type.rb +7 -0
- data/lib/graphql/introspection.rb +2 -1
- data/lib/graphql/query.rb +1 -1
- data/lib/graphql/schema/build_from_definition.rb +1 -2
- data/lib/graphql/schema/directive/one_of.rb +12 -0
- data/lib/graphql/schema/input_object.rb +35 -0
- data/lib/graphql/schema/late_bound_type.rb +4 -0
- data/lib/graphql/schema.rb +3 -1
- data/lib/graphql/static_validation/all_rules.rb +1 -0
- data/lib/graphql/static_validation/literal_validator.rb +4 -0
- data/lib/graphql/static_validation/rules/one_of_input_objects_are_valid.rb +66 -0
- data/lib/graphql/static_validation/rules/one_of_input_objects_are_valid_error.rb +29 -0
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +1 -1
- data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
- data/lib/graphql/version.rb +1 -1
- metadata +6 -4
- data/lib/graphql/execution/instrumentation.rb +0 -92
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a3bc6610f88b6689ee6991a6ad3543580f9bdf1b3cb9f2f7e3f78862d76072f7
|
4
|
+
data.tar.gz: ce52505d6c43e330aa8e5def38719f62bdfe067274a86530c889350c64994bd0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f3aac93aa1013c257c84521b54285719d6edb8c212a83140123902a06f3cffabc1005d2a176b723cff5677614eab7ae1617b71ce7eb403e721fcbafbe07c1e1
|
7
|
+
data.tar.gz: a84bf37728b5c0ff8795070fd682ac7af83e8888d47cadff5103a76ee333788c30bf78f505528fee4f868288b99dfc11d82f7c604710e13b4a7d9690eb2efe36
|
@@ -23,5 +23,8 @@ class <%= schema_name %> < GraphQL::Schema
|
|
23
23
|
# to return the correct GraphQL object type for `obj`
|
24
24
|
raise(GraphQL::RequiredImplementationMissingError)
|
25
25
|
end
|
26
|
+
|
27
|
+
# Stop validating when it encounters this many errors:
|
28
|
+
validate_max_errors(100)
|
26
29
|
end
|
27
30
|
<% end -%>
|
@@ -86,6 +86,15 @@ module GraphQL
|
|
86
86
|
!@pending_keys.empty?
|
87
87
|
end
|
88
88
|
|
89
|
+
# Add these key-value pairs to this source's cache
|
90
|
+
# (future loads will use these merged values).
|
91
|
+
# @param results [Hash<Object => Object>] key-value pairs to cache in this source
|
92
|
+
# @return [void]
|
93
|
+
def merge(results)
|
94
|
+
@results.merge!(results)
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
89
98
|
# Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
|
90
99
|
# @api private
|
91
100
|
# @return [void]
|
@@ -11,76 +11,202 @@ require "graphql/execution/interpreter/handles_raw_value"
|
|
11
11
|
module GraphQL
|
12
12
|
module Execution
|
13
13
|
class Interpreter
|
14
|
-
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
end
|
14
|
+
class << self
|
15
|
+
# Used internally to signal that the query shouldn't be executed
|
16
|
+
# @api private
|
17
|
+
NO_OPERATION = {}.freeze
|
19
18
|
|
20
|
-
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
19
|
+
# @param schema [GraphQL::Schema]
|
20
|
+
# @param queries [Array<GraphQL::Query, Hash>]
|
21
|
+
# @param context [Hash]
|
22
|
+
# @param max_complexity [Integer, nil]
|
23
|
+
# @return [Array<Hash>] One result per query
|
24
|
+
def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
|
25
|
+
queries = query_options.map do |opts|
|
26
|
+
case opts
|
27
|
+
when Hash
|
28
|
+
GraphQL::Query.new(schema, nil, **opts)
|
29
|
+
when GraphQL::Query
|
30
|
+
opts
|
31
|
+
else
|
32
|
+
raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
|
33
|
+
end
|
34
|
+
end
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
36
|
+
multiplex = Execution::Multiplex.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
|
37
|
+
multiplex.trace("execute_multiplex", { multiplex: multiplex }) do
|
38
|
+
schema = multiplex.schema
|
39
|
+
queries = multiplex.queries
|
40
|
+
query_instrumenters = schema.instrumenters[:query]
|
41
|
+
multiplex_instrumenters = schema.instrumenters[:multiplex]
|
34
42
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
43
|
+
# First, run multiplex instrumentation, then query instrumentation for each query
|
44
|
+
call_hooks(multiplex_instrumenters, multiplex, :before_multiplex, :after_multiplex) do
|
45
|
+
each_query_call_hooks(query_instrumenters, queries) do
|
46
|
+
schema = multiplex.schema
|
47
|
+
multiplex_analyzers = schema.multiplex_analyzers
|
48
|
+
queries = multiplex.queries
|
49
|
+
if multiplex.max_complexity
|
50
|
+
multiplex_analyzers += [GraphQL::Analysis::AST::MaxQueryComplexity]
|
51
|
+
end
|
40
52
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
+
schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers)
|
54
|
+
begin
|
55
|
+
# Since this is basically the batching context,
|
56
|
+
# share it for a whole multiplex
|
57
|
+
multiplex.context[:interpreter_instance] ||= multiplex.schema.query_execution_strategy.new
|
58
|
+
# Do as much eager evaluation of the query as possible
|
59
|
+
results = []
|
60
|
+
queries.each_with_index do |query, idx|
|
61
|
+
multiplex.dataloader.append_job {
|
62
|
+
operation = query.selected_operation
|
63
|
+
result = if operation.nil? || !query.valid? || query.context.errors.any?
|
64
|
+
NO_OPERATION
|
65
|
+
else
|
66
|
+
begin
|
67
|
+
# Although queries in a multiplex _share_ an Interpreter instance,
|
68
|
+
# they also have another item of state, which is private to that query
|
69
|
+
# in particular, assign it here:
|
70
|
+
runtime = Runtime.new(query: query)
|
71
|
+
query.context.namespace(:interpreter)[:runtime] = runtime
|
53
72
|
|
54
|
-
|
55
|
-
|
73
|
+
query.trace("execute_query", {query: query}) do
|
74
|
+
runtime.run_eager
|
75
|
+
end
|
76
|
+
rescue GraphQL::ExecutionError => err
|
77
|
+
query.context.errors << err
|
78
|
+
NO_OPERATION
|
79
|
+
end
|
80
|
+
end
|
81
|
+
results[idx] = result
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
multiplex.dataloader.run
|
86
|
+
|
87
|
+
# Then, work through lazy results in a breadth-first way
|
88
|
+
multiplex.dataloader.append_job {
|
89
|
+
tracer = multiplex
|
90
|
+
query = multiplex.queries.length == 1 ? multiplex.queries[0] : nil
|
91
|
+
queries = multiplex ? multiplex.queries : [query]
|
92
|
+
final_values = queries.map do |query|
|
93
|
+
runtime = query.context.namespace(:interpreter)[:runtime]
|
94
|
+
# it might not be present if the query has an error
|
95
|
+
runtime ? runtime.final_result : nil
|
96
|
+
end
|
97
|
+
final_values.compact!
|
98
|
+
tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
|
99
|
+
Interpreter::Resolve.resolve_all(final_values, multiplex.dataloader)
|
100
|
+
end
|
101
|
+
queries.each do |query|
|
102
|
+
runtime = query.context.namespace(:interpreter)[:runtime]
|
103
|
+
if runtime
|
104
|
+
runtime.delete_interpreter_context(:current_path)
|
105
|
+
runtime.delete_interpreter_context(:current_field)
|
106
|
+
runtime.delete_interpreter_context(:current_object)
|
107
|
+
runtime.delete_interpreter_context(:current_arguments)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
}
|
111
|
+
multiplex.dataloader.run
|
112
|
+
|
113
|
+
# Then, find all errors and assign the result to the query object
|
114
|
+
results.each_with_index do |data_result, idx|
|
115
|
+
query = queries[idx]
|
116
|
+
# Assign the result so that it can be accessed in instrumentation
|
117
|
+
query.result_values = if data_result.equal?(NO_OPERATION)
|
118
|
+
if !query.valid? || query.context.errors.any?
|
119
|
+
# A bit weird, but `Query#static_errors` _includes_ `query.context.errors`
|
120
|
+
{ "errors" => query.static_errors.map(&:to_h) }
|
121
|
+
else
|
122
|
+
data_result
|
123
|
+
end
|
124
|
+
else
|
125
|
+
result = {
|
126
|
+
"data" => query.context.namespace(:interpreter)[:runtime].final_result
|
127
|
+
}
|
128
|
+
|
129
|
+
if query.context.errors.any?
|
130
|
+
error_result = query.context.errors.map(&:to_h)
|
131
|
+
result["errors"] = error_result
|
132
|
+
end
|
56
133
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
134
|
+
result
|
135
|
+
end
|
136
|
+
if query.context.namespace?(:__query_result_extensions__)
|
137
|
+
query.result_values["extensions"] = query.context.namespace(:__query_result_extensions__)
|
138
|
+
end
|
139
|
+
# Get the Query::Result, not the Hash
|
140
|
+
results[idx] = query.result
|
141
|
+
end
|
142
|
+
|
143
|
+
results
|
144
|
+
rescue Exception
|
145
|
+
# TODO rescue at a higher level so it will catch errors in analysis, too
|
146
|
+
# Assign values here so that the query's `@executed` becomes true
|
147
|
+
queries.map { |q| q.result_values ||= {} }
|
148
|
+
raise
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
63
153
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
# Call the before_ hooks of each query,
|
158
|
+
# Then yield if no errors.
|
159
|
+
# `call_hooks` takes care of appropriate cleanup.
|
160
|
+
def each_query_call_hooks(instrumenters, queries, i = 0)
|
161
|
+
if i >= queries.length
|
162
|
+
yield
|
163
|
+
else
|
164
|
+
query = queries[i]
|
165
|
+
call_hooks(instrumenters, query, :before_query, :after_query) {
|
166
|
+
each_query_call_hooks(instrumenters, queries, i + 1) {
|
167
|
+
yield
|
168
|
+
}
|
169
|
+
}
|
170
|
+
end
|
69
171
|
end
|
70
|
-
|
71
|
-
|
72
|
-
|
172
|
+
|
173
|
+
# Call each before hook, and if they all succeed, yield.
|
174
|
+
# If they don't all succeed, call after_ for each one that succeeded.
|
175
|
+
def call_hooks(instrumenters, object, before_hook_name, after_hook_name)
|
176
|
+
begin
|
177
|
+
successful = []
|
178
|
+
instrumenters.each do |instrumenter|
|
179
|
+
instrumenter.public_send(before_hook_name, object)
|
180
|
+
successful << instrumenter
|
181
|
+
end
|
182
|
+
|
183
|
+
# if any before hooks raise an exception, quit calling before hooks,
|
184
|
+
# but call the after hooks on anything that succeeded but also
|
185
|
+
# raise the exception that came from the before hook.
|
186
|
+
rescue GraphQL::ExecutionError => err
|
187
|
+
object.context.errors << err
|
188
|
+
rescue => e
|
189
|
+
raise call_after_hooks(successful, object, after_hook_name, e)
|
190
|
+
end
|
191
|
+
|
192
|
+
begin
|
193
|
+
yield # Call the user code
|
194
|
+
ensure
|
195
|
+
ex = call_after_hooks(successful, object, after_hook_name, nil)
|
196
|
+
raise ex if ex
|
197
|
+
end
|
73
198
|
end
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
199
|
+
|
200
|
+
def call_after_hooks(instrumenters, object, after_hook_name, ex)
|
201
|
+
instrumenters.reverse_each do |instrumenter|
|
202
|
+
begin
|
203
|
+
instrumenter.public_send(after_hook_name, object)
|
204
|
+
rescue => e
|
205
|
+
ex = e
|
206
|
+
end
|
81
207
|
end
|
208
|
+
ex
|
82
209
|
end
|
83
|
-
nil
|
84
210
|
end
|
85
211
|
|
86
212
|
class ListResultFailedError < GraphQL::Error
|
@@ -87,16 +87,28 @@ module GraphQL
|
|
87
87
|
|
88
88
|
# Like {#selects?}, but can be used for chaining.
|
89
89
|
# It returns a null object (check with {#selected?})
|
90
|
+
# @param field_name [String, Symbol]
|
90
91
|
# @return [GraphQL::Execution::Lookahead]
|
91
92
|
def selection(field_name, selected_type: @selected_type, arguments: nil)
|
92
|
-
|
93
|
+
next_field_defn = case field_name
|
94
|
+
when String
|
95
|
+
@query.get_field(selected_type, field_name)
|
96
|
+
when Symbol
|
97
|
+
# Try to avoid the `.to_s` below, if possible
|
98
|
+
all_fields = @query.warden.fields(selected_type)
|
99
|
+
if (match_by_orig_name = all_fields.find { |f| f.original_name == field_name })
|
100
|
+
match_by_orig_name
|
101
|
+
else
|
102
|
+
guessed_name = Schema::Member::BuildType.camelize(field_name.to_s)
|
103
|
+
@query.get_field(selected_type, guessed_name)
|
104
|
+
end
|
105
|
+
end
|
93
106
|
|
94
|
-
next_field_defn = @query.get_field(selected_type, next_field_name)
|
95
107
|
if next_field_defn
|
96
108
|
next_nodes = []
|
97
109
|
@ast_nodes.each do |ast_node|
|
98
110
|
ast_node.selections.each do |selection|
|
99
|
-
find_selected_nodes(selection,
|
111
|
+
find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes)
|
100
112
|
end
|
101
113
|
end
|
102
114
|
|
@@ -196,23 +208,6 @@ module GraphQL
|
|
196
208
|
|
197
209
|
private
|
198
210
|
|
199
|
-
# If it's a symbol, stringify and camelize it
|
200
|
-
def normalize_name(name)
|
201
|
-
if name.is_a?(Symbol)
|
202
|
-
Schema::Member::BuildType.camelize(name.to_s)
|
203
|
-
else
|
204
|
-
name
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
def normalize_keyword(keyword)
|
209
|
-
if keyword.is_a?(String)
|
210
|
-
Schema::Member::BuildType.underscore(keyword).to_sym
|
211
|
-
else
|
212
|
-
keyword
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
211
|
def skipped_by_directive?(ast_selection)
|
217
212
|
ast_selection.directives.each do |directive|
|
218
213
|
dir_defn = @query.schema.directives.fetch(directive.name)
|
@@ -265,11 +260,11 @@ module GraphQL
|
|
265
260
|
|
266
261
|
# If a selection on `node` matches `field_name` (which is backed by `field_defn`)
|
267
262
|
# and matches the `arguments:` constraints, then add that node to `matches`
|
268
|
-
def find_selected_nodes(node,
|
263
|
+
def find_selected_nodes(node, field_defn, arguments:, matches:)
|
269
264
|
return if skipped_by_directive?(node)
|
270
265
|
case node
|
271
266
|
when GraphQL::Language::Nodes::Field
|
272
|
-
if node.name ==
|
267
|
+
if node.name == field_defn.graphql_name
|
273
268
|
if arguments.nil? || arguments.empty?
|
274
269
|
# No constraint applied
|
275
270
|
matches << node
|
@@ -278,10 +273,10 @@ module GraphQL
|
|
278
273
|
end
|
279
274
|
end
|
280
275
|
when GraphQL::Language::Nodes::InlineFragment
|
281
|
-
node.selections.each { |s| find_selected_nodes(s,
|
276
|
+
node.selections.each { |s| find_selected_nodes(s, field_defn, arguments: arguments, matches: matches) }
|
282
277
|
when GraphQL::Language::Nodes::FragmentSpread
|
283
278
|
frag_defn = @query.fragments[node.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})")
|
284
|
-
frag_defn.selections.each { |s| find_selected_nodes(s,
|
279
|
+
frag_defn.selections.each { |s| find_selected_nodes(s, field_defn, arguments: arguments, matches: matches) }
|
285
280
|
else
|
286
281
|
raise "Unexpected selection comparison on #{node.class.name} (#{node})"
|
287
282
|
end
|
@@ -290,9 +285,14 @@ module GraphQL
|
|
290
285
|
def arguments_match?(arguments, field_defn, field_node)
|
291
286
|
query_kwargs = @query.arguments_for(field_node, field_defn)
|
292
287
|
arguments.all? do |arg_name, arg_value|
|
293
|
-
|
288
|
+
arg_name_sym = if arg_name.is_a?(String)
|
289
|
+
Schema::Member::BuildType.underscore(arg_name).to_sym
|
290
|
+
else
|
291
|
+
arg_name
|
292
|
+
end
|
293
|
+
|
294
294
|
# Make sure the constraint is present with a matching value
|
295
|
-
query_kwargs.key?(
|
295
|
+
query_kwargs.key?(arg_name_sym) && query_kwargs[arg_name_sym] == arg_value
|
296
296
|
end
|
297
297
|
end
|
298
298
|
end
|
@@ -23,13 +23,10 @@ module GraphQL
|
|
23
23
|
# @see {Schema#multiplex} for public API
|
24
24
|
# @api private
|
25
25
|
class Multiplex
|
26
|
-
# Used internally to signal that the query shouldn't be executed
|
27
|
-
# @api private
|
28
|
-
NO_OPERATION = {}.freeze
|
29
|
-
|
30
26
|
include Tracing::Traceable
|
31
27
|
|
32
28
|
attr_reader :context, :queries, :schema, :max_complexity, :dataloader
|
29
|
+
|
33
30
|
def initialize(schema:, queries:, context:, max_complexity:)
|
34
31
|
@schema = schema
|
35
32
|
@queries = queries
|
@@ -43,118 +40,6 @@ module GraphQL
|
|
43
40
|
end
|
44
41
|
@max_complexity = max_complexity
|
45
42
|
end
|
46
|
-
|
47
|
-
class << self
|
48
|
-
# @param schema [GraphQL::Schema]
|
49
|
-
# @param queries [Array<GraphQL::Query, Hash>]
|
50
|
-
# @param context [Hash]
|
51
|
-
# @param max_complexity [Integer, nil]
|
52
|
-
# @return [Array<Hash>] One result per query
|
53
|
-
def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
|
54
|
-
queries = query_options.map do |opts|
|
55
|
-
case opts
|
56
|
-
when Hash
|
57
|
-
GraphQL::Query.new(schema, nil, **opts)
|
58
|
-
when GraphQL::Query
|
59
|
-
opts
|
60
|
-
else
|
61
|
-
raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
multiplex = self.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
|
66
|
-
multiplex.trace("execute_multiplex", { multiplex: multiplex }) do
|
67
|
-
GraphQL::Execution::Instrumentation.apply_instrumenters(multiplex) do
|
68
|
-
schema = multiplex.schema
|
69
|
-
multiplex_analyzers = schema.multiplex_analyzers
|
70
|
-
if multiplex.max_complexity
|
71
|
-
multiplex_analyzers += [GraphQL::Analysis::AST::MaxQueryComplexity]
|
72
|
-
end
|
73
|
-
|
74
|
-
schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers)
|
75
|
-
|
76
|
-
begin
|
77
|
-
multiplex.schema.query_execution_strategy.begin_multiplex(multiplex)
|
78
|
-
# Do as much eager evaluation of the query as possible
|
79
|
-
results = []
|
80
|
-
queries.each_with_index do |query, idx|
|
81
|
-
multiplex.dataloader.append_job { begin_query(results, idx, query, multiplex) }
|
82
|
-
end
|
83
|
-
|
84
|
-
multiplex.dataloader.run
|
85
|
-
|
86
|
-
# Then, work through lazy results in a breadth-first way
|
87
|
-
multiplex.dataloader.append_job {
|
88
|
-
multiplex.schema.query_execution_strategy.finish_multiplex(results, multiplex)
|
89
|
-
}
|
90
|
-
multiplex.dataloader.run
|
91
|
-
|
92
|
-
# Then, find all errors and assign the result to the query object
|
93
|
-
results.each_with_index do |data_result, idx|
|
94
|
-
query = queries[idx]
|
95
|
-
finish_query(data_result, query, multiplex)
|
96
|
-
# Get the Query::Result, not the Hash
|
97
|
-
results[idx] = query.result
|
98
|
-
end
|
99
|
-
|
100
|
-
results
|
101
|
-
rescue Exception
|
102
|
-
# TODO rescue at a higher level so it will catch errors in analysis, too
|
103
|
-
# Assign values here so that the query's `@executed` becomes true
|
104
|
-
queries.map { |q| q.result_values ||= {} }
|
105
|
-
raise
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
# @param query [GraphQL::Query]
|
112
|
-
def begin_query(results, idx, query, multiplex)
|
113
|
-
operation = query.selected_operation
|
114
|
-
result = if operation.nil? || !query.valid? || query.context.errors.any?
|
115
|
-
NO_OPERATION
|
116
|
-
else
|
117
|
-
begin
|
118
|
-
query.schema.query_execution_strategy.begin_query(query, multiplex)
|
119
|
-
rescue GraphQL::ExecutionError => err
|
120
|
-
query.context.errors << err
|
121
|
-
NO_OPERATION
|
122
|
-
end
|
123
|
-
end
|
124
|
-
results[idx] = result
|
125
|
-
nil
|
126
|
-
end
|
127
|
-
|
128
|
-
private
|
129
|
-
|
130
|
-
# @param data_result [Hash] The result for the "data" key, if any
|
131
|
-
# @param query [GraphQL::Query] The query which was run
|
132
|
-
# @return [Hash] final result of this query, including all values and errors
|
133
|
-
def finish_query(data_result, query, multiplex)
|
134
|
-
# Assign the result so that it can be accessed in instrumentation
|
135
|
-
query.result_values = if data_result.equal?(NO_OPERATION)
|
136
|
-
if !query.valid? || query.context.errors.any?
|
137
|
-
# A bit weird, but `Query#static_errors` _includes_ `query.context.errors`
|
138
|
-
{ "errors" => query.static_errors.map(&:to_h) }
|
139
|
-
else
|
140
|
-
data_result
|
141
|
-
end
|
142
|
-
else
|
143
|
-
# Use `context.value` which was assigned during execution
|
144
|
-
result = query.schema.query_execution_strategy.finish_query(query, multiplex)
|
145
|
-
|
146
|
-
if query.context.errors.any?
|
147
|
-
error_result = query.context.errors.map(&:to_h)
|
148
|
-
result["errors"] = error_result
|
149
|
-
end
|
150
|
-
|
151
|
-
result
|
152
|
-
end
|
153
|
-
if query.context.namespace?(:__query_result_extensions__)
|
154
|
-
query.result_values["extensions"] = query.context.namespace(:__query_result_extensions__)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
43
|
end
|
159
44
|
end
|
160
45
|
end
|
data/lib/graphql/execution.rb
CHANGED
@@ -29,6 +29,13 @@ module GraphQL
|
|
29
29
|
|
30
30
|
field :specifiedByURL, String, resolver_method: :specified_by_url
|
31
31
|
|
32
|
+
field :is_one_of, Boolean, null: false
|
33
|
+
|
34
|
+
def is_one_of
|
35
|
+
object.kind.input_object? &&
|
36
|
+
object.directives.any? { |d| d.graphql_name == "oneOf" }
|
37
|
+
end
|
38
|
+
|
32
39
|
def specified_by_url
|
33
40
|
if object.kind.scalar?
|
34
41
|
object.specified_by_url
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GraphQL
|
3
3
|
module Introspection
|
4
|
-
def self.query(include_deprecated_args: false, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false)
|
4
|
+
def self.query(include_deprecated_args: false, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false, include_is_one_of: false)
|
5
5
|
# The introspection query to end all introspection queries, copied from
|
6
6
|
# https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js
|
7
7
|
<<-QUERY
|
@@ -30,6 +30,7 @@ fragment FullType on __Type {
|
|
30
30
|
name
|
31
31
|
description
|
32
32
|
#{include_specified_by_url ? "specifiedByURL" : ""}
|
33
|
+
#{include_is_one_of ? "isOneOf" : ""}
|
33
34
|
fields(includeDeprecated: true) {
|
34
35
|
name
|
35
36
|
description
|
data/lib/graphql/query.rb
CHANGED
@@ -196,7 +196,7 @@ module GraphQL
|
|
196
196
|
# @return [Hash] A GraphQL response, with `"data"` and/or `"errors"` keys
|
197
197
|
def result
|
198
198
|
if !@executed
|
199
|
-
Execution::
|
199
|
+
Execution::Interpreter.run_all(@schema, [self], context: @context)
|
200
200
|
end
|
201
201
|
@result ||= Query::Result.new(query: self, values: @result_values)
|
202
202
|
end
|
@@ -55,14 +55,13 @@ module GraphQL
|
|
55
55
|
end
|
56
56
|
})
|
57
57
|
|
58
|
+
directives.merge!(GraphQL::Schema.default_directives)
|
58
59
|
document.definitions.each do |definition|
|
59
60
|
if definition.is_a?(GraphQL::Language::Nodes::DirectiveDefinition)
|
60
61
|
directives[definition.name] = build_directive(definition, directive_type_resolver)
|
61
62
|
end
|
62
63
|
end
|
63
64
|
|
64
|
-
directives = GraphQL::Schema.default_directives.merge(directives)
|
65
|
-
|
66
65
|
# In case any directives referenced built-in types for their arguments:
|
67
66
|
replace_late_bound_types_with_built_in(types)
|
68
67
|
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
class Schema
|
4
|
+
class Directive < GraphQL::Schema::Member
|
5
|
+
class OneOf < GraphQL::Schema::Directive
|
6
|
+
description "Requires that exactly one field must be supplied and that field must not be `null`."
|
7
|
+
locations(GraphQL::Schema::Directive::INPUT_OBJECT)
|
8
|
+
default_directive true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -69,6 +69,19 @@ module GraphQL
|
|
69
69
|
true
|
70
70
|
end
|
71
71
|
|
72
|
+
def self.one_of
|
73
|
+
if !one_of?
|
74
|
+
if all_argument_definitions.any? { |arg| arg.type.non_null? }
|
75
|
+
raise ArgumentError, "`one_of` may not be used with required arguments -- add `required: false` to argument definitions to use `one_of`"
|
76
|
+
end
|
77
|
+
directive(GraphQL::Schema::Directive::OneOf)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.one_of?
|
82
|
+
directives.any? { |d| d.is_a?(GraphQL::Schema::Directive::OneOf) }
|
83
|
+
end
|
84
|
+
|
72
85
|
def unwrap_value(value)
|
73
86
|
case value
|
74
87
|
when Array
|
@@ -109,6 +122,14 @@ module GraphQL
|
|
109
122
|
class << self
|
110
123
|
def argument(*args, **kwargs, &block)
|
111
124
|
argument_defn = super(*args, **kwargs, &block)
|
125
|
+
if one_of?
|
126
|
+
if argument_defn.type.non_null?
|
127
|
+
raise ArgumentError, "Argument '#{argument_defn.path}' must be nullable because it is part of a OneOf type, add `required: false`."
|
128
|
+
end
|
129
|
+
if argument_defn.default_value?
|
130
|
+
raise ArgumentError, "Argument '#{argument_defn.path}' cannot have a default value because it is part of a OneOf type, remove `default_value: ...`."
|
131
|
+
end
|
132
|
+
end
|
112
133
|
# Add a method access
|
113
134
|
method_name = argument_defn.keyword
|
114
135
|
class_eval <<-RUBY, __FILE__, __LINE__
|
@@ -166,6 +187,20 @@ module GraphQL
|
|
166
187
|
end
|
167
188
|
end
|
168
189
|
|
190
|
+
if one_of?
|
191
|
+
if input.size == 1
|
192
|
+
input.each do |name, value|
|
193
|
+
if value.nil?
|
194
|
+
result ||= Query::InputValidationResult.new
|
195
|
+
result.add_problem("'#{graphql_name}' requires exactly one argument, but '#{name}' was `null`.")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
else
|
199
|
+
result ||= Query::InputValidationResult.new
|
200
|
+
result.add_problem("'#{graphql_name}' requires exactly one argument, but #{input.size} were provided.")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
169
204
|
result
|
170
205
|
end
|
171
206
|
|
data/lib/graphql/schema.rb
CHANGED
@@ -31,6 +31,7 @@ require "graphql/schema/union"
|
|
31
31
|
require "graphql/schema/directive"
|
32
32
|
require "graphql/schema/directive/deprecated"
|
33
33
|
require "graphql/schema/directive/include"
|
34
|
+
require "graphql/schema/directive/one_of"
|
34
35
|
require "graphql/schema/directive/skip"
|
35
36
|
require "graphql/schema/directive/feature"
|
36
37
|
require "graphql/schema/directive/flagged"
|
@@ -913,6 +914,7 @@ module GraphQL
|
|
913
914
|
"include" => GraphQL::Schema::Directive::Include,
|
914
915
|
"skip" => GraphQL::Schema::Directive::Skip,
|
915
916
|
"deprecated" => GraphQL::Schema::Directive::Deprecated,
|
917
|
+
"oneOf" => GraphQL::Schema::Directive::OneOf,
|
916
918
|
}.freeze
|
917
919
|
end
|
918
920
|
|
@@ -990,7 +992,7 @@ module GraphQL
|
|
990
992
|
# @param context [Hash] Multiplex-level context
|
991
993
|
# @return [Array<Hash>] One result for each query in the input
|
992
994
|
def multiplex(queries, **kwargs)
|
993
|
-
GraphQL::Execution::
|
995
|
+
GraphQL::Execution::Interpreter.run_all(self, queries, **kwargs)
|
994
996
|
end
|
995
997
|
|
996
998
|
def instrumenters
|
@@ -108,6 +108,10 @@ module GraphQL
|
|
108
108
|
arg_type = @warden.get_argument(type, name).type
|
109
109
|
recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type)
|
110
110
|
end
|
111
|
+
|
112
|
+
if type.one_of? && ast_node.arguments.size != 1
|
113
|
+
results << Query::InputValidationResult.from_problem("`#{type.graphql_name}` is a OneOf type, so only one argument may be given (instead of #{ast_node.arguments.size})")
|
114
|
+
end
|
111
115
|
merge_results(results)
|
112
116
|
end
|
113
117
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module StaticValidation
|
4
|
+
module OneOfInputObjectsAreValid
|
5
|
+
def on_input_object(node, parent)
|
6
|
+
return super unless parent.is_a?(GraphQL::Language::Nodes::Argument)
|
7
|
+
|
8
|
+
parent_type = get_parent_type(context, parent)
|
9
|
+
return super unless parent_type && parent_type.kind.input_object? && parent_type.one_of?
|
10
|
+
|
11
|
+
validate_one_of_input_object(node, context, parent_type)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_one_of_input_object(ast_node, context, parent_type)
|
18
|
+
present_fields = ast_node.arguments.map(&:name)
|
19
|
+
input_object_type = parent_type.to_type_signature
|
20
|
+
|
21
|
+
if present_fields.count != 1
|
22
|
+
add_error(
|
23
|
+
OneOfInputObjectsAreValidError.new(
|
24
|
+
"OneOf Input Object '#{input_object_type}' must specify exactly one key.",
|
25
|
+
path: context.path,
|
26
|
+
nodes: ast_node,
|
27
|
+
input_object_type: input_object_type
|
28
|
+
)
|
29
|
+
)
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
field = present_fields.first
|
34
|
+
value = ast_node.arguments.first.value
|
35
|
+
|
36
|
+
if value.is_a?(GraphQL::Language::Nodes::NullValue)
|
37
|
+
add_error(
|
38
|
+
OneOfInputObjectsAreValidError.new(
|
39
|
+
"Argument '#{input_object_type}.#{field}' must be non-null.",
|
40
|
+
path: [*context.path, field],
|
41
|
+
nodes: ast_node.arguments.first,
|
42
|
+
input_object_type: input_object_type
|
43
|
+
)
|
44
|
+
)
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
49
|
+
variable_name = value.name
|
50
|
+
variable_type = @declared_variables[variable_name].type
|
51
|
+
|
52
|
+
unless variable_type.is_a?(GraphQL::Language::Nodes::NonNullType)
|
53
|
+
add_error(
|
54
|
+
OneOfInputObjectsAreValidError.new(
|
55
|
+
"Variable '#{variable_name}' must be non-nullable to be used for OneOf Input Object '#{input_object_type}'.",
|
56
|
+
path: [*context.path, field],
|
57
|
+
nodes: ast_node,
|
58
|
+
input_object_type: input_object_type
|
59
|
+
)
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module StaticValidation
|
4
|
+
class OneOfInputObjectsAreValidError < StaticValidation::Error
|
5
|
+
attr_reader :input_object_type
|
6
|
+
|
7
|
+
def initialize(message, path:, nodes:, input_object_type:)
|
8
|
+
super(message, path: path, nodes: nodes)
|
9
|
+
@input_object_type = input_object_type
|
10
|
+
end
|
11
|
+
|
12
|
+
# A hash representation of this Message
|
13
|
+
def to_h
|
14
|
+
extensions = {
|
15
|
+
"code" => code,
|
16
|
+
"inputObjectType" => input_object_type
|
17
|
+
}
|
18
|
+
|
19
|
+
super.merge({
|
20
|
+
"extensions" => extensions
|
21
|
+
})
|
22
|
+
end
|
23
|
+
|
24
|
+
def code
|
25
|
+
"invalidOneOfInputObject"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -23,7 +23,7 @@ module GraphQL
|
|
23
23
|
problems = validation_result.problems
|
24
24
|
first_problem = problems && problems.first
|
25
25
|
if first_problem
|
26
|
-
error_message = first_problem["
|
26
|
+
error_message = first_problem["explanation"]
|
27
27
|
end
|
28
28
|
|
29
29
|
error_message ||= "Default value for $#{node.name} doesn't match type #{type.to_type_signature}"
|
@@ -75,10 +75,12 @@ module GraphQL
|
|
75
75
|
end
|
76
76
|
|
77
77
|
def analytics_enabled?
|
78
|
+
# [Deprecated] options[:analytics_enabled] will be removed in the future
|
78
79
|
analytics_available? && Datadog::Contrib::Analytics.enabled?(options.fetch(:analytics_enabled, false))
|
79
80
|
end
|
80
81
|
|
81
82
|
def analytics_sample_rate
|
83
|
+
# [Deprecated] options[:analytics_sample_rate] will be removed in the future
|
82
84
|
options.fetch(:analytics_sample_rate, 1.0)
|
83
85
|
end
|
84
86
|
|
data/lib/graphql/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-08
|
11
|
+
date: 2022-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: benchmark-ips
|
@@ -303,7 +303,6 @@ files:
|
|
303
303
|
- lib/graphql/execution.rb
|
304
304
|
- lib/graphql/execution/directive_checks.rb
|
305
305
|
- lib/graphql/execution/errors.rb
|
306
|
-
- lib/graphql/execution/instrumentation.rb
|
307
306
|
- lib/graphql/execution/interpreter.rb
|
308
307
|
- lib/graphql/execution/interpreter/argument_value.rb
|
309
308
|
- lib/graphql/execution/interpreter/arguments.rb
|
@@ -394,6 +393,7 @@ files:
|
|
394
393
|
- lib/graphql/schema/directive/feature.rb
|
395
394
|
- lib/graphql/schema/directive/flagged.rb
|
396
395
|
- lib/graphql/schema/directive/include.rb
|
396
|
+
- lib/graphql/schema/directive/one_of.rb
|
397
397
|
- lib/graphql/schema/directive/skip.rb
|
398
398
|
- lib/graphql/schema/directive/transform.rb
|
399
399
|
- lib/graphql/schema/enum.rb
|
@@ -497,6 +497,8 @@ files:
|
|
497
497
|
- lib/graphql/static_validation/rules/mutation_root_exists_error.rb
|
498
498
|
- lib/graphql/static_validation/rules/no_definitions_are_present.rb
|
499
499
|
- lib/graphql/static_validation/rules/no_definitions_are_present_error.rb
|
500
|
+
- lib/graphql/static_validation/rules/one_of_input_objects_are_valid.rb
|
501
|
+
- lib/graphql/static_validation/rules/one_of_input_objects_are_valid_error.rb
|
500
502
|
- lib/graphql/static_validation/rules/operation_names_are_valid.rb
|
501
503
|
- lib/graphql/static_validation/rules/operation_names_are_valid_error.rb
|
502
504
|
- lib/graphql/static_validation/rules/query_root_exists.rb
|
@@ -595,7 +597,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
595
597
|
- !ruby/object:Gem::Version
|
596
598
|
version: '0'
|
597
599
|
requirements: []
|
598
|
-
rubygems_version: 3.2.
|
600
|
+
rubygems_version: 3.2.33
|
599
601
|
signing_key:
|
600
602
|
specification_version: 4
|
601
603
|
summary: A GraphQL language and runtime for Ruby
|
@@ -1,92 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module GraphQL
|
3
|
-
module Execution
|
4
|
-
module Instrumentation
|
5
|
-
# This function implements the instrumentation policy:
|
6
|
-
#
|
7
|
-
# - Instrumenters are a stack; the first `before_query` will have the last `after_query`
|
8
|
-
# - If a `before_` hook returned without an error, its corresponding `after_` hook will run.
|
9
|
-
# - If the `before_` hook did _not_ run, the `after_` hook will not be called.
|
10
|
-
#
|
11
|
-
# When errors are raised from `after_` hooks:
|
12
|
-
# - Subsequent `after_` hooks _are_ called
|
13
|
-
# - The first raised error is captured; later errors are ignored
|
14
|
-
# - If an error was capture, it's re-raised after all hooks are finished
|
15
|
-
#
|
16
|
-
# Partial runs of instrumentation are possible:
|
17
|
-
# - If a `before_multiplex` hook raises an error, no `before_query` hooks will run
|
18
|
-
# - If a `before_query` hook raises an error, subsequent `before_query` hooks will not run (on any query)
|
19
|
-
def self.apply_instrumenters(multiplex)
|
20
|
-
schema = multiplex.schema
|
21
|
-
queries = multiplex.queries
|
22
|
-
query_instrumenters = schema.instrumenters[:query]
|
23
|
-
multiplex_instrumenters = schema.instrumenters[:multiplex]
|
24
|
-
|
25
|
-
# First, run multiplex instrumentation, then query instrumentation for each query
|
26
|
-
call_hooks(multiplex_instrumenters, multiplex, :before_multiplex, :after_multiplex) do
|
27
|
-
each_query_call_hooks(query_instrumenters, queries) do
|
28
|
-
# Let them be executed
|
29
|
-
yield
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class << self
|
35
|
-
private
|
36
|
-
# Call the before_ hooks of each query,
|
37
|
-
# Then yield if no errors.
|
38
|
-
# `call_hooks` takes care of appropriate cleanup.
|
39
|
-
def each_query_call_hooks(instrumenters, queries, i = 0)
|
40
|
-
if i >= queries.length
|
41
|
-
yield
|
42
|
-
else
|
43
|
-
query = queries[i]
|
44
|
-
call_hooks(instrumenters, query, :before_query, :after_query) {
|
45
|
-
each_query_call_hooks(instrumenters, queries, i + 1) {
|
46
|
-
yield
|
47
|
-
}
|
48
|
-
}
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# Call each before hook, and if they all succeed, yield.
|
53
|
-
# If they don't all succeed, call after_ for each one that succeeded.
|
54
|
-
def call_hooks(instrumenters, object, before_hook_name, after_hook_name)
|
55
|
-
begin
|
56
|
-
successful = []
|
57
|
-
instrumenters.each do |instrumenter|
|
58
|
-
instrumenter.public_send(before_hook_name, object)
|
59
|
-
successful << instrumenter
|
60
|
-
end
|
61
|
-
|
62
|
-
# if any before hooks raise an exception, quit calling before hooks,
|
63
|
-
# but call the after hooks on anything that succeeded but also
|
64
|
-
# raise the exception that came from the before hook.
|
65
|
-
rescue GraphQL::ExecutionError => err
|
66
|
-
object.context.errors << err
|
67
|
-
rescue => e
|
68
|
-
raise call_after_hooks(successful, object, after_hook_name, e)
|
69
|
-
end
|
70
|
-
|
71
|
-
begin
|
72
|
-
yield # Call the user code
|
73
|
-
ensure
|
74
|
-
ex = call_after_hooks(successful, object, after_hook_name, nil)
|
75
|
-
raise ex if ex
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def call_after_hooks(instrumenters, object, after_hook_name, ex)
|
80
|
-
instrumenters.reverse_each do |instrumenter|
|
81
|
-
begin
|
82
|
-
instrumenter.public_send(after_hook_name, object)
|
83
|
-
rescue => e
|
84
|
-
ex = e
|
85
|
-
end
|
86
|
-
end
|
87
|
-
ex
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|