graphql 2.2.5 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/templates/schema.erb +3 -0
  3. data/lib/graphql/analysis/ast/field_usage.rb +36 -9
  4. data/lib/graphql/analysis/ast/visitor.rb +8 -0
  5. data/lib/graphql/analysis/ast.rb +10 -1
  6. data/lib/graphql/backtrace/inspect_result.rb +0 -12
  7. data/lib/graphql/coercion_error.rb +1 -9
  8. data/lib/graphql/dataloader/request.rb +5 -0
  9. data/lib/graphql/execution/interpreter/argument_value.rb +5 -1
  10. data/lib/graphql/execution/interpreter/runtime.rb +9 -0
  11. data/lib/graphql/execution/interpreter.rb +90 -150
  12. data/lib/graphql/introspection/entry_points.rb +9 -3
  13. data/lib/graphql/introspection/schema_type.rb +3 -1
  14. data/lib/graphql/language/document_from_schema_definition.rb +2 -3
  15. data/lib/graphql/language/lexer.rb +48 -30
  16. data/lib/graphql/language/nodes.rb +1 -1
  17. data/lib/graphql/language/parser.rb +25 -11
  18. data/lib/graphql/language/printer.rb +4 -0
  19. data/lib/graphql/language.rb +60 -0
  20. data/lib/graphql/pagination/array_connection.rb +6 -6
  21. data/lib/graphql/query/context.rb +30 -33
  22. data/lib/graphql/query/validation_pipeline.rb +2 -2
  23. data/lib/graphql/query/variables.rb +3 -3
  24. data/lib/graphql/query.rb +3 -3
  25. data/lib/graphql/schema/argument.rb +1 -0
  26. data/lib/graphql/schema/base_64_encoder.rb +3 -5
  27. data/lib/graphql/schema/build_from_definition.rb +9 -1
  28. data/lib/graphql/schema/field.rb +33 -30
  29. data/lib/graphql/schema/input_object.rb +1 -2
  30. data/lib/graphql/schema/interface.rb +5 -1
  31. data/lib/graphql/schema/loader.rb +2 -1
  32. data/lib/graphql/schema/member/has_arguments.rb +2 -2
  33. data/lib/graphql/schema/mutation.rb +7 -0
  34. data/lib/graphql/schema/resolver.rb +19 -10
  35. data/lib/graphql/schema/unique_within_type.rb +1 -1
  36. data/lib/graphql/schema.rb +119 -28
  37. data/lib/graphql/static_validation/literal_validator.rb +1 -2
  38. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  39. data/lib/graphql/static_validation/validator.rb +3 -0
  40. data/lib/graphql/subscriptions/serialize.rb +2 -0
  41. data/lib/graphql/subscriptions.rb +0 -3
  42. data/lib/graphql/testing/helpers.rb +32 -6
  43. data/lib/graphql/tracing/data_dog_trace.rb +21 -34
  44. data/lib/graphql/tracing/data_dog_tracing.rb +7 -21
  45. data/lib/graphql/tracing/legacy_hooks_trace.rb +74 -0
  46. data/lib/graphql/tracing/platform_tracing.rb +3 -1
  47. data/lib/graphql/tracing/{prometheus_tracing → prometheus_trace}/graphql_collector.rb +3 -1
  48. data/lib/graphql/tracing/prometheus_trace.rb +2 -2
  49. data/lib/graphql/tracing/sentry_trace.rb +112 -0
  50. data/lib/graphql/tracing.rb +3 -1
  51. data/lib/graphql/version.rb +1 -1
  52. data/lib/graphql.rb +10 -2
  53. metadata +38 -23
  54. data/lib/graphql/schema/base_64_bp.rb +0 -26
  55. data/lib/graphql/subscriptions/instrumentation.rb +0 -28
@@ -3,7 +3,7 @@ module GraphQL
3
3
  module Language
4
4
 
5
5
  class Lexer
6
- def initialize(graphql_str, filename: nil)
6
+ def initialize(graphql_str, filename: nil, max_tokens: nil)
7
7
  if !(graphql_str.encoding == Encoding::UTF_8 || graphql_str.ascii_only?)
8
8
  graphql_str = graphql_str.dup.force_encoding(Encoding::UTF_8)
9
9
  end
@@ -11,6 +11,8 @@ module GraphQL
11
11
  @filename = filename
