graphql 1.5.3 → 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/define/assign_enum_value.rb +1 -1
  3. data/lib/graphql/execution/directive_checks.rb +5 -5
  4. data/lib/graphql/internal_representation.rb +1 -0
  5. data/lib/graphql/internal_representation/node.rb +117 -16
  6. data/lib/graphql/internal_representation/rewrite.rb +39 -94
  7. data/lib/graphql/internal_representation/scope.rb +88 -0
  8. data/lib/graphql/introspection/schema_field.rb +5 -10
  9. data/lib/graphql/introspection/type_by_name_field.rb +8 -13
  10. data/lib/graphql/introspection/typename_field.rb +5 -10
  11. data/lib/graphql/query.rb +24 -155
  12. data/lib/graphql/query/arguments_cache.rb +25 -0
  13. data/lib/graphql/query/validation_pipeline.rb +114 -0
  14. data/lib/graphql/query/variables.rb +18 -14
  15. data/lib/graphql/schema.rb +4 -3
  16. data/lib/graphql/schema/mask.rb +55 -0
  17. data/lib/graphql/schema/possible_types.rb +2 -2
  18. data/lib/graphql/schema/type_expression.rb +19 -4
  19. data/lib/graphql/schema/warden.rb +1 -3
  20. data/lib/graphql/static_validation/rules/fields_will_merge.rb +3 -2
  21. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +4 -2
  22. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +3 -1
  23. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -20
  24. data/lib/graphql/static_validation/validation_context.rb +6 -18
  25. data/lib/graphql/version.rb +1 -1
  26. data/spec/graphql/enum_type_spec.rb +12 -0
  27. data/spec/graphql/internal_representation/rewrite_spec.rb +26 -5
  28. data/spec/graphql/query_spec.rb +23 -3
  29. data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +2 -2
  30. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +12 -0
  31. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +14 -0
  32. metadata +6 -2
@@ -1,16 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
3
  module Introspection
4
- # A wrapper to implement `__schema`
5
- class SchemaField
6
- def self.create(wrapped_type)
7
- GraphQL::Field.define do
8
- name("__schema")
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
- # A wrapper to create `__type(name: )` dynamically.
5
- class TypeByNameField
6
- def self.create(schema)
7
- GraphQL::Field.define do
8
- name("__type")
9
- description("A type in the GraphQL system")
10
- type(GraphQL::Introspection::TypeType)
11
- argument :name, !types.String
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
- # A wrapper to create `__typename`.
5
- class TypenameField
6
- def self.create(wrapped_type)
7
- GraphQL::Field.define do
8
- name "__typename"
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
@@ -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, :max_depth, :query_string, :warden, :provided_variables
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 = MergedMask.combine(schema.default_mask, except: except, only: only)
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
- @parse_error = nil
58
+ parse_error = nil
67
59
  @document = document || begin
68
60
  GraphQL.parse(query_string)
69
61
  rescue GraphQL::ParseError => err
70
- @parse_error = err
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 = Hash.new { |h, k| h[k] = {} }
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, @operation_name)
87
+ @selected_operation = find_operation(@operations, operation_name)
98
88
  if @selected_operation.nil?
99
- @validation_errors << GraphQL::Query::OperationNameMissingError.new(@operation_name)
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(irep_node, definition)
188
- @arguments_cache[irep_node][definition] ||= begin
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
- def valid?
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