graphql 2.5.11 → 2.6.10

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 (154) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/field_extractor.rb +17 -1
  4. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  5. data/lib/generators/graphql/templates/schema.erb +2 -1
  6. data/lib/graphql/analysis/query_complexity.rb +31 -15
  7. data/lib/graphql/analysis.rb +20 -13
  8. data/lib/graphql/backtrace/table.rb +10 -1
  9. data/lib/graphql/current.rb +7 -1
  10. data/lib/graphql/dashboard/application_controller.rb +45 -0
  11. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  12. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  13. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  15. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  16. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  17. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  18. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  19. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  20. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  21. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  22. data/lib/graphql/dashboard.rb +14 -73
  23. data/lib/graphql/dataloader/async_dataloader.rb +336 -60
  24. data/lib/graphql/dataloader/null_dataloader.rb +48 -10
  25. data/lib/graphql/dataloader/source.rb +32 -19
  26. data/lib/graphql/dataloader.rb +107 -39
  27. data/lib/graphql/date_encoding_error.rb +1 -1
  28. data/lib/graphql/execution/directive_checks.rb +2 -0
  29. data/lib/graphql/execution/field_resolve_step.rb +804 -0
  30. data/lib/graphql/execution/finalize.rb +229 -0
  31. data/lib/graphql/execution/input_values.rb +333 -0
  32. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -0
  33. data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
  34. data/lib/graphql/execution/interpreter/resolve.rb +7 -13
  35. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  36. data/lib/graphql/execution/interpreter/runtime.rb +60 -40
  37. data/lib/graphql/execution/interpreter.rb +17 -23
  38. data/lib/graphql/execution/lazy.rb +1 -1
  39. data/lib/graphql/execution/load_argument_step.rb +102 -0
  40. data/lib/graphql/execution/multiplex.rb +1 -1
  41. data/lib/graphql/execution/next.rb +98 -0
  42. data/lib/graphql/execution/prepare_object_step.rb +151 -0
  43. data/lib/graphql/execution/resolve_type_step.rb +27 -0
  44. data/lib/graphql/execution/runner.rb +457 -0
  45. data/lib/graphql/execution/selections_step.rb +95 -0
  46. data/lib/graphql/execution.rb +15 -4
  47. data/lib/graphql/execution_error.rb +17 -12
  48. data/lib/graphql/float_decoding_error.rb +13 -0
  49. data/lib/graphql/float_encoding_error.rb +28 -0
  50. data/lib/graphql/introspection/directive_type.rb +7 -3
  51. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  52. data/lib/graphql/introspection/entry_points.rb +11 -3
  53. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  54. data/lib/graphql/introspection/field_type.rb +13 -5
  55. data/lib/graphql/introspection/input_value_type.rb +21 -13
  56. data/lib/graphql/introspection/schema_type.rb +6 -2
  57. data/lib/graphql/introspection/type_type.rb +64 -28
  58. data/lib/graphql/invalid_null_error.rb +11 -5
  59. data/lib/graphql/language/block_string.rb +6 -11
  60. data/lib/graphql/language/cache.rb +84 -14
  61. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  62. data/lib/graphql/language/lexer.rb +27 -17
  63. data/lib/graphql/language/nodes.rb +2 -1
  64. data/lib/graphql/language/parser.rb +1 -1
  65. data/lib/graphql/language.rb +32 -17
  66. data/lib/graphql/pagination/connection.rb +2 -0
  67. data/lib/graphql/pagination/connections.rb +30 -0
  68. data/lib/graphql/pagination/relation_connection.rb +10 -1
  69. data/lib/graphql/query/context.rb +10 -3
  70. data/lib/graphql/query/null_context.rb +9 -3
  71. data/lib/graphql/query/partial.rb +18 -3
  72. data/lib/graphql/query/variable_validation_error.rb +16 -1
  73. data/lib/graphql/query/variables.rb +1 -1
  74. data/lib/graphql/query.rb +12 -3
  75. data/lib/graphql/railtie.rb +2 -1
  76. data/lib/graphql/runtime_error.rb +6 -0
  77. data/lib/graphql/schema/argument.rb +13 -1
  78. data/lib/graphql/schema/build_from_definition.rb +28 -1
  79. data/lib/graphql/schema/directive/feature.rb +4 -0
  80. data/lib/graphql/schema/directive/transform.rb +20 -0
  81. data/lib/graphql/schema/directive.rb +48 -13
  82. data/lib/graphql/schema/field/connection_extension.rb +4 -37
  83. data/lib/graphql/schema/field/scope_extension.rb +18 -13
  84. data/lib/graphql/schema/field.rb +79 -48
  85. data/lib/graphql/schema/field_extension.rb +11 -8
  86. data/lib/graphql/schema/has_single_input_argument.rb +24 -13
  87. data/lib/graphql/schema/input_object.rb +6 -2
  88. data/lib/graphql/schema/interface.rb +26 -0
  89. data/lib/graphql/schema/introspection_system.rb +6 -21
  90. data/lib/graphql/schema/list.rb +5 -1
  91. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -11
  92. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  93. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  94. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  95. data/lib/graphql/schema/member/has_fields.rb +86 -5
  96. data/lib/graphql/schema/member.rb +5 -0
  97. data/lib/graphql/schema/non_null.rb +1 -1
  98. data/lib/graphql/schema/object.rb +1 -0
  99. data/lib/graphql/schema/printer.rb +1 -1
  100. data/lib/graphql/schema/ractor_shareable.rb +1 -0
  101. data/lib/graphql/schema/relay_classic_mutation.rb +16 -2
  102. data/lib/graphql/schema/resolver.rb +76 -1
  103. data/lib/graphql/schema/subscription.rb +53 -10
  104. data/lib/graphql/schema/timeout.rb +2 -2
  105. data/lib/graphql/schema/validator/allow_blank_validator.rb +3 -3
  106. data/lib/graphql/schema/validator/allow_null_validator.rb +3 -3
  107. data/lib/graphql/schema/validator/exclusion_validator.rb +2 -2
  108. data/lib/graphql/schema/validator/format_validator.rb +3 -3
  109. data/lib/graphql/schema/validator/inclusion_validator.rb +2 -2
  110. data/lib/graphql/schema/validator/length_validator.rb +6 -6
  111. data/lib/graphql/schema/validator/numericality_validator.rb +19 -19
  112. data/lib/graphql/schema/validator/required_validator.rb +38 -5
  113. data/lib/graphql/schema/validator.rb +9 -0
  114. data/lib/graphql/schema/visibility/profile.rb +74 -53
  115. data/lib/graphql/schema/visibility/visit.rb +1 -1
  116. data/lib/graphql/schema/visibility.rb +32 -24
  117. data/lib/graphql/schema/wrapper.rb +11 -1
  118. data/lib/graphql/schema.rb +93 -27
  119. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  120. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  121. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  122. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  123. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  124. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  125. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  126. data/lib/graphql/static_validation/rules/fields_will_merge.rb +351 -248
  127. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  128. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  129. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  130. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  131. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  132. data/lib/graphql/static_validation/validation_context.rb +1 -1
  133. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  134. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +40 -10
  135. data/lib/graphql/subscriptions/serialize.rb +10 -4
  136. data/lib/graphql/subscriptions.rb +36 -1
  137. data/lib/graphql/testing/helpers.rb +12 -9
  138. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  139. data/lib/graphql/testing.rb +1 -0
  140. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  141. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  142. data/lib/graphql/tracing/perfetto_trace.rb +213 -81
  143. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  144. data/lib/graphql/tracing/trace.rb +6 -0
  145. data/lib/graphql/types/float.rb +18 -4
  146. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  147. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  148. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  149. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  150. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  151. data/lib/graphql/unauthorized_error.rb +9 -1
  152. data/lib/graphql/version.rb +1 -1
  153. data/lib/graphql.rb +8 -2
  154. metadata +23 -59