12
12
  @scanner = StringScanner.new(graphql_str)
13
13
  @pos = nil
14
+ @max_tokens = max_tokens || Float::INFINITY
15
+ @tokens_count = 0
14
16
  end
15
17
 
16
18
  def eos?
@@ -22,6 +24,10 @@ module GraphQL
22
24
  def advance
23
25
  @scanner.skip(IGNORE_REGEXP)
24
26
  return false if @scanner.eos?
27
+ @tokens_count += 1
28
+ if @tokens_count > @max_tokens
29
+ raise_parse_error("This query is too large to execute.")
30
+ end
25
31
  @pos = @scanner.pos
26
32
  next_byte = @string.getbyte(@pos)
27
33
  next_byte_is_for = FIRST_BYTES[next_byte]
@@ -52,6 +58,17 @@ module GraphQL
52
58
  :IDENTIFIER
53
59
  when ByteFor::NUMBER
54
60
  @scanner.skip(NUMERIC_REGEXP)
61
+
62
+ if GraphQL.reject_numbers_followed_by_names
63
+ new_pos = @scanner.pos
64
+ peek_byte = @string.getbyte(new_pos)
65
+ next_first_byte = FIRST_BYTES[peek_byte]
66
+ if next_first_byte == ByteFor::NAME || next_first_byte == ByteFor::IDENTIFIER
67
+ number_part = token_value
68
+ name_part = @scanner.scan(IDENTIFIER_REGEXP)
69
+ raise_parse_error("Name after number is not allowed (in `#{number_part}#{name_part}`)")
70
+ end
71
+ end
55
72
  # Check for a matched decimal:
56
73
  @scanner[1] ? :FLOAT : :INT
57
74
  when ByteFor::ELLIPSIS
@@ -89,6 +106,8 @@ module GraphQL
89
106
  "..."
90
107
  elsif token_name == :STRING
91
108
  string_value
109
+ elsif @scanner.matched_size.nil?
110
+ @scanner.peek(1)
92
111
  else
93
112
  token_value
94
113
  end
@@ -107,29 +126,27 @@ module GraphQL
107
126
  }
108
127
  UTF_8 = /\\u(?:([\dAa-f]{4})|\{([\da-f]{4,})\})(?:\\u([\dAa-f]{4}))?/i
