graphql 1.5.3 → 1.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql/define/assign_enum_value.rb +1 -1
- data/lib/graphql/execution/directive_checks.rb +5 -5
- data/lib/graphql/internal_representation.rb +1 -0
- data/lib/graphql/internal_representation/node.rb +117 -16
- data/lib/graphql/internal_representation/rewrite.rb +39 -94
- data/lib/graphql/internal_representation/scope.rb +88 -0
- data/lib/graphql/introspection/schema_field.rb +5 -10
- data/lib/graphql/introspection/type_by_name_field.rb +8 -13
- data/lib/graphql/introspection/typename_field.rb +5 -10
- data/lib/graphql/query.rb +24 -155
- data/lib/graphql/query/arguments_cache.rb +25 -0
- data/lib/graphql/query/validation_pipeline.rb +114 -0
- data/lib/graphql/query/variables.rb +18 -14
- data/lib/graphql/schema.rb +4 -3
- data/lib/graphql/schema/mask.rb +55 -0
- data/lib/graphql/schema/possible_types.rb +2 -2
- data/lib/graphql/schema/type_expression.rb +19 -4
- data/lib/graphql/schema/warden.rb +1 -3
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +3 -2
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +4 -2
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +3 -1
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -20
- data/lib/graphql/static_validation/validation_context.rb +6 -18
- data/lib/graphql/version.rb +1 -1
- data/spec/graphql/enum_type_spec.rb +12 -0
- data/spec/graphql/internal_representation/rewrite_spec.rb +26 -5
- data/spec/graphql/query_spec.rb +23 -3
- data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +2 -2
- data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +12 -0
- data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +14 -0
- metadata +6 -2
@@ -1,16 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GraphQL
|
3
3
|
module Introspection
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
description("This GraphQL schema")
|
10
|
-
type(!GraphQL::Introspection::SchemaType)
|
11
|
-
resolve ->(o, a, c) { wrapped_type }
|
12
|
-
end
|
13
|
-
end
|
4
|
+
SchemaField = GraphQL::Field.define do
|
5
|
+
name("__schema")
|
6
|
+
description("This GraphQL schema")
|
7
|
+
type(!GraphQL::Introspection::SchemaType)
|
8
|
+
resolve ->(o, a, ctx) { ctx.query.schema }
|
14
9
|
end
|
15
10
|
end
|
16
11
|
end
|
@@ -1,19 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GraphQL
|
3
3
|
module Introspection
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
resolve ->(o, args, ctx) {
|
13
|
-
ctx.warden.get_type(args["name"])
|
14
|
-
}
|
15
|
-
end
|
16
|
-
end
|
4
|
+
TypeByNameField = GraphQL::Field.define do
|
5
|
+
name("__type")
|
6
|
+
description("A type in the GraphQL system")
|
7
|
+
type(GraphQL::Introspection::TypeType)
|
8
|
+
argument :name, !types.String
|
9
|
+
resolve ->(o, args, ctx) {
|
10
|
+
ctx.warden.get_type(args["name"])
|
11
|
+
}
|
17
12
|
end
|
18
13
|
end
|
19
14
|
end
|
@@ -1,16 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GraphQL
|
3
3
|
module Introspection
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
description "The name of this type"
|
10
|
-
type -> { !GraphQL::STRING_TYPE }
|
11
|
-
resolve ->(obj, a, c) { wrapped_type.name }
|
12
|
-
end
|
13
|
-
end
|
4
|
+
TypenameField = GraphQL::Field.define do
|
5
|
+
name "__typename"
|
6
|
+
description "The name of this type"
|
7
|
+
type -> { !GraphQL::STRING_TYPE }
|
8
|
+
resolve ->(obj, a, ctx) { ctx.irep_node.owner_type }
|
14
9
|
end
|
15
10
|
end
|
16
11
|
end
|
data/lib/graphql/query.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "graphql/query/arguments"
|
3
|
+
require "graphql/query/arguments_cache"
|
3
4
|
require "graphql/query/context"
|
4
5
|
require "graphql/query/executor"
|
5
6
|
require "graphql/query/literal_input"
|
@@ -7,6 +8,7 @@ require "graphql/query/serial_execution"
|
|
7
8
|
require "graphql/query/variables"
|
8
9
|
require "graphql/query/input_validation_result"
|
9
10
|
require "graphql/query/variable_validation_error"
|
11
|
+
require "graphql/query/validation_pipeline"
|
10
12
|
|
11
13
|
module GraphQL
|
12
14
|
# A combination of query string and {Schema} instance which can be reduced to a {#result}.
|
@@ -24,7 +26,7 @@ module GraphQL
|
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
27
|
-
attr_reader :schema, :document, :context, :fragments, :operations, :root_value, :
|
29
|
+
attr_reader :schema, :document, :context, :fragments, :operations, :root_value, :query_string, :warden, :provided_variables
|
28
30
|
|
29
31
|
# Prepare query `query_string` on `schema`
|
30
32
|
# @param schema [GraphQL::Schema]
|
@@ -41,20 +43,10 @@ module GraphQL
|
|
41
43
|
fail ArgumentError, "a query string or document is required" unless query_string || document
|
42
44
|
|
43
45
|
@schema = schema
|
44
|
-
mask =
|
46
|
+
mask = GraphQL::Schema::Mask.combine(schema.default_mask, except: except, only: only)
|
45
47
|
@context = Context.new(query: self, values: context)
|
46
48
|
@warden = GraphQL::Schema::Warden.new(mask, schema: @schema, context: @context)
|
47
|
-
@max_depth = max_depth || schema.max_depth
|
48
|
-
@max_complexity = max_complexity || schema.max_complexity
|
49
|
-
@query_analyzers = schema.query_analyzers.dup
|
50
|
-
if @max_depth
|
51
|
-
@query_analyzers << GraphQL::Analysis::MaxQueryDepth.new(@max_depth)
|
52
|
-
end
|
53
|
-
if @max_complexity
|
54
|
-
@query_analyzers << GraphQL::Analysis::MaxQueryComplexity.new(@max_complexity)
|
55
|
-
end
|
56
49
|
@root_value = root_value
|
57
|
-
@operation_name = operation_name
|
58
50
|
@fragments = {}
|
59
51
|
@operations = {}
|
60
52
|
if variables.is_a?(String)
|
@@ -63,14 +55,15 @@ module GraphQL
|
|
63
55
|
@provided_variables = variables
|
64
56
|
end
|
65
57
|
@query_string = query_string
|
66
|
-
|
58
|
+
parse_error = nil
|
67
59
|
@document = document || begin
|
68
60
|
GraphQL.parse(query_string)
|
69
61
|
rescue GraphQL::ParseError => err
|
70
|
-
|
62
|
+
parse_error = err
|
71
63
|
@schema.parse_error(err, @context)
|
72
64
|
nil
|
73
65
|
end
|
66
|
+
|
74
67
|
@document && @document.definitions.each do |part|
|
75
68
|
if part.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
76
69
|
@fragments[part.name] = part
|
@@ -83,25 +76,31 @@ module GraphQL
|
|
83
76
|
|
84
77
|
@resolved_types_cache = Hash.new { |h, k| h[k] = @schema.resolve_type(k, @context) }
|
85
78
|
|
86
|
-
@arguments_cache =
|
87
|
-
@validation_errors = []
|
88
|
-
@analysis_errors = []
|
89
|
-
@internal_representation = nil
|
90
|
-
@was_validated = false
|
79
|
+
@arguments_cache = ArgumentsCache.build(self)
|
91
80
|
|
92
81
|
# Trying to execute a document
|
93
82
|
# with no operations returns an empty hash
|
94
83
|
@ast_variables = []
|
95
84
|
@mutation = false
|
85
|
+
operation_name_error = nil
|
96
86
|
if @operations.any?
|
97
|
-
@selected_operation = find_operation(@operations,
|
87
|
+
@selected_operation = find_operation(@operations, operation_name)
|
98
88
|
if @selected_operation.nil?
|
99
|
-
|
89
|
+
operation_name_error = GraphQL::Query::OperationNameMissingError.new(operation_name)
|
100
90
|
else
|
101
91
|
@ast_variables = @selected_operation.variables
|
102
92
|
@mutation = @selected_operation.operation_type == "mutation"
|
103
93
|
end
|
104
94
|
end
|
95
|
+
|
96
|
+
@validation_pipeline = GraphQL::Query::ValidationPipeline.new(
|
97
|
+
query: self,
|
98
|
+
parse_error: parse_error,
|
99
|
+
operation_name_error: operation_name_error,
|
100
|
+
max_depth: max_depth || schema.max_depth,
|
101
|
+
max_complexity: max_complexity || schema.max_complexity,
|
102
|
+
)
|
103
|
+
|
105
104
|
@result = nil
|
106
105
|
@executed = false
|
107
106
|
end
|
@@ -152,75 +151,28 @@ module GraphQL
|
|
152
151
|
@ast_variables,
|
153
152
|
@provided_variables,
|
154
153
|
)
|
155
|
-
@validation_errors.concat(vars.errors)
|
156
154
|
vars
|
157
155
|
end
|
158
156
|
end
|
159
157
|
|
160
|
-
# @return [Hash<String, nil => GraphQL::InternalRepresentation::Node] Operation name -> Irep node pairs
|
161
|
-
def internal_representation
|
162
|
-
valid?
|
163
|
-
@internal_representation
|
164
|
-
end
|
165
|
-
|
166
158
|
def irep_selection
|
167
159
|
@selection ||= internal_representation[selected_operation.name]
|
168
160
|
end
|
169
161
|
|
170
|
-
|
171
|
-
# TODO this should probably contain error instances, not hashes
|
172
|
-
# @return [Array<Hash>] Static validation errors for the query string
|
173
|
-
def validation_errors
|
174
|
-
valid?
|
175
|
-
@validation_errors
|
176
|
-
end
|
177
|
-
|
178
|
-
# TODO this should probably contain error instances, not hashes
|
179
|
-
# @return [Array<Hash>] Errors for this particular query run (eg, exceeds max complexity)
|
180
|
-
def analysis_errors
|
181
|
-
valid?
|
182
|
-
@analysis_errors
|
183
|
-
end
|
184
|
-
|
185
162
|
# Node-level cache for calculating arguments. Used during execution and query analysis.
|
163
|
+
# @api private
|
186
164
|
# @return [GraphQL::Query::Arguments] Arguments for this node, merging default values, literal values and query variables
|
187
|
-
def arguments_for(
|
188
|
-
@arguments_cache[
|
189
|
-
ast_node = case irep_node
|
190
|
-
when GraphQL::Language::Nodes::AbstractNode
|
191
|
-
irep_node
|
192
|
-
else
|
193
|
-
irep_node.ast_node
|
194
|
-
end
|
195
|
-
ast_arguments = ast_node.arguments
|
196
|
-
if ast_arguments.none?
|
197
|
-
definition.default_arguments
|
198
|
-
else
|
199
|
-
GraphQL::Query::LiteralInput.from_arguments(
|
200
|
-
ast_arguments,
|
201
|
-
definition.arguments,
|
202
|
-
self.variables
|
203
|
-
)
|
204
|
-
end
|
205
|
-
end
|
165
|
+
def arguments_for(irep_or_ast_node, definition)
|
166
|
+
@arguments_cache[irep_or_ast_node][definition]
|
206
167
|
end
|
207
168
|
|
208
169
|
# @return [GraphQL::Language::Nodes::Document, nil]
|
209
170
|
attr_reader :selected_operation
|
210
171
|
|
211
|
-
|
212
|
-
@was_validated ||= begin
|
213
|
-
@was_validated = true
|
214
|
-
@valid = @parse_error.nil? && document_valid? && query_valid? && variables.errors.none?
|
215
|
-
true
|
216
|
-
end
|
217
|
-
|
218
|
-
@valid
|
219
|
-
end
|
172
|
+
def_delegators :@validation_pipeline, :valid?, :analysis_errors, :validation_errors, :internal_representation
|
220
173
|
|
221
174
|
def_delegators :@warden, :get_type, :get_field, :possible_types, :root_type_for_operation
|
222
175
|
|
223
|
-
|
224
176
|
# @param value [Object] Any runtime value
|
225
177
|
# @return [GraphQL::ObjectType, nil] The runtime type of `value` from {Schema#resolve_type}
|
226
178
|
# @see {#possible_types} to apply filtering from `only` / `except`
|
@@ -234,30 +186,6 @@ module GraphQL
|
|
234
186
|
|
235
187
|
private
|
236
188
|
|
237
|
-
# Assert that the passed-in query string is internally consistent
|
238
|
-
def document_valid?
|
239
|
-
validation_result = schema.static_validator.validate(self)
|
240
|
-
@validation_errors.concat(validation_result[:errors])
|
241
|
-
@internal_representation = validation_result[:irep]
|
242
|
-
@validation_errors.none?
|
243
|
-
end
|
244
|
-
|
245
|
-
# Given that we _could_ execute this query, _should_ we?
|
246
|
-
# - Does it violate any query analyzers?
|
247
|
-
def query_valid?
|
248
|
-
@analysis_errors = begin
|
249
|
-
if @query_analyzers.any?
|
250
|
-
reduce_results = GraphQL::Analysis.analyze_query(self, @query_analyzers)
|
251
|
-
reduce_results
|
252
|
-
.flatten # accept n-dimensional array
|
253
|
-
.select { |r| r.is_a?(GraphQL::AnalysisError) }
|
254
|
-
else
|
255
|
-
[]
|
256
|
-
end
|
257
|
-
end
|
258
|
-
@analysis_errors.none?
|
259
|
-
end
|
260
|
-
|
261
189
|
def find_operation(operations, operation_name)
|
262
190
|
if operation_name.nil? && operations.length == 1
|
263
191
|
operations.values.first
|
@@ -267,64 +195,5 @@ module GraphQL
|
|
267
195
|
operations.fetch(operation_name)
|
268
196
|
end
|
269
197
|
end
|
270
|
-
|
271
|
-
# @api private
|
272
|
-
class InvertedMask
|
273
|
-
def initialize(inner_mask)
|
274
|
-
@inner_mask = inner_mask
|
275
|
-
end
|
276
|
-
|
277
|
-
# Returns true when the inner mask returned false
|
278
|
-
# Returns false when the inner mask returned true
|
279
|
-
def call(member, ctx)
|
280
|
-
!@inner_mask.call(member, ctx)
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
# @api private
|
285
|
-
class LegacyMaskWrap
|
286
|
-
def initialize(inner_mask)
|
287
|
-
@inner_mask = inner_mask
|
288
|
-
end
|
289
|
-
|
290
|
-
def call(member, ctx)
|
291
|
-
@inner_mask.call(member)
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
# @api private
|
296
|
-
class MergedMask
|
297
|
-
def initialize(first_mask, second_mask)
|
298
|
-
@first_mask = first_mask
|
299
|
-
@second_mask = second_mask
|
300
|
-
end
|
301
|
-
|
302
|
-
def call(member, ctx)
|
303
|
-
@first_mask.call(member, ctx) || @second_mask.call(member, ctx)
|
304
|
-
end
|
305
|
-
|
306
|
-
def self.combine(default_mask, except:, only:)
|
307
|
-
query_mask = if except
|
308
|
-
wrap_if_legacy_mask(except)
|
309
|
-
elsif only
|
310
|
-
InvertedMask.new(wrap_if_legacy_mask(only))
|
311
|
-
end
|
312
|
-
|
313
|
-
if query_mask && (default_mask != GraphQL::Schema::NullMask)
|
314
|
-
self.new(default_mask, query_mask)
|
315
|
-
else
|
316
|
-
query_mask || default_mask
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
def self.wrap_if_legacy_mask(mask)
|
321
|
-
if (mask.is_a?(Proc) && mask.arity == 1) || mask.method(:call).arity == 1
|
322
|
-
warn("Schema.execute(..., except:) filters now accept two arguments, `(member, ctx)`. One-argument filters are deprecated.")
|
323
|
-
LegacyMaskWrap.new(mask)
|
324
|
-
else
|
325
|
-
mask
|
326
|
-
end
|
327
|
-
end
|
328
|
-
end
|
329
198
|
end
|
330
199
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
class Query
|
4
|
+
module ArgumentsCache
|
5
|
+
# @return [Hash<InternalRepresentation::Node, GraphQL::Language::NodesDirectiveNode => Hash<GraphQL::Field, GraphQL::Directive => GraphQL::Query::Arguments>>]
|
6
|
+
def self.build(query)
|
7
|
+
Hash.new do |h1, irep_or_ast_node|
|
8
|
+
Hash.new do |h2, definition|
|
9
|
+
ast_node = irep_or_ast_node.is_a?(GraphQL::InternalRepresentation::Node) ? irep_or_ast_node.ast_node : irep_or_ast_node
|
10
|
+
ast_arguments = ast_node.arguments
|
11
|
+
if ast_arguments.none?
|
12
|
+
definition.default_arguments
|
13
|
+
else
|
14
|
+
GraphQL::Query::LiteralInput.from_arguments(
|
15
|
+
ast_arguments,
|
16
|
+
definition.arguments,
|
17
|
+
query.variables,
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
class Query
|
4
|
+
# Contain the validation pipeline and expose the results.
|
5
|
+
#
|
6
|
+
# 0. Checks in {Query#initialize}:
|
7
|
+
# - Rescue a ParseError, halt if there is one
|
8
|
+
# - Check for selected operation, halt if not found
|
9
|
+
# 1. Validate the AST, halt if errors
|
10
|
+
# 2. Validate the variables, halt if errors
|
11
|
+
# 3. Run query analyzers, halt if errors
|
12
|
+
#
|
13
|
+
# {#valid?} is false if any of the above checks halted the pipeline.
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
class ValidationPipeline
|
17
|
+
def initialize(query:, parse_error:, operation_name_error:, max_depth:, max_complexity:)
|
18
|
+
@validation_errors = []
|
19
|
+
@analysis_errors = []
|
20
|
+
@internal_representation = nil
|
21
|
+
@parse_error = parse_error
|
22
|
+
@operation_name_error = operation_name_error
|
23
|
+
@query = query
|
24
|
+
@schema = query.schema
|
25
|
+
@max_depth = max_depth
|
26
|
+
@max_complexity = max_complexity
|
27
|
+
|
28
|
+
@has_validated = false
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Boolean] does this query have errors that should prevent it from running?
|
32
|
+
def valid?
|
33
|
+
ensure_has_validated
|
34
|
+
@valid
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Array<GraphQL::AnalysisError>] Errors for this particular query run (eg, exceeds max complexity)
|
38
|
+
def analysis_errors
|
39
|
+
ensure_has_validated
|
40
|
+
@analysis_errors
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Array<GraphQL::StaticValidation::Message>] Static validation errors for the query string
|
44
|
+
def validation_errors
|
45
|
+
ensure_has_validated
|
46
|
+
@validation_errors
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Hash<String, nil => GraphQL::InternalRepresentation::Node] Operation name -> Irep node pairs
|
50
|
+
def internal_representation
|
51
|
+
ensure_has_validated
|
52
|
+
@internal_representation
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# If the pipeline wasn't run yet, run it.
|
58
|
+
# If it was already run, do nothing.
|
59
|
+
def ensure_has_validated
|
60
|
+
return if @has_validated
|
61
|
+
@has_validated = true
|
62
|
+
|
63
|
+
if @parse_error
|
64
|
+
# This is kind of crazy: we push the parse error into `ctx`
|
65
|
+
# in {DefaultParseError} so that users can _opt out_ by redefining that hook.
|
66
|
+
# That means we can't _re-add_ the error here (otherwise we'd either
|
67
|
+
# add it twice _or_ override the user's choice to not add it).
|
68
|
+
# So we just have to know that it was invalid and go from there.
|
69
|
+
@valid = false
|
70
|
+
return
|
71
|
+
elsif @operation_name_error
|
72
|
+
@validation_errors << @operation_name_error
|
73
|
+
else
|
74
|
+
validation_result = @schema.static_validator.validate(@query)
|
75
|
+
@validation_errors.concat(validation_result[:errors])
|
76
|
+
@internal_representation = validation_result[:irep]
|
77
|
+
|
78
|
+
if @validation_errors.none?
|
79
|
+
@validation_errors.concat(@query.variables.errors)
|
80
|
+
end
|
81
|
+
|
82
|
+
if @validation_errors.none?
|
83
|
+
query_analyzers = build_analyzers(@schema, @max_depth, @max_complexity)
|
84
|
+
if query_analyzers.any?
|
85
|
+
analysis_results = GraphQL::Analysis.analyze_query(@query, query_analyzers)
|
86
|
+
@analysis_errors = analysis_results
|
87
|
+
.flatten # accept n-dimensional array
|
88
|
+
.select { |r| r.is_a?(GraphQL::AnalysisError) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@valid = @validation_errors.none? && @analysis_errors.none?
|
94
|
+
end
|
95
|
+
|
96
|
+
# If there are max_* values, add them,
|
97
|
+
# otherwise reuse the schema's list of analyzers.
|
98
|
+
def build_analyzers(schema, max_depth, max_complexity)
|
99
|
+
if max_depth || max_complexity
|
100
|
+
qa = schema.query_analyzers.dup
|
101
|
+
if max_depth
|
102
|
+
qa << GraphQL::Analysis::MaxQueryDepth.new(max_depth)
|
103
|
+
end
|
104
|
+
if max_complexity
|
105
|
+
qa << GraphQL::Analysis::MaxQueryComplexity.new(max_complexity)
|
106
|
+
end
|
107
|
+
qa
|
108
|
+
else
|
109
|
+
schema.query_analyzers
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|