@@ -21,17 +21,12 @@ module GraphQL
21
21
  next
22
22
  end
23
23
  line_length = line.size
24
- line_indent = if line.match?(/\A [^ ]/)
25
- 2
26
- elsif line.match?(/\A [^ ]/)
27
- 4
28
- elsif line.match?(/\A[^ ]/)
29
- 0
30
- else
31
- line[/\A */].size
24
+ leading = 0
25
+ while leading < line_length && line.getbyte(leading) == 32
26
+ leading += 1
32
27
  end
33
- if line_indent < line_length && (common_indent.nil? || line_indent < common_indent)
34
- common_indent = line_indent
28
+ if leading < line_length && (common_indent.nil? || leading < common_indent)
29
+ common_indent = leading
35
30
  end
36
31
  end
37
32
 
@@ -41,7 +36,7 @@ module GraphQL
41
36
  if idx == 0
42
37
  next
43
38
  else
44
- line.slice!(0, common_indent)
39
+ line[0, common_indent] = ""
45
40
  end
46
41
  end
47
42
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'graphql/version'
4
4
  require 'digest/sha2'
5
+ require 'openssl'
6
+ require 'securerandom'
7
+ require 'tempfile'
5
8
 
6
9
  module GraphQL
7
10
  module Language
@@ -9,7 +12,11 @@ module GraphQL
9
12
  #