109
128
  VALID_STRING = /\A(?:[^\\]|#{ESCAPES}|#{UTF_8})*\z/o
129
+ ESCAPED = /(?:#{ESCAPES}|#{UTF_8})/o
110
130
 
111
131
  def string_value
112
132
  str = token_value
113
133
  is_block = str.start_with?('"""')
114
134
  if is_block
115
135
  str.gsub!(/\A"""|"""\z/, '')
136
+ return Language::BlockString.trim_whitespace(str)
116
137
  else
117
138
  str.gsub!(/\A"|"\z/, '')
118
- end
119
-
120
- if is_block
121
- str = Language::BlockString.trim_whitespace(str)
122
- end
123
-
124
- if !str.valid_encoding? || !str.match?(VALID_STRING)
125
- raise_parse_error("Bad unicode escape in #{str.inspect}")
126
- else
127
- Lexer.replace_escaped_characters_in_place(str)
128
139
 
129
- if !str.valid_encoding?
140
+ if !str.valid_encoding? || !str.match?(VALID_STRING)
130
141
  raise_parse_error("Bad unicode escape in #{str.inspect}")
131
142
  else
132
- str
143
+ Lexer.replace_escaped_characters_in_place(str)
144
+
145
+ if !str.valid_encoding?
146
+ raise_parse_error("Bad unicode escape in #{str.inspect}")
147
+ else
148
+ str
149
+ end
133
150
  end
134
151
  end
135
152
  end
@@ -156,6 +173,7 @@ module GraphQL
156
173
  INT_REGEXP = /-?(?:[0]|[1-9][0-9]*)/
157
174
  FLOAT_DECIMAL_REGEXP = /[.][0-9]+/
158
175
  FLOAT_EXP_REGEXP = /[eE][+-]?[0-9]+/
176
+ # TODO: FLOAT_EXP_REGEXP should not be allowed to follow INT_REGEXP, integers are not allowed to have exponent parts.
159
177
  NUMERIC_REGEXP = /#{INT_REGEXP}(#{FLOAT_DECIMAL_REGEXP}#{FLOAT_EXP_REGEXP}|#{FLOAT_DECIMAL_REGEXP}|#{FLOAT_EXP_REGEXP})?/
160
178
 
161
179
  KEYWORDS = [
@@ -250,11 +268,10 @@ module GraphQL
250
268
  FOUR_DIGIT_UNICODE = /#{UNICODE_DIGIT}{4}/
251
269
  N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}{4,}#{Punctuation::RCURLY}}x
252
270
  UNICODE_ESCAPE = %r{\\u(?:#{FOUR_DIGIT_UNICODE}|#{N_DIGIT_UNICODE})}
253
- # # https://graphql.github.io/graphql-spec/June2018/#sec-String-Value
254
271
  STRING_ESCAPE = %r{[\\][\\/bfnrt]}
255
272
  BLOCK_QUOTE = '"""'
256
273
  ESCAPED_QUOTE = /\\"/;
257
- STRING_CHAR = /#{ESCAPED_QUOTE}|[^"\\]|#{UNICODE_ESCAPE}|#{STRING_ESCAPE}/
274
+ STRING_CHAR = /#{ESCAPED_QUOTE}|[^"\\\n\r]|#{UNICODE_ESCAPE}|#{STRING_ESCAPE}/
258
275
  QUOTED_STRING_REGEXP = %r{#{QUOTE} (?:#{STRING_CHAR})* #{QUOTE}}x
259
276
  BLOCK_STRING_REGEXP = %r{
260
277
  #{BLOCK_QUOTE}
@@ -299,24 +316,25 @@ module GraphQL
299
316
  # Replace any escaped unicode or whitespace with the _actual_ characters
300
317
  # To avoid allocating more strings, this modifies the string passed into it
301
318
  def self.replace_escaped_characters_in_place(raw_string)
302
- raw_string.gsub!(ESCAPES, ESCAPES_REPLACE)
303
- raw_string.gsub!(UTF_8) do |_matched_str|
304
- codepoint_1 = ($1 || $2).to_i(16)
305
- codepoint_2 = $3
306
-
307
- if codepoint_2
308
- codepoint_2 = codepoint_2.to_i(16)
309
- if (codepoint_1 >= 0xD800 && codepoint_1 <= 0xDBFF) && # leading surrogate
310
- (codepoint_2 >= 0xDC00 && codepoint_2 <= 0xDFFF) # trailing surrogate
311
- # A surrogate pair
312
- combined = ((codepoint_1 - 0xD800) * 0x400) + (codepoint_2 - 0xDC00) + 0x10000
313
- [combined].pack('U'.freeze)
319
+ raw_string.gsub!(ESCAPED) do |matched_str|
320
+ if (point_str_1 = $1 || $2)
321
+ codepoint_1 = point_str_1.to_i(16)
322
+ if (codepoint_2 = $3)
323
+ codepoint_2 = codepoint_2.to_i(16)
324
+ if (codepoint_1 >= 0xD800 && codepoint_1 <= 0xDBFF) && # leading surrogate
325
+ (codepoint_2 >= 0xDC00 && codepoint_2 <= 0xDFFF) # trailing surrogate
326
+ # A surrogate pair
327
+ combined = ((codepoint_1 - 0xD800) * 0x400) + (codepoint_2 - 0xDC00) + 0x10000
328
+ [combined].pack('U'.freeze)
329
+ else
330
+ # Two separate code points
331
+ [codepoint_1].pack('U'.freeze) + [codepoint_2].pack('U'.freeze)
332
+ end
314
333
  else
315
- # Two separate code points
316
- [codepoint_1].pack('U'.freeze) + [codepoint_2].pack('U'.freeze)
334
+ [codepoint_1].pack('U'.freeze)
317
335
  end
318
336
  else
319
- [codepoint_1].pack('U'.freeze)
337
+ ESCAPES_REPLACE[matched_str]
320
338
  end
321
339
  end
322
340
  nil
@@ -512,7 +512,7 @@ module GraphQL
512
512
  # An operation-level query variable
513
513
  class VariableDefinition < AbstractNode
514
514
  scalar_methods :name, :type, :default_value
515
- children_methods false
515
+ children_methods(directives: Directive)
516
516
  # @!attribute default_value
517
517
  # @return [String, Integer, Float, Boolean, Array, NullValue] A Ruby value to use if no other value is provided
518
518
 
@@ -12,8 +12,8 @@ module GraphQL
12
12
  class << self
13
13
  attr_accessor :cache
14
14
 
15
- def parse(graphql_str, filename: nil, trace: Tracing::NullTrace)
16
- self.new(graphql_str, filename: filename, trace: trace).parse
15
+ def parse(graphql_str, filename: nil, trace: Tracing::NullTrace, max_tokens: nil)
16
+ self.new(graphql_str, filename: filename, trace: trace, max_tokens: max_tokens).parse
17
17
  end
18
18
 
19
19
  def parse_file(filename, trace: Tracing::NullTrace)
@@ -27,14 +27,15 @@ module GraphQL
27
27
  end
28
28
  end
29
29
 
30
- def initialize(graphql_str, filename: nil, trace: Tracing::NullTrace)
30
+ def initialize(graphql_str, filename: nil, trace: Tracing::NullTrace, max_tokens: nil)
31
31
  if graphql_str.nil?
32
32
  raise GraphQL::ParseError.new("No query string was present", nil, nil, nil)
33
33
  end
34
- @lexer = Lexer.new(graphql_str, filename: filename)
34
+ @lexer = Lexer.new(graphql_str, filename: filename, max_tokens: max_tokens)
35
35
  @graphql_str = graphql_str
36
36
  @filename = filename
37
37
  @trace = trace
38
+ @dedup_identifiers = false
38
39
  end
39
40
 
40
41
  def parse
@@ -121,7 +122,17 @@ module GraphQL
121
122
  value
122
123
  end
123
124
 
124
- defs << Nodes::VariableDefinition.new(pos: loc, name: var_name, type: var_type, default_value: default_value, filename: @filename, source_string: @graphql_str)
125
+ directives = parse_directives
126
+
127
+ defs << Nodes::VariableDefinition.new(
128
+ pos: loc,
129
+ name: var_name,
130
+ type: var_type,
131
+ default_value: default_value,
132
+ directives: directives,
133
+ filename: @filename,
134
+ source_string: @graphql_str
135
+ )
125
136
  end
126
137
  expect_token(:RPAREN)
127
138
  defs
@@ -722,6 +733,9 @@ module GraphQL
722
733
  # Only use when we care about the expected token's value
723
734
  def expect_token_value(tok)
724
735
  token_value = @lexer.token_value
736
+ if @dedup_identifiers
737
+ token_value = -token_value
738
+ end
725
739
  expect_token(tok)
726
740
  token_value
727
741
  end
@@ -729,12 +743,12 @@ module GraphQL
729
743
  # token_value works for when the scanner matched something
730
744
  # which is usually fine and it's good for it to be fast at that.
731
745
  def debug_token_value
732
- if token_name && Lexer::Punctuation.const_defined?(token_name)
733
- Lexer::Punctuation.const_get(token_name)
734
- elsif token_name == :ELLIPSIS
735
- "..."
736
- else
737
- @lexer.token_value
746
+ @lexer.debug_token_value(token_name)
747
+ end
748
+ class SchemaParser < Parser
749
+ def initialize(*args, **kwargs)
750
+ super
751
+ @dedup_identifiers = true
738
752
  end
739
753
  end
740
754
  end
@@ -208,6 +208,10 @@ module GraphQL
208
208
  print_string(" = ")
209
209
  print_node(variable_definition.default_value)
210
210
  end
211
+ variable_definition.directives.each do |dir|
212
+ print_string(" ")
213
+ print_directive(dir)
214
+ end
211
215
  end
212
216
 
213
217
  def print_variable_identifier(variable_identifier)
@@ -12,6 +12,7 @@ require "graphql/language/static_visitor"
12
12
  require "graphql/language/token"
13
13
  require "graphql/language/visitor"
14
14
  require "graphql/language/definition_slice"
15
+ require "strscan"
15
16
 
16
17
  module GraphQL
17
18
  module Language
@@ -32,6 +33,65 @@ module GraphQL
32
33
  else
33
34
  JSON.generate(value, quirks_mode: true)
34
35
  end
36
+ rescue JSON::GeneratorError
37
+ if Float::INFINITY == value
38
+ "Infinity"
39
+ else
40
+ raise
41
+ end
42
+ end
43
+
44
+ # Returns a new string if any single-quoted newlines were escaped.
45
+ # Otherwise, returns `query_str` unchanged.
46
+ # @return [String]
47
+ def self.escape_single_quoted_newlines(query_str)
48
+ scanner = StringScanner.new(query_str)
49
+ inside_single_quoted_string = false
50
+ new_query_str = nil
51
+ while !scanner.eos?
52
+ if (match = scanner.scan(/(?:\\"|[^"\n\r]|""")+/m)) && new_query_str
53
+ new_query_str << match
54
+ elsif scanner.scan('"')
55
+ new_query_str && (new_query_str << '"')
56
+ inside_single_quoted_string = !inside_single_quoted_string
57
+ elsif scanner.scan("\n")
58
+ if inside_single_quoted_string
59
+ new_query_str ||= query_str[0, scanner.pos - 1]
60
+ new_query_str << '\\n'
61
+ else
62
+ new_query_str && (new_query_str << "\n")
63
+ end
64
+ elsif scanner.scan("\r")
65
+ if inside_single_quoted_string
66
+ new_query_str ||= query_str[0, scanner.pos - 1]
67
+ new_query_str << '\\r'
68
+ else
69
+ new_query_str && (new_query_str << "\r")
70
+ end
71
+ elsif scanner.eos?
72
+ break
73
+ else
74
+ raise ArgumentError, "Unmatchable string scanner segment: #{scanner.rest.inspect}"
75
+ end
76
+ end
77
+ new_query_str || query_str
78
+ end
79
+
80
+ INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP = %r{
81
+ (
82
+ ((?<num>#{Lexer::INT_REGEXP}(#{Lexer::FLOAT_EXP_REGEXP})?)(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
83
+ |
84
+ ((?<num>#{Lexer::INT_REGEXP}#{Lexer::FLOAT_DECIMAL_REGEXP}#{Lexer::FLOAT_EXP_REGEXP})(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
85
+ |
86
+ ((?<num>#{Lexer::INT_REGEXP}#{Lexer::FLOAT_DECIMAL_REGEXP})(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
87
+ )}x
88
+
89
+ def self.add_space_between_numbers_and_names(query_str)
90
+ if query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP)
91
+ query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k<num> \\k<name>:")
92
+ else
93
+ query_str
94
+ end
35
95
  end
36
96
  end
37
97
  end
@@ -35,10 +35,10 @@ module GraphQL
35
35
  def load_nodes
36
36
  @nodes ||= begin
37
37
  sliced_nodes = if before && after
38
- end_idx = index_from_cursor(before)-1
38
+ end_idx = index_from_cursor(before) - 2
39
39
  end_idx < 0 ? [] : items[index_from_cursor(after)..end_idx] || []
40
40
  elsif before
41
- end_idx = index_from_cursor(before)-2
41
+ end_idx = index_from_cursor(before) - 2
42
42
  end_idx < 0 ? [] : items[0..end_idx] || []
43
43
  elsif after
44
44
  items[index_from_cursor(after)..-1] || []
@@ -56,12 +56,12 @@ module GraphQL
56
56
  false
57
57
  end
58
58
 
59
- @has_next_page = if first
60
- # There are more items after these items
61
- sliced_nodes.count > first
62
- elsif before
59
+ @has_next_page = if before
63
60
  # The original array is longer than the `before` index
64
61
  index_from_cursor(before) < items.length + 1
62
+ elsif first
63
+ # There are more items after these items
64
+ sliced_nodes.count > first
65
65
  else
66
66
  false
67
67
  end
@@ -6,36 +6,6 @@ module GraphQL
6
6
  # Expose some query-specific info to field resolve functions.
7
7
  # It delegates `[]` to the hash that's passed to `GraphQL::Query#initialize`.
8
8
  class Context
9
- module SharedMethods
10
- # Return this value to tell the runtime
11
- # to exclude this field from the response altogether
12
- def skip
13
- GraphQL::Execution::SKIP
14
- end
15
-
16
- # Add error at query-level.
17
- # @param error [GraphQL::ExecutionError] an execution error
18
- # @return [void]
19
- def add_error(error)
20
- if !error.is_a?(ExecutionError)
21
- raise TypeError, "expected error to be a ExecutionError, but was #{error.class}"
22
- end
23
- errors << error
24
- nil
25
- end
26
-
27
- # @example Print the GraphQL backtrace during field resolution
28
- # puts ctx.backtrace
29
- #
30
- # @return [GraphQL::Backtrace] The backtrace for this point in query execution
31
- def backtrace
32
- GraphQL::Backtrace.new(self)
33
- end
34
-
35
- def execution_errors
36
- @execution_errors ||= ExecutionErrors.new(self)
37
- end
38
- end
39
9
 
40
10
  class ExecutionErrors
41
11
  def initialize(ctx)
@@ -59,7 +29,6 @@ module GraphQL
59
29
  alias :push :add
60
30
  end
61
31
 
62
- include SharedMethods
63
32
  extend Forwardable
64
33
 
65
34
  # @return [Array<GraphQL::ExecutionError>] errors returned during execution
@@ -77,11 +46,10 @@ module GraphQL
77
46
  # Make a new context which delegates key lookup to `values`
78
47
  # @param query [GraphQL::Query] the query who owns this context
79
48
  # @param values [Hash] A hash of arbitrary values which will be accessible at query-time
80
- def initialize(query:, schema: query.schema, values:, object:)
49
+ def initialize(query:, schema: query.schema, values:)
81
50
  @query = query
82
51
  @schema = schema
83
52
  @provided_values = values || {}
84
- @object = object
85
53
  # Namespaced storage, where user-provided values are in `nil` namespace:
86
54
  @storage = Hash.new { |h, k| h[k] = {} }
87
55
  @storage[nil] = @provided_values
@@ -140,6 +108,35 @@ module GraphQL
140
108
  end
141
109
  end
142
110
 
111
+ # Return this value to tell the runtime
112
+ # to exclude this field from the response altogether
113
+ def skip
114
+ GraphQL::Execution::SKIP
115
+ end
116
+
117
+ # Add error at query-level.
118
+ # @param error [GraphQL::ExecutionError] an execution error
119
+ # @return [void]
120
+ def add_error(error)
121
+ if !error.is_a?(ExecutionError)
122
+ raise TypeError, "expected error to be a ExecutionError, but was #{error.class}"
123
+ end
124
+ errors << error
125
+ nil
126
+ end
127
+
128
+ # @example Print the GraphQL backtrace during field resolution
129
+ # puts ctx.backtrace
130
+ #
131
+ # @return [GraphQL::Backtrace] The backtrace for this point in query execution
132
+ def backtrace
133
+ GraphQL::Backtrace.new(self)
134
+ end
135
+
136
+ def execution_errors
137
+ @execution_errors ||= ExecutionErrors.new(self)
138
+ end
139
+
143
140
  def current_path
144
141
  current_runtime_state = Thread.current[:__graphql_runtime_info]
145
142
  query_runtime_state = current_runtime_state && current_runtime_state[@query]
@@ -14,7 +14,7 @@ module GraphQL
14
14
  #
15
15
  # @api private
16
16
  class ValidationPipeline
17
- attr_reader :max_depth, :max_complexity
17
+ attr_reader :max_depth, :max_complexity, :validate_timeout_remaining
18
18
 
19
19
  def initialize(query:, parse_error:, operation_name_error:, max_depth:, max_complexity:)
20
20
  @validation_errors = []
@@ -71,7 +71,7 @@ module GraphQL
71
71
  validator = @query.static_validator || @schema.static_validator
72
72
  validation_result = validator.validate(@query, validate: @query.validate, timeout: @schema.validate_timeout, max_errors: @schema.validate_max_errors)
73
73
  @validation_errors.concat(validation_result[:errors])
74
-
74
+ @validate_timeout_remaining = validation_result[:remaining_timeout]
75
75
  if @validation_errors.empty?
76
76
  @validation_errors.concat(@query.variables.errors)
77
77
  end
@@ -26,7 +26,7 @@ module GraphQL
26
26
  # - Then, fall back to the default value from the query string
27
27
  # If it's still nil, raise an error if it's required.
28
28
  variable_type = schema.type_from_ast(ast_variable.type, context: ctx)
29
- if variable_type.nil?
29
+ if variable_type.nil? || !variable_type.unwrap.kind.input?
30
30
  # Pass -- it will get handled by a validator
31
31
  else
32
32
  variable_name = ast_variable.name
@@ -80,12 +80,12 @@ module GraphQL
80
80
  else
81
81
  val
82
82
  end
83
- end
83
+ end
84
84
 
85
85
  def add_max_errors_reached_message
86
86
  message = "Too many errors processing variables, max validation error limit reached. Execution aborted"
87
87
  validation_result = GraphQL::Query::InputValidationResult.from_problem(message)
88
- errors << GraphQL::Query::VariableValidationError.new(nil, nil, nil, validation_result, msg: message)
88
+ errors << GraphQL::Query::VariableValidationError.new(nil, nil, nil, validation_result, msg: message)
89
89
  end
90
90
  end
91
91
  end
data/lib/graphql/query.rb CHANGED
@@ -99,7 +99,7 @@ module GraphQL
99
99
  # Even if `variables: nil` is passed, use an empty hash for simpler logic
100
100
  variables ||= {}
101
101
  @schema = schema
102
- @context = schema.context_class.new(query: self, object: root_value, values: context)
102
+ @context = schema.context_class.new(query: self, values: context)
103
103
  @warden = warden
104
104
  @subscription_topic = subscription_topic
105
105
  @root_value = root_value
@@ -317,7 +317,7 @@ module GraphQL
317
317
  end
318
318
 
319
319
  def_delegators :validation_pipeline, :validation_errors,
320
- :analyzers, :ast_analyzers, :max_depth, :max_complexity
320
+ :analyzers, :ast_analyzers, :max_depth, :max_complexity, :validate_timeout_remaining
321
321
 
322
322
  attr_accessor :analysis_errors
323
323
  def valid?
@@ -395,7 +395,7 @@ module GraphQL
395
395
  parse_error = nil
396
396
  @document ||= begin
397
397
  if query_string
398
- GraphQL.parse(query_string, trace: self.current_trace)
398
+ GraphQL.parse(query_string, trace: self.current_trace, max_tokens: @schema.max_query_string_tokens)
399
399
  end
400
400
  rescue GraphQL::ParseError => err
401
401
  parse_error = err
@@ -290,6 +290,7 @@ module GraphQL
290
290
  # TODO code smell to access such a deeply-nested constant in a distant module
291
291
  argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
292
292
  value: resolved_loaded_value,
293
+ original_value: resolved_coerced_value,
293
294
  definition: self,
294
295
  default_used: default_used,
295
296
  )
@@ -1,18 +1,16 @@
1
1
  # frozen_string_literal: true
2
-
3
- require 'graphql/schema/base_64_bp'
4
-
2
+ require "base64"
5
3
  module GraphQL
6
4
  class Schema
7
5
  # @api private
8
6
  module Base64Encoder
9
7
  def self.encode(unencoded_text, nonce: false)
10
- Base64Bp.urlsafe_encode64(unencoded_text, padding: false)
8
+ Base64.urlsafe_encode64(unencoded_text, padding: false)
11
9
  end
12
10
 
13
11
  def self.decode(encoded_text, nonce: false)
14
12
  # urlsafe_decode64 is for forward compatibility
15
- Base64Bp.urlsafe_decode64(encoded_text)
13
+ Base64.urlsafe_decode64(encoded_text)
16
14
  rescue ArgumentError
17
15
  raise GraphQL::ExecutionError, "Invalid input: #{encoded_text.inspect}"
18
16
  end
@@ -7,10 +7,16 @@ module GraphQL
7
7
  class << self
8
8
  # @see {Schema.from_definition}
9
9
  def from_definition(schema_superclass, definition_string, parser: GraphQL.default_parser, **kwargs)
10
+ if defined?(parser::SchemaParser)
11
+ parser = parser::SchemaParser
12
+ end
10
13
  from_document(schema_superclass, parser.parse(definition_string), **kwargs)
11
14
  end
12
15
 
13
16
  def from_definition_path(schema_superclass, definition_path, parser: GraphQL.default_parser, **kwargs)
17
+ if defined?(parser::SchemaParser)
18
+ parser = parser::SchemaParser
19
+ end
14
20
  from_document(schema_superclass, parser.parse_file(definition_path), **kwargs)
15
21
  end
16
22
 
@@ -120,10 +126,12 @@ module GraphQL
120
126
 
121
127
  builder = self
122
128
 
129
+ found_types = types.values
123
130
  schema_class = Class.new(schema_superclass) do
124
131
  begin
125
132
  # Add these first so that there's some chance of resolving late-bound types
126
- orphan_types types.values
133
+ add_type_and_traverse(found_types, root: false)
134
+ orphan_types(found_types.select { |t| t.respond_to?(:kind) && t.kind.object? })
127
135
  query query_root_type
128
136
  mutation mutation_root_type
129
137
  subscription subscription_root_type
@@ -471,6 +471,8 @@ module GraphQL
471
471
  if arguments[:last] && (max_possible_page_size.nil? || arguments[:last] > max_possible_page_size)
472
472
  max_possible_page_size = arguments[:last]
473
473
  end
474
+ elsif arguments.is_a?(GraphQL::UnauthorizedError)
475
+ raise arguments
474
476
  end
475
477
 
476
478
  if max_possible_page_size.nil?
@@ -483,41 +485,24 @@ module GraphQL
483
485
  metadata_complexity = 0
484
486
  lookahead = GraphQL::Execution::Lookahead.new(query: query, field: self, ast_nodes: nodes, owner_type: owner)
485
487
 
486
- if (page_info_lookahead = lookahead.selection(:page_info)).selected?
487
- metadata_complexity += 1 # pageInfo
488
- metadata_complexity += page_info_lookahead.selections.size # subfields
489
- end
490
-
491
- if lookahead.selects?(:total) || lookahead.selects?(:total_count) || lookahead.selects?(:count)
492
- metadata_complexity += 1
488
+ lookahead.selections.each do |next_lookahead|
489
+ # this includes `pageInfo`, `nodes` and `edges` and any custom fields
490
+ # TODO this doesn't support procs yet -- unlikely to need it.
491
+ metadata_complexity += next_lookahead.field.complexity
492
+ if next_lookahead.name != :nodes && next_lookahead.name != :edges
493
+ # subfields, eg, for pageInfo -- assumes no subselections
494
+ metadata_complexity += next_lookahead.selections.size
495
+ end
493
496
  end
494
497
 
495
- nodes_edges_complexity = 0
496
- nodes_edges_complexity += 1 if lookahead.selects?(:edges)
497
- nodes_edges_complexity += 1 if lookahead.selects?(:nodes)
498
-
499
498
  # Possible bug: selections on `edges` and `nodes` are _both_ multiplied here. Should they be?
500
- items_complexity = child_complexity - metadata_complexity - nodes_edges_complexity
501
- # Add 1 for _this_ field
502
- 1 + (max_possible_page_size * items_complexity) + metadata_complexity + nodes_edges_complexity
499
+ items_complexity = child_complexity - metadata_complexity
500
+ subfields_complexity = (max_possible_page_size * items_complexity) + metadata_complexity
501
+ # Apply this field's own complexity
502
+ apply_own_complexity_to(subfields_complexity, query, nodes)
503
503
  end
504
504
  else
505
- defined_complexity = complexity
506
- case defined_complexity
507
- when Proc
508
- arguments = query.arguments_for(nodes.first, self)
509
- if arguments.is_a?(GraphQL::ExecutionError)
510
- return child_complexity
511
- elsif arguments.respond_to?(:keyword_arguments)
512
- arguments = arguments.keyword_arguments
513
- end
514
-
515
- defined_complexity.call(query.context, arguments, child_complexity)
516
- when Numeric
517
- defined_complexity + child_complexity
518
- else
519
- raise("Invalid complexity: #{defined_complexity.inspect} on #{path} (#{inspect})")
520
- end
505
+ apply_own_complexity_to(child_complexity, query, nodes)
521
506
  end
522
507
  end
523
508
 
@@ -882,6 +867,24 @@ ERR
882
867
  yield(obj, args)
883
868
  end
884
869
  end
870
+
871
+ def apply_own_complexity_to(child_complexity, query, nodes)
872
+ case (own_complexity = complexity)
873
+ when Numeric
874
+ own_complexity + child_complexity
875
+ when Proc
876
+ arguments = query.arguments_for(nodes.first, self)
877
+ if arguments.is_a?(GraphQL::ExecutionError)
878
+ return child_complexity
879
+ elsif arguments.respond_to?(:keyword_arguments)
880
+ arguments = arguments.keyword_arguments
881
+ end
882
+
883
+ own_complexity.call(query.context, arguments, child_complexity)
884
+ else
885
+ raise ArgumentError, "Invalid complexity for #{self.path}: #{own_complexity.inspect}"
886
+ end
887
+ end
885
888
  end
886
889
  end
887
890
  end