graphql 1.5.3 → 1.5.4
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/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
|