10
13
  # With Rails, parser caching may enabled by setting `config.graphql.parser_cache = true` in your Rails application.
11
14
  #
12
- # The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new("some_dir")`.
15
+ # The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new(Pathname.new("some_dir"), secret: ENV.fetch("GRAPHQL_CACHE_SECRET"))`.
16
+ # The `secret` should be a stable value stored outside of the cache directory.
17
+ # When it isn't provided, a process-local secret is generated and cache entries
18
+ # are rebuilt after the process restarts. Pass `secret: nil` to disable cache
19
+ # signing. This should only be used when the cache directory is trusted.
13
20
  # This will create a directory (`tmp/cache/graphql` by default) that stores a cache of parsed files.
14
21
  #
15
22
  # Much like [bootsnap](https://github.com/Shopify/bootsnap), the parser cache needs to be cleaned up manually.
@@ -18,32 +25,95 @@ module GraphQL
18
25
  #
19
26
  # @see GraphQL::Railtie for simple Rails integration
20
27
  class Cache
21
- def initialize(path)
28
+ # @param path [Pathname] The directory where cache entries are stored.
29
+ # @param secret [String, nil] A stable secret for verifying cache entries. When omitted,
30
+ # a process-local secret is generated. Pass `nil` to disable cache signing.
31
+ def initialize(path, secret: SecureRandom.random_bytes(32))
22
32
  @path = path
33
+ @secret = secret
23
34
  end
24
35
 
25
36
  DIGEST = Digest::SHA256.new << GraphQL::VERSION
37
+ HMAC_SIZE = OpenSSL::Digest::SHA256.new.digest_length
38
+ InvalidCache = Class.new(StandardError)
39
+ private_constant :InvalidCache
26
40
 
27
41
  def fetch(filename)
28
- hash = DIGEST.dup << filename
42
+ cache_key = cache_key_for(filename)
43
+ return yield unless cache_key
44
+
45
+ cache_path = @path.join(cache_key)
46
+
47
+ begin
48
+ return load_cache(cache_path, cache_key) if cache_path.file?
49
+ rescue InvalidCache, SystemCallError
50
+ # Rebuild caches created by older versions or with an invalid signature.
51
+ end
52
+
53
+ payload = yield
29
54
  begin
30
- hash << File.mtime(filename).to_i.to_s
55
+ write_cache(cache_path, cache_key, payload)
31
56
  rescue SystemCallError
32
- return yield
57
+ # Parser caching is best-effort; return the parsed payload if the cache cannot be written.
33
58
  end
34
- cache_path = @path.join(hash.to_s)
59
+ payload
60
+ end
61
+
62
+ private
63
+
64
+ def cache_key_for(filename)
65
+ content_digest = Digest::SHA256.file(filename).hexdigest
66
+ (DIGEST.dup << filename << content_digest).to_s
67
+ rescue SystemCallError
68
+ nil
69
+ end
70
+
71
+ def load_cache(cache_path, cache_key)
72
+ cache_data = cache_path.binread
73
+ return Marshal.load(cache_data) unless @secret
74
+
75
+ signature = cache_data.byteslice(0, HMAC_SIZE)
76
+ payload = cache_data.byteslice(HMAC_SIZE..-1)
77
+ raise InvalidCache unless signature && payload
35
78
 
36
- if cache_path.exist?
37
- Marshal.load(cache_path.read)
79
+ expected_signature = signature_for(cache_key, payload)
80
+ unless secure_compare(signature, expected_signature)
81
+ raise InvalidCache
82
+ end
83
+ Marshal.load(payload)
84
+ end
85
+
86
+ def write_cache(cache_path, cache_key, payload)
87
+ @path.mkpath
88
+ serialized_payload = Marshal.dump(payload)
89
+ cache_data = if @secret
90
+ signature_for(cache_key, serialized_payload) + serialized_payload
38
91
  else
