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.
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