39
- payload = yield
40
- tmp_path = "#{cache_path}.#{rand}"
92
+ serialized_payload
93
+ end
94
+
95
+ Tempfile.create(['graphql-cache-', '.tmp'], @path.to_s) do |tempfile|
96
+ tempfile.binmode
97
+ tempfile.write(cache_data)
98
+ tempfile.flush
99
+ tempfile.fsync
100
+ tempfile.close
101
+ File.rename(tempfile.path, cache_path.to_s)
102
+ end
103
+ end
104
+
105
+ def signature_for(cache_key, payload)
106
+ OpenSSL::HMAC.digest('SHA256', @secret, cache_key + payload)
107
+ end
108
+
109
+ def secure_compare(left, right)
110
+ return false unless left.bytesize == right.bytesize
41
111
 
42
- @path.mkpath
43
- File.binwrite(tmp_path, Marshal.dump(payload))
44
- File.rename(tmp_path, cache_path.to_s)
45
- payload
112
+ result = 0
113
+ left.bytes.each_with_index do |byte, index|
114
+ result |= byte ^ right.getbyte(index)
46
115
  end
116
+ result.zero?
47
117
  end
48
118
  end
49
119
  end
@@ -52,8 +52,9 @@ module GraphQL
52
52
 
53
53
  def build_object_type_node(object_type)
54
54
  ints = @types.interfaces(object_type)
55
+
55
56
  if !ints.empty?
56
- ints.sort_by!(&:graphql_name)
57
+ ints = ints.sort_by(&:graphql_name)
57
58
  ints.map! { |iface| build_type_name_node(iface) }
58
59
  end
59
60
 
@@ -9,6 +9,9 @@ module GraphQL
9
9
  end
10
10
  @string = graphql_str
11
11
  @filename = filename
12
+ if !@string.valid_encoding?
13
+ raise_parse_error("Parse error on bad Unicode escape sequence", nil, nil)
14
+ end
12
15
  @scanner = StringScanner.new(graphql_str)
13
16
  @pos = nil
14
17
  @max_tokens = max_tokens || Float::INFINITY
@@ -28,7 +31,15 @@ module GraphQL
28
31
  attr_reader :pos, :tokens_count
29
32
 
30
33
  def advance
31
- @scanner.skip(IGNORE_REGEXP)
34
+ loop do
35
+ @scanner.skip(IGNORE_REGEXP)
36
+ if @scanner.skip(COMMENT_REGEXP)
37
+ @tokens_count += 1
38
+ next
39
+ end
40
+ break
41
+ end
42
+
32
43
  if @scanner.eos?
33
44
  @finished = true
34
45
  return false
@@ -102,10 +113,6 @@ module GraphQL
102
113
  @scanner.pos += 1
103
114
  :UNKNOWN_CHAR
104
115
  end
105
- rescue ArgumentError => err
106
- if err.message == "invalid byte sequence in UTF-8"
107
- raise_parse_error("Parse error on bad Unicode escape sequence", nil, nil)
108
- end
109
116
  end
110
117
 
111
118
  def token_value
@@ -122,7 +129,7 @@ module GraphQL
122
129
  elsif token_name == :STRING
123
130
  string_value
124
131
  elsif @scanner.matched_size.nil?
125
- @scanner.peek(1)
132
+ @string.byteslice(@scanner.pos - 1, 1)
126
133
  else
127
134
  token_value
128
135
  end
@@ -139,7 +146,7 @@ module GraphQL
139
146
  "\\r" => "\r",
140
147
  "\\t" => "\t",
141
148
  }
142
- UTF_8 = /\\u(?:([\dAa-f]{4})|\{([\da-f]{4,})\})(?:\\u([\dAa-f]{4}))?/i
149
+ UTF_8 = /\\u(?:([\da-f]{4})|\{([\da-f]+)\})(?:\\u([\da-f]{4}))?/i
143
150
  VALID_STRING = /\A(?:[^\\]|#{ESCAPES}|#{UTF_8})*\z/o
144
151
  ESCAPED = /(?:#{ESCAPES}|#{UTF_8})/o
145
152
 
@@ -155,7 +162,11 @@ module GraphQL
155
162
  if !str.valid_encoding? || !str.match?(VALID_STRING)
156
163
  raise_parse_error("Bad unicode escape in #{str.inspect}")
157
164
  else
158
- Lexer.replace_escaped_characters_in_place(str)
165
+ begin
166
+ Lexer.replace_escaped_characters_in_place(str)
167
+ rescue RangeError
168
+ raise_parse_error("Bad unicode escape in #{str.inspect}")
169
+ end
159
170
 
160
171
  if !str.valid_encoding?
161
172
  raise_parse_error("Bad unicode escape in #{str.inspect}")
@@ -167,23 +178,22 @@ module GraphQL
167
178
  end
168
179
 
169
180
  def line_number
170
- @scanner.string[0..@pos].count("\n") + 1
181
+ @scanner.string.byteslice(0, @pos).b.count("\n".b) + 1
171
182
  end
172
183
 
173
184
  def column_number
174
- @scanner.string[0..@pos].split("\n").last.length
185
+ line_prefix = @scanner.string.byteslice(0, @pos)
186
+ line_prefix = line_prefix.b unless line_prefix.valid_encoding?
187
+ newline_index = line_prefix.rindex("\n")
188
+ newline_index ? line_prefix.length - newline_index : line_prefix.length + 1
175
189
  end
176
190
 
177
191
  def raise_parse_error(message, line = line_number, col = column_number)
178
192
  raise GraphQL::ParseError.new(message, line, col, @string, filename: @filename)
179
193
  end
180
194
 
181
- IGNORE_REGEXP = %r{
182
- (?:
183
- [, \c\r\n\t]+ |
184
- \#.*$
185
- )*
186
- }x
195
+ IGNORE_REGEXP = /[, \c\r\n\t]+/
196
+ COMMENT_REGEXP = /\#[^\n]*/
187
197
  IDENTIFIER_REGEXP = /[_A-Za-z][_0-9A-Za-z]*/
188
198
  INT_REGEXP = /-?(?:[0]|[1-9][0-9]*)/
189
199
  FLOAT_DECIMAL_REGEXP = /[.][0-9]+/
@@ -282,7 +292,7 @@ module GraphQL
282
292
  QUOTE = '"'
283
293
  UNICODE_DIGIT = /[0-9A-Za-z]/
284
294
  FOUR_DIGIT_UNICODE = /#{UNICODE_DIGIT}{4}/
285
- N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}{4,}#{Punctuation::RCURLY}}x
295
+ N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}+#{Punctuation::RCURLY}}x
286
296
  UNICODE_ESCAPE = %r{\\u(?:#{FOUR_DIGIT_UNICODE}|#{N_DIGIT_UNICODE})}
287
297
  STRING_ESCAPE = %r{[\\][\\/bfnrt]}
288
298
  BLOCK_QUOTE = '"""'
@@ -304,7 +304,8 @@ module GraphQL
304
304
  children_method_names.map { |m| "#{m}: NO_CHILDREN" } +
305
305
  DEFAULT_INITIALIZE_OPTIONS
306
306
 
307
- assignments = scalar_method_names.map { |m| "@#{m} = #{m}"} +
307
+ # Intern descriptions so identical strings across SDL documents share one frozen object.
308
+ assignments = scalar_method_names.map { |m| m == :description ? "@#{m} = #{m} && -#{m}" : "@#{m} = #{m}" } +
308
309
  children_method_names.map { |m| "@#{m} = #{m}.freeze" }
309
310
 
310
311
  if name.end_with?("Definition") && name != "FragmentDefinition"
@@ -806,7 +806,7 @@ module GraphQL
806
806
 
807
807
  def expect_token(expected_token_name)
808
808
  unless @token_name == expected_token_name
809
- raise_parse_error("Expected #{expected_token_name}, actual: #{token_name || "(none)"} (#{debug_token_value.inspect})")
809
+ raise_parse_error("Expected #{expected_token_name}, #{@token_name == false ? "not end of file" : "actual: #{@token_name} (#{debug_token_value.inspect})"}")
810
810
  end
811
811
  advance_token
812
812
  end
@@ -31,11 +31,11 @@ module GraphQL
31
31
 
32
32
  "[#{serialized_array}]"
33
33
  else
34
- JSON.generate(value, quirks_mode: true)
34
+ JSON.generate(value)
35
35
  end
36
36
  rescue JSON::GeneratorError
37
- if Float::INFINITY == value
38
- "Infinity"
37
+ if value.is_a?(Float) && !value.finite?
38
+ value.to_s
39
39
  else
40
40
  raise
41
41
  end
@@ -47,13 +47,19 @@ module GraphQL
47
47
  def self.escape_single_quoted_newlines(query_str)
48
48
  scanner = StringScanner.new(query_str)
49
49
  inside_single_quoted_string = false
50
+ inside_triple_quoted_string = false
50
51
  new_query_str = nil
51
52
  while !scanner.eos?
52
- if scanner.skip(/(?:\\"|[^"\n\r]|""")+/m)
53
+ if scanner.skip('"""')
54
+ inside_triple_quoted_string = !inside_triple_quoted_string
55
+ new_query_str && (new_query_str << scanner.matched)
56
+ elsif scanner.skip(/(?:\\"|[^"\n\r])+/m)
53
57
  new_query_str && (new_query_str << scanner.matched)
54
58
  elsif scanner.skip('"')
55
59
  new_query_str && (new_query_str << '"')
56
- inside_single_quoted_string = !inside_single_quoted_string
60
+ if !inside_triple_quoted_string
61
+ inside_single_quoted_string = !inside_single_quoted_string
62
+ end
57
63
  elsif scanner.skip("\n")
58
64
  if inside_single_quoted_string
59
65
  new_query_str ||= query_str[0, scanner.pos - 1]
@@ -77,21 +83,30 @@ module GraphQL
77
83
  new_query_str || query_str
78
84
  end
79
85
 
86
+ LEADING_REGEX = Regexp.union(" ", *Lexer::Punctuation.constants.map { |const| Lexer::Punctuation.const_get(const) })
87
+
88
+ # Optimized pattern using:
89
+ # - Possessive quantifiers (*+, ++) to prevent backtracking in number patterns
90
+ # - Atomic group (?>...) for IGNORE to prevent backtracking
91
+ # - Single unified number pattern instead of three alternatives
92
+ EFFICIENT_NUMBER_REGEXP = /-?(?:0|[1-9][0-9]*+)(?:\.[0-9]++)?(?:[eE][+-]?[0-9]++)?/
93
+ EFFICIENT_IGNORE_REGEXP = /(?>[, \r\n\t]+|\#[^\n]*$)*/
94
+
95
+ MAYBE_INVALID_NUMBER = /\d[_a-zA-Z]/
96
+
80
97
  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
98
+ (?<leading>#{LEADING_REGEX})
99
+ (?<num>#{EFFICIENT_NUMBER_REGEXP})
100
+ (?<name>#{Lexer::IDENTIFIER_REGEXP})
101
+ #{EFFICIENT_IGNORE_REGEXP}
102
+ :
103
+ }x
88
104
 
89
105
  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
106
+ # Fast check for digit followed by identifier char. If this doesn't match, skip the more expensive regexp entirely.
107
+ return query_str unless query_str.match?(MAYBE_INVALID_NUMBER)
108
+ return query_str unless query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP)
109
+ query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k<leading>\\k<num> \\k<name>:")
95
110
  end
96
111
  end
97
112
  end
@@ -94,6 +94,8 @@ module GraphQL
94
94
  @was_authorized_by_scope_items = detect_was_authorized_by_scope_items
95
95
  end
96
96
 
97
+ attr_writer :was_authorized_by_scope_items
98
+
97
99
  def was_authorized_by_scope_items?
98
100
  @was_authorized_by_scope_items
99
101
  end
@@ -83,6 +83,36 @@ module GraphQL
83
83
  end
84
84
  end
85
85
 
86
+ def populate_connection(field, object, value, original_arguments, context)
87
+ if value.is_a? GraphQL::ExecutionError
88
+ raise value
89
+ elsif value.nil?
90
+ nil
91
+ elsif value.is_a?(GraphQL::Pagination::Connection)
92
+ # update the connection with some things that may not have been provided
93
+ value.context ||= context
94
+ value.parent ||= object
95
+ value.first_value ||= original_arguments[:first]
96
+ value.after_value ||= original_arguments[:after]
97
+ value.last_value ||= original_arguments[:last]
98
+ value.before_value ||= original_arguments[:before]
99
+ value.arguments ||= original_arguments # rubocop:disable Development/ContextIsPassedCop -- unrelated .arguments method
100
+ value.field ||= field
101
+ if field.has_max_page_size? && !value.has_max_page_size_override?
102
+ value.max_page_size = field.max_page_size
103
+ end
104
+ if field.has_default_page_size? && !value.has_default_page_size_override?
105
+ value.default_page_size = field.default_page_size
106
+ end
107
+ if (custom_t = context.schema.connections.edge_class_for_field(field))
108
+ value.edge_class = custom_t
109
+ end
110
+ value
111
+ else
112
+ context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers
113
+ context.schema.connections.wrap(field, object, value, original_arguments, context)
114
+ end
115
+ end
86
116
  # use an override if there is one
87
117
  # @api private
88
118
  def edge_class_for_field(field)
@@ -221,7 +221,16 @@ module GraphQL
221
221
  # returns an array of nodes
222
222
  def load_nodes
223
223
  # Return an array so we can consistently use `.index(node)` on it
224
- @nodes ||= limited_nodes.to_a
224
+ return @nodes if @nodes
225
+ if (@context[:dataloader].is_a?(GraphQL::Dataloader::AsyncDataloader))
226
+ # `AsyncDataloader` may resolve sibling fields (eg, `edges` and `pageInfo`)
227
+ # in separate Fibers, so several callers can get here before `@nodes` is set.
228
+ (@load_lock ||= Mutex.new).synchronize do
229
+ @nodes ||= limited_nodes.to_a
230
+ end
231
+ else
232
+ @nodes = limited_nodes.to_a
233
+ end
225
234
  end
226
235
  end
227
236
  end
@@ -29,6 +29,7 @@ module GraphQL
29
29
  end
30
30
 
31
31
  extend Forwardable
32
+ include Schema::Member::HasDataloader
32
33
 
33
34
  # @return [Array<GraphQL::ExecutionError>] errors returned during execution
34
35
  attr_reader :errors
@@ -111,20 +112,26 @@ module GraphQL
111
112
  # Return this value to tell the runtime
112
113
  # to exclude this field from the response altogether
113
114
  def skip
114
- GraphQL::Execution::SKIP
115
+ GraphQL::Execution::Skip.new
115
116
  end
116
117
 
117
118
  # Add error at query-level.
118
119
  # @param error [GraphQL::ExecutionError] an execution error
119
120
  # @return [void]
120
121
  def add_error(error)
121
- if !error.is_a?(ExecutionError)
122
- raise TypeError, "expected error to be a ExecutionError, but was #{error.class}"
122
+ if !error.is_a?(GraphQL::RuntimeError)
123
+ raise TypeError, "expected error to be a GraphQL::RuntimeError, but was #{error.class}"
123
124
  end
124
125
  errors << error
125
126
  nil
126
127
  end
127
128
 
129
+ # @param value [Object] Any object to be inserted directly into the final response
130
+ # @return [GraphQL::Execution::Interpreter::RawValue] Return this from the field
131
+ def raw_value(value)
132
+ GraphQL::Execution::Interpreter::RawValue.new(value)
133
+ end
134
+
128
135
  # @example Print the GraphQL backtrace during field resolution
129
136
  # puts ctx.backtrace
130
137
  #
@@ -4,7 +4,13 @@ module GraphQL
4
4
  class Query
5
5
  # This object can be `ctx` in places where there is no query
6
6
  class NullContext < Context
7
- include Singleton
7
+ def self.instance
8
+ @instance ||= self.new
9
+ end
10
+
11
+ def self.instance=(new_inst)
12
+ @instance = new_inst
13
+ end
8
14
 
9
15
  class NullQuery
10
16
  def after_lazy(value)
@@ -20,10 +26,10 @@ module GraphQL
20
26
  attr_reader :schema, :query, :warden, :dataloader
21
27
  def_delegators GraphQL::EmptyObjects::EMPTY_HASH, :[], :fetch, :dig, :key?, :to_h
22
28
 
23
- def initialize
29
+ def initialize(schema: NullSchema)
24
30
  @query = NullQuery.new
25
31
  @dataloader = GraphQL::Dataloader::NullDataloader.new
26
- @schema = NullSchema
32
+ @schema = schema
27
33
  @warden = Schema::Warden::NullWarden.new(context: self, schema: @schema)
28
34
  @types = @warden.visibility_profile
29
35
  freeze
@@ -32,6 +32,7 @@ module GraphQL
32
32
  @multiplex = nil
33
33
  @result_values = nil
34
34
  @result = nil
35
+ @finalizers = @top_level_finalizers = nil
35
36
 
36
37
  if fragment_node
37
38
  @ast_nodes = [fragment_node]
@@ -51,6 +52,10 @@ module GraphQL
51
52
  @leaf
52
53
  end
53
54
 
55
+ def root_value
56
+ object
57
+ end
58
+
54
59
  attr_reader :context, :query, :ast_nodes, :root_type, :object, :field_definition, :path, :schema
55
60
 
56
61
  attr_accessor :multiplex, :result_values
@@ -90,10 +95,22 @@ module GraphQL
90
95
  @query.fragments
91
96
  end
92
97
 
98
+ def validate
99
+ @query.validate
100
+ end
101
+
93
102
  def valid?
94
103
  @query.valid?
95
104
  end
96
105
 
106
+ def query?
107
+ true
108
+ end
109
+
110
+ def run_partials(...)
111
+ @query.run_partials(...)
112
+ end
113
+
97
114
  def analyzers
98
115
  EmptyObjects::EMPTY_ARRAY
99
116
  end
@@ -107,7 +124,7 @@ module GraphQL
107
124
  end
108
125
 
109
126
  def selected_operation
110
- ast_nodes.first
127
+ Language::Nodes::OperationDefinition.new(selections: ast_nodes.flat_map(&:selections))
111
128
  end
112
129
 
113
130
  def static_errors
@@ -123,7 +140,6 @@ module GraphQL
123
140
  def set_type_info_from_path
124
141
  selections = [@query.selected_operation]
125
142
  type = @query.root_type
126
- parent_type = nil
127
143
  field_defn = nil
128
144
 
129
145
  @path.each do |name_in_doc|
@@ -162,7 +178,6 @@ module GraphQL
162
178
  end
163
179
  field_name = next_selections.first.name
164
180
  field_defn = @schema.get_field(type, field_name, @query.context) || raise("Invariant: no field called #{field_name} on #{type.graphql_name}")
165
- parent_type = type
166
181
  type = field_defn.type
167
182
  if type.non_null?
168
183
  type = type.of_type
@@ -22,7 +22,7 @@ module GraphQL
22
22
  # It is possible there are other extension items in this error, so handle
23
23
  # a one level deep merge explicitly. However beyond that only show the
24
24
  # latest value and problems.
25
- super.merge({ "extensions" => { "value" => value, "problems" => validation_result.problems }}) do |key, oldValue, newValue|
25
+ super.merge({ "extensions" => { "value" => value_for_extensions, "problems" => validation_result.problems }}) do |key, oldValue, newValue|
26
26
  if oldValue.respond_to?(:merge)
27
27
  oldValue.merge(newValue)
28
28
  else
@@ -33,6 +33,21 @@ module GraphQL
33
33
 
34
34
  private
35
35
 
36
+ def value_for_extensions(value = @value)
37
+ case value
38
+ when Array
39
+ value.map { |item| value_for_extensions(item) }
40
+ when Hash
41
+ value.each_with_object({}) do |(key, item), result|
42
+ result[key] = value_for_extensions(item)
43
+ end
44
+ when Float
45
+ value.finite? ? value : value.to_s
46
+ else
47
+ value
48
+ end
49
+ end
50
+
36
51
  def problem_fields
37
52
  @problem_fields ||= @validation_result
38
53
  .problems
@@ -19,7 +19,7 @@ module GraphQL
19
19
  @storage = ast_variables.each_with_object({}) do |ast_variable, memo|
20
20
  if schema.validate_max_errors && schema.validate_max_errors <= @errors.count
21
21
  add_max_errors_reached_message
22
- break
22
+ break memo
23
23
  end
24
24
  # Find the right value for this variable:
25
25
  # - First, use the value provided at runtime
data/lib/graphql/query.rb CHANGED
@@ -45,8 +45,8 @@ module GraphQL
45
45
  end
46
46
 
47
47
  # @api private
48
- def handle_or_reraise(err)
49
- @schema.handle_or_reraise(context, err)
48
+ def handle_or_reraise(err, **kwargs)
49
+ @schema.handle_or_reraise(context, err, **kwargs)
50
50
  end
51
51
  end
52
52
 
@@ -159,6 +159,7 @@ module GraphQL
159
159
  @root_value = root_value
160
160
  @fragments = nil
161
161
  @operations = nil
162
+ @finalizers = @top_level_finalizers = nil
162
163
  @validate = validate
163
164
  self.static_validator = static_validator if static_validator
164
165
  context_tracers = (context ? context.fetch(:tracers, []) : [])
@@ -262,6 +263,10 @@ module GraphQL
262
263
  with_prepared_ast { @operations }
263
264
  end
264
265
 
266
+ def path
267
+ EmptyObjects::EMPTY_ARRAY
268
+ end
269
+
265
270
  # Run subtree partials of this query and return their results.
266
271
  # Each partial is identified with a `path:` and `object:`
267
272
  # where the path references a field in the AST and the object will be treated
@@ -271,7 +276,11 @@ module GraphQL
271
276
  # @return [Array<GraphQL::Query::Result>]
272
277
  def run_partials(partials_hashes)
273
278
  partials = partials_hashes.map { |partial_options| Partial.new(query: self, **partial_options) }
274
- Execution::Interpreter.run_all(@schema, partials, context: @context)
279
+ if context[:__graphql_execute_next]
280
+ Execution::Next.run_all(@schema, partials, context: @context)
281
+ else
282
+ Execution::Interpreter.run_all(@schema, partials, context: @context)
283
+ end
275
284
  end
276
285
 
277
286
  # Get the result for this query, executing it once