graphql 2.0.17 → 2.0.18

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/ast.rb +2 -2
  3. data/lib/graphql/backtrace/tracer.rb +1 -1
  4. data/lib/graphql/execution/interpreter/resolve.rb +19 -0
  5. data/lib/graphql/execution/interpreter/runtime.rb +96 -88
  6. data/lib/graphql/execution/interpreter.rb +8 -13
  7. data/lib/graphql/execution/lazy.rb +2 -4
  8. data/lib/graphql/execution/multiplex.rb +2 -1
  9. data/lib/graphql/graphql_ext.bundle +0 -0
  10. data/lib/graphql/language/lexer.rb +216 -1505
  11. data/lib/graphql/language/lexer.ri +744 -0
  12. data/lib/graphql/language/parser.rb +9 -9
  13. data/lib/graphql/language/parser.y +9 -9
  14. data/lib/graphql/pagination/active_record_relation_connection.rb +0 -8
  15. data/lib/graphql/query/context.rb +45 -11
  16. data/lib/graphql/query.rb +15 -2
  17. data/lib/graphql/schema/field.rb +31 -19
  18. data/lib/graphql/schema/member/has_deprecation_reason.rb +3 -4
  19. data/lib/graphql/schema/member/has_fields.rb +6 -1
  20. data/lib/graphql/schema/object.rb +2 -4
  21. data/lib/graphql/schema/resolver/has_payload_type.rb +9 -9
  22. data/lib/graphql/schema/timeout.rb +23 -27
  23. data/lib/graphql/schema/warden.rb +8 -1
  24. data/lib/graphql/schema.rb +34 -0
  25. data/lib/graphql/static_validation/validator.rb +1 -1
  26. data/lib/graphql/tracing/active_support_notifications_trace.rb +16 -0
  27. data/lib/graphql/tracing/appoptics_trace.rb +231 -0
  28. data/lib/graphql/tracing/appsignal_trace.rb +66 -0
  29. data/lib/graphql/tracing/data_dog_trace.rb +148 -0
  30. data/lib/graphql/tracing/new_relic_trace.rb +75 -0
  31. data/lib/graphql/tracing/notifications_trace.rb +41 -0
  32. data/lib/graphql/tracing/platform_trace.rb +107 -0
  33. data/lib/graphql/tracing/platform_tracing.rb +15 -3
  34. data/lib/graphql/tracing/prometheus_trace.rb +89 -0
  35. data/lib/graphql/tracing/prometheus_tracing.rb +3 -3
  36. data/lib/graphql/tracing/scout_trace.rb +72 -0
  37. data/lib/graphql/tracing/statsd_trace.rb +56 -0
  38. data/lib/graphql/tracing.rb +136 -39
  39. data/lib/graphql/type_kinds.rb +6 -3
  40. data/lib/graphql/version.rb +1 -1
  41. data/lib/graphql.rb +7 -8
  42. metadata +14 -3
  43. data/lib/graphql/language/lexer.rl +0 -280
@@ -16,22 +16,22 @@ module_eval(<<'...end parser.y/module_eval...', 'parser.y', 448)
16
16
 
17
17
  EMPTY_ARRAY = [].freeze
18
18
 
19
- def initialize(query_string, filename:, tracer: Tracing::NullTracer)
19
+ def initialize(query_string, filename:, trace: Tracing::NullTrace)
20
20
  raise GraphQL::ParseError.new("No query string was present", nil, nil, query_string) if query_string.nil?
21
21
  @query_string = query_string
22
22
  @filename = filename
23
- @tracer = tracer
23
+ @trace = trace
24
24
  @reused_next_token = [nil, nil]
25
25
  end
26
26
 
27
27
  def parse_document
28
28
  @document ||= begin
29
29
  # Break the string into tokens
30
- @tracer.trace("lex", {query_string: @query_string}) do
30
+ @trace.lex(query_string: @query_string) do
31
31
  @tokens ||= GraphQL.scan(@query_string)
32
32
  end
33
33
  # From the tokens, build an AST
34
- @tracer.trace("parse", {query_string: @query_string}) do
34
+ @trace.parse(query_string: @query_string) do
35
35
  if @tokens.empty?
36
36
  raise GraphQL::ParseError.new("Unexpected end of document", nil, nil, @query_string)
37
37
  else
@@ -44,17 +44,17 @@ end
44
44
  class << self
45
45
  attr_accessor :cache
46
46
 
47
- def parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
48
- new(query_string, filename: filename, tracer: tracer).parse_document
47
+ def parse(query_string, filename: nil, trace: GraphQL::Tracing::NullTrace)
48
+ new(query_string, filename: filename, trace: trace).parse_document
49
49
  end
50
50
 
51
- def parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
51
+ def parse_file(filename, trace: GraphQL::Tracing::NullTrace)
52
52
  if cache
53
53
  cache.fetch(filename) do
54
- parse(File.read(filename), filename: filename, tracer: tracer)
54
+ parse(File.read(filename), filename: filename, trace: trace)
55
55
  end
56
56
  else
57
- parse(File.read(filename), filename: filename, tracer: tracer)
57
+ parse(File.read(filename), filename: filename, trace: trace)
58
58
  end
59
59
  end
60
60
  end
@@ -448,22 +448,22 @@ end
448
448
 
449
449
  EMPTY_ARRAY = [].freeze
450
450
 
451
- def initialize(query_string, filename:, tracer: Tracing::NullTracer)
451
+ def initialize(query_string, filename:, trace: Tracing::NullTrace)
452
452
  raise GraphQL::ParseError.new("No query string was present", nil, nil, query_string) if query_string.nil?
453
453
  @query_string = query_string
454
454
  @filename = filename
455
- @tracer = tracer
455
+ @trace = trace
456
456
  @reused_next_token = [nil, nil]
457
457
  end
458
458
 
459
459
  def parse_document
460
460
  @document ||= begin
461
461
  # Break the string into tokens
462
- @tracer.trace("lex", {query_string: @query_string}) do
462
+ @trace.lex(query_string: @query_string) do
463
463
  @tokens ||= GraphQL.scan(@query_string)
464
464
  end
465
465
  # From the tokens, build an AST
466
- @tracer.trace("parse", {query_string: @query_string}) do
466
+ @trace.parse(query_string: @query_string) do
467
467
  if @tokens.empty?
468
468
  raise GraphQL::ParseError.new("Unexpected end of document", nil, nil, @query_string)
469
469
  else
@@ -476,17 +476,17 @@ end
476
476
  class << self
477
477
  attr_accessor :cache
478
478
 
479
- def parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
480
- new(query_string, filename: filename, tracer: tracer).parse_document
479
+ def parse(query_string, filename: nil, trace: GraphQL::Tracing::NullTrace)
480
+ new(query_string, filename: filename, trace: trace).parse_document
481
481
  end
482
482
 
483
- def parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
483
+ def parse_file(filename, trace: GraphQL::Tracing::NullTrace)
484
484
  if cache
485
485
  cache.fetch(filename) do
486
- parse(File.read(filename), filename: filename, tracer: tracer)
486
+ parse(File.read(filename), filename: filename, trace: trace)
487
487
  end
488
488
  else
489
- parse(File.read(filename), filename: filename, tracer: tracer)
489
+ parse(File.read(filename), filename: filename, trace: trace)
490
490
  end
491
491
  end
492
492
  end
@@ -7,14 +7,6 @@ module GraphQL
7
7
  class ActiveRecordRelationConnection < Pagination::RelationConnection
8
8
  private
9
9
 
10
- def relation_larger_than(relation, initial_offset, size)
11
- if already_loaded?(relation)
12
- (relation.size + initial_offset) > size
13
- else
14
- set_offset(sliced_nodes, initial_offset + size).exists?
15
- end
16
- end
17
-
18
10
  def relation_count(relation)
19
11
  int_or_hash = if already_loaded?(relation)
20
12
  relation.size
@@ -72,6 +72,18 @@ module GraphQL
72
72
  # @return [Array<String, Integer>] The current position in the result
73
73
  attr_reader :path
74
74
 
75
+ module EmptyScopedContext
76
+ EMPTY_HASH = {}.freeze
77
+
78
+ def self.key?(k)
79
+ false
80
+ end
81
+
82
+ def self.merged_context
83
+ EMPTY_HASH
84
+ end
85
+ end
86
+
75
87
  # Make a new context which delegates key lookup to `values`
76
88
  # @param query [GraphQL::Query] the query who owns this context
77
89
  # @param values [Hash] A hash of arbitrary values which will be accessible at query-time
@@ -87,13 +99,14 @@ module GraphQL
87
99
  @path = []
88
100
  @value = nil
89
101
  @context = self # for SharedMethods TODO delete sharedmethods
90
- @scoped_context = ScopedContext.new(self)
102
+ @scoped_context = EmptyScopedContext
91
103
  end
92
104
 
93
105
  class ScopedContext
94
106
  def initialize(query_context)
95
107
  @query_context = query_context
96
108
  @scoped_contexts = {}
109
+ @all_keys = Set.new
97
110
  @no_path = [].freeze
98
111
  end
99
112
 
@@ -106,6 +119,7 @@ module GraphQL
106
119
  end
107
120
 
108
121
  def merge!(hash)
122
+ @all_keys.merge(hash.keys)
109
123
  ctx = @scoped_contexts
110
124
  current_path.each do |path_part|
111
125
  ctx = ctx[path_part] ||= { parent: ctx }
@@ -114,15 +128,12 @@ module GraphQL
114
128
  this_scoped_ctx.merge!(hash)
115
129
  end
116
130
 
117
- def current_path
118
- thread_info = Thread.current[:__graphql_runtime_info]
119
- (thread_info && thread_info[:current_path]) || @no_path
120
- end
121
-
122
131
  def key?(key)
123
- each_present_path_ctx do |path_ctx|
124
- if path_ctx.key?(key)
125
- return true
132
+ if @all_keys.include?(key)
133
+ each_present_path_ctx do |path_ctx|
134
+ if path_ctx.key?(key)
135
+ return true
136
+ end
126
137
  end
127
138
  end
128
139
  false
@@ -137,6 +148,10 @@ module GraphQL
137
148
  nil
138
149
  end
139
150
 
151
+ def current_path
152
+ @query_context.current_path || @no_path
153
+ end
154
+
140
155
  def dig(key, *other_keys)
141
156
  each_present_path_ctx do |path_ctx|
142
157
  if path_ctx.key?(key)
@@ -209,14 +224,30 @@ module GraphQL
209
224
  elsif @provided_values.key?(key)
210
225
  @provided_values[key]
211
226
  elsif RUNTIME_METADATA_KEYS.include?(key)
212
- thread_info = Thread.current[:__graphql_runtime_info]
213
- thread_info && thread_info[key]
227
+ if key == :current_path
228
+ current_path
229
+ else
230
+ thread_info = Thread.current[:__graphql_runtime_info]
231
+ thread_info && thread_info[key]
232
+ end
214
233
  else
215
234
  # not found
216
235
  nil
217
236
  end
218
237
  end
219
238
 
239
+ def current_path
240
+ thread_info = Thread.current[:__graphql_runtime_info]
241
+ path = thread_info &&
242
+ (result = thread_info[:current_result]) &&
243
+ (result.path)
244
+ if path && (rn = thread_info[:current_result_name])
245
+ path = path.dup
246
+ path.push(rn)
247
+ end
248
+ path
249
+ end
250
+
220
251
  def delete(key)
221
252
  if @scoped_context.key?(key)
222
253
  @scoped_context.delete(key)
@@ -298,6 +329,9 @@ module GraphQL
298
329
  end
299
330
 
300
331
  def scoped_merge!(hash)
332
+ if @scoped_context == EmptyScopedContext
333
+ @scoped_context = ScopedContext.new(self)
334
+ end
301
335
  @scoped_context.merge!(hash)
302
336
  end
303
337
 
data/lib/graphql/query.rb CHANGED
@@ -95,12 +95,20 @@ module GraphQL
95
95
  @fragments = nil
96
96
  @operations = nil
97
97
  @validate = validate
98
- @tracers = schema.tracers + (context ? context.fetch(:tracers, []) : [])
98
+ context_tracers = (context ? context.fetch(:tracers, []) : [])
99
+ @tracers = schema.tracers + context_tracers
100
+
99
101
  # Support `ctx[:backtrace] = true` for wrapping backtraces
100
102
  if context && context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
103
+ context_tracers += [GraphQL::Backtrace::Tracer]
101
104
  @tracers << GraphQL::Backtrace::Tracer
102
105
  end
103
106
 
107
+ if context_tracers.any? && !(schema.trace_class <= GraphQL::Tracing::LegacyTrace)
108
+ raise ArgumentError, "context[:tracers] and context[:backtrace] are not supported without `tracer_class(GraphQL::Tracing::LegacyTrace)` in the schema configuration, please add it."
109
+ end
110
+
111
+
104
112
  @analysis_errors = []
105
113
  if variables.is_a?(String)
106
114
  raise ArgumentError, "Query variables should be a Hash, not a String. Try JSON.parse to prepare variables."
@@ -157,6 +165,11 @@ module GraphQL
157
165
 
158
166
  attr_accessor :multiplex
159
167
 
168
+ # @return [GraphQL::Tracing::Trace]
169
+ def current_trace
170
+ @current_trace ||= multiplex ? multiplex.current_trace : schema.new_trace(multiplex: multiplex, query: self)
171
+ end
172
+
160
173
  def subscription_update?
161
174
  @subscription_topic && subscription?
162
175
  end
@@ -362,7 +375,7 @@ module GraphQL
362
375
  parse_error = nil
363
376
  @document ||= begin
364
377
  if query_string
365
- GraphQL.parse(query_string, tracer: self)
378
+ GraphQL.parse(query_string, trace: self.current_trace)
366
379
  end
367
380
  rescue GraphQL::ParseError => err
368
381
  parse_error = err
@@ -219,7 +219,7 @@ module GraphQL
219
219
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
220
220
  # @param validates [Array<Hash>] Configurations for validating this field
221
221
  # @fallback_value [Object] A fallback value if the method is not defined
222
- def initialize(type: nil, name: nil, owner: nil, null: nil, description: :not_given, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: :not_given, default_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: :not_given, &definition_block)
222
+ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: :not_given, &definition_block)
223
223
  if name.nil?
224
224
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
225
225
  end
@@ -233,9 +233,10 @@ module GraphQL
233
233
 
234
234
  @underscored_name = -Member::BuildType.underscore(name_s)
235
235
  @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s)
236
- if description != :not_given
237
- @description = description
238
- end
236
+
237
+ @description = description
238
+ @type = @owner_type = @own_validators = @own_directives = @own_arguments = nil # these will be prepared later if necessary
239
+
239
240
  self.deprecation_reason = deprecation_reason
240
241
 
241
242
  if method && hash_key && dig
@@ -257,6 +258,9 @@ module GraphQL
257
258
  if hash_key
258
259
  @hash_key = hash_key
259
260
  @hash_key_str = hash_key.to_s
261
+ else
262
+ @hash_key = NOT_CONFIGURED
263
+ @hash_key_str = NOT_CONFIGURED
260
264
  end
261
265
 
262
266
  @method_str = -method_name.to_s
@@ -272,15 +276,11 @@ module GraphQL
272
276
  true
273
277
  end
274
278
  @connection = connection
275
- @has_max_page_size = max_page_size != :not_given
276
- @max_page_size = max_page_size == :not_given ? nil : max_page_size
277
- @has_default_page_size = default_page_size != :not_given
278
- @default_page_size = default_page_size == :not_given ? nil : default_page_size
279
+ @max_page_size = max_page_size
280
+ @default_page_size = default_page_size
279
281
  @introspection = introspection
280
282
  @extras = extras
281
- if !broadcastable.nil?
282
- @broadcastable = broadcastable
283
- end
283
+ @broadcastable = broadcastable
284
284
  @resolver_class = resolver_class
285
285
  @scope = scope
286
286
  @trace = trace
@@ -355,7 +355,7 @@ module GraphQL
355
355
  # @return [Boolean, nil]
356
356
  # @see GraphQL::Subscriptions::BroadcastAnalyzer
357
357
  def broadcastable?
358
- if defined?(@broadcastable)
358
+ if !NOT_CONFIGURED.equal?(@broadcastable)
359
359
  @broadcastable
360
360
  elsif @resolver_class
361
361
  @resolver_class.broadcastable?
@@ -369,10 +369,10 @@ module GraphQL
369
369
  def description(text = nil)
370
370
  if text
371
371
  @description = text
372
- elsif defined?(@description)
372
+ elsif !NOT_CONFIGURED.equal?(@description)
373
373
  @description
374
374
  elsif @resolver_class
375
- @description || @resolver_class.description
375
+ @resolver_class.description
376
376
  else
377
377
  nil
378
378
  end
@@ -544,22 +544,34 @@ module GraphQL
544
544
 
545
545
  # @return [Boolean] True if this field's {#max_page_size} should override the schema default.
546
546
  def has_max_page_size?
547
- @has_max_page_size || (@resolver_class && @resolver_class.has_max_page_size?)
547
+ !NOT_CONFIGURED.equal?(@max_page_size) || (@resolver_class && @resolver_class.has_max_page_size?)
548
548
  end
549
549
 
550
550
  # @return [Integer, nil] Applied to connections if {#has_max_page_size?}
551
551
  def max_page_size
552
- @max_page_size || (@resolver_class && @resolver_class.max_page_size)
552
+ if !NOT_CONFIGURED.equal?(@max_page_size)
553
+ @max_page_size
554
+ elsif @resolver_class && @resolver_class.has_max_page_size?
555
+ @resolver_class.max_page_size
556
+ else
557
+ nil
558
+ end
553
559
  end
554
560
 
555
561
  # @return [Boolean] True if this field's {#default_page_size} should override the schema default.
556
562
  def has_default_page_size?
557
- @has_default_page_size || (@resolver_class && @resolver_class.has_default_page_size?)
563
+ !NOT_CONFIGURED.equal?(@default_page_size) || (@resolver_class && @resolver_class.has_default_page_size?)
558
564
  end
559
565
 
560
566
  # @return [Integer, nil] Applied to connections if {#has_default_page_size?}
561
567
  def default_page_size
562
- @default_page_size || (@resolver_class && @resolver_class.default_page_size)
568
+ if !NOT_CONFIGURED.equal?(@default_page_size)
569
+ @default_page_size
570
+ elsif @resolver_class && @resolver_class.has_default_page_size?
571
+ @resolver_class.default_page_size
572
+ else
573
+ nil
574
+ end
563
575
  end
564
576
 
565
577
  class MissingReturnTypeError < GraphQL::Error; end
@@ -661,7 +673,7 @@ module GraphQL
661
673
 
662
674
  inner_object = obj.object
663
675
 
664
- if defined?(@hash_key)
676
+ if !NOT_CONFIGURED.equal?(@hash_key)
665
677
  hash_value = if inner_object.is_a?(Hash)
666
678
  inner_object.key?(@hash_key) ? inner_object[@hash_key] : inner_object[@hash_key_str]
667
679
  elsif inner_object.respond_to?(:[])
@@ -5,17 +5,16 @@ module GraphQL
5
5
  class Member
6
6
  module HasDeprecationReason
7
7
  # @return [String, nil] Explains why this member was deprecated (if present, this will be marked deprecated in introspection)
8
- def deprecation_reason
9
- dir = self.directives.find { |d| d.is_a?(GraphQL::Schema::Directive::Deprecated) }
10
- dir && dir.arguments[:reason] # rubocop:disable Development/ContextIsPassedCop -- definition-related
11
- end
8
+ attr_reader :deprecation_reason
12
9
 
13
10
  # Set the deprecation reason for this member, or remove it by assigning `nil`
14
11
  # @param text [String, nil]
15
12
  def deprecation_reason=(text)
13
+ @deprecation_reason = text
16
14
  if text.nil?
17
15
  remove_directive(GraphQL::Schema::Directive::Deprecated)
18
16
  else
17
+ # This removes a previously-attached directive, if there is one:
19
18
  directive(GraphQL::Schema::Directive::Deprecated, reason: text)
20
19
  end
21
20
  end
@@ -72,7 +72,12 @@ module GraphQL
72
72
  def add_field(field_defn, method_conflict_warning: field_defn.method_conflict_warning?)
73
73
  # Check that `field_defn.original_name` equals `resolver_method` and `method_sym` --
74
74
  # that shows that no override value was given manually.
75
- if method_conflict_warning && CONFLICT_FIELD_NAMES.include?(field_defn.resolver_method) && field_defn.original_name == field_defn.resolver_method && field_defn.original_name == field_defn.method_sym && field_defn.hash_key.nil? && field_defn.dig_keys.nil?
75
+ if method_conflict_warning &&
76
+ CONFLICT_FIELD_NAMES.include?(field_defn.resolver_method) &&
77
+ field_defn.original_name == field_defn.resolver_method &&
78
+ field_defn.original_name == field_defn.method_sym &&
79
+ field_defn.hash_key == NOT_CONFIGURED &&
80
+ field_defn.dig_keys.nil?
76
81
  warn(conflict_field_name_warning(field_defn))
77
82
  end
78
83
  prev_defn = own_fields[field_defn.name]
@@ -48,9 +48,7 @@ module GraphQL
48
48
  # @return [GraphQL::Schema::Object, GraphQL::Execution::Lazy]
49
49
  # @raise [GraphQL::UnauthorizedError] if the user-provided hook returns `false`
50
50
  def authorized_new(object, context)
51
- trace_payload = { context: context, type: self, object: object, path: context[:current_path] }
52
-
53
- maybe_lazy_auth_val = context.query.trace("authorized", trace_payload) do
51
+ maybe_lazy_auth_val = context.query.current_trace.authorized(query: context.query, type: self, object: object) do
54
52
  begin
55
53
  authorized?(object, context)
56
54
  rescue GraphQL::UnauthorizedError => err
@@ -62,7 +60,7 @@ module GraphQL
62
60
 
63
61
  auth_val = if context.schema.lazy?(maybe_lazy_auth_val)
64
62
  GraphQL::Execution::Lazy.new do
65
- context.query.trace("authorized_lazy", trace_payload) do
63
+ context.query.current_trace.authorized_lazy(query: context.query, type: self, object: object) do
66
64
  context.schema.sync_lazy(maybe_lazy_auth_val)
67
65
  end
68
66
  end
@@ -89,16 +89,16 @@ module GraphQL
89
89
  def generate_payload_type
90
90
  resolver_name = graphql_name
91
91
  resolver_fields = all_field_definitions
92
- Class.new(object_class) do
93
- graphql_name("#{resolver_name}Payload")
94
- description("Autogenerated return type of #{resolver_name}.")
95
- resolver_fields.each do |f|
96
- # Reattach the already-defined field here
97
- # (The field's `.owner` will still point to the mutation, not the object type, I think)
98
- # Don't re-warn about a method conflict. Since this type is generated, it should be fixed in the resolver instead.
99
- add_field(f, method_conflict_warning: false)
100
- end
92
+ pt = Class.new(object_class)
93
+ pt.graphql_name("#{resolver_name}Payload")
94
+ pt.description("Autogenerated return type of #{resolver_name}.")
95
+ resolver_fields.each do |f|
96
+ # Reattach the already-defined field here
97
+ # (The field's `.owner` will still point to the mutation, not the object type, I think)
98
+ # Don't re-warn about a method conflict. Since this type is generated, it should be fixed in the resolver instead.
99
+ pt.add_field(f, method_conflict_warning: false)
101
100
  end
101
+ pt
102
102
  end
103
103
  end
104
104
  end
@@ -33,60 +33,56 @@ module GraphQL
33
33
  # end
34
34
  #
35
35
  class Timeout
36
- def self.use(schema, **options)
37
- tracer = new(**options)
38
- schema.tracer(tracer)
36
+ def self.use(schema, max_seconds: nil)
37
+ timeout = self.new(max_seconds: max_seconds)
38
+ schema.trace_with(self::Trace, timeout: timeout)
39
39
  end
40
40
 
41
- # @param max_seconds [Numeric] how many seconds the query should be allowed to resolve new fields
42
41
  def initialize(max_seconds:)
43
42
  @max_seconds = max_seconds
44
43
  end
45
44
 
46
- def trace(key, data)
47
- case key
48
- when 'execute_multiplex'
49
- data.fetch(:multiplex).queries.each do |query|
50
- timeout_duration_s = max_seconds(query)
45
+ module Trace
46
+ # @param max_seconds [Numeric] how many seconds the query should be allowed to resolve new fields
47
+ def initialize(timeout:, **rest)
48
+ @timeout = timeout
49
+ super
50
+ end
51
+
52
+ def execute_multiplex(multiplex:)
53
+ multiplex.queries.each do |query|
54
+ timeout_duration_s = @timeout.max_seconds(query)
51
55
  timeout_state = if timeout_duration_s == false
52
56
  # if the method returns `false`, don't apply a timeout
53
57
  false
54
58
  else
55
59
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
56
- timeout_at = now + (max_seconds(query) * 1000)
60
+ timeout_at = now + (timeout_duration_s * 1000)
57
61
  {
58
62
  timeout_at: timeout_at,
59
63
  timed_out: false
60
64
  }
61
65
  end
62
- query.context.namespace(self.class)[:state] = timeout_state
66
+ query.context.namespace(@timeout)[:state] = timeout_state
63
67
  end
68
+ super
69
+ end
64
70
 
65
- yield
66
- when 'execute_field', 'execute_field_lazy'
67
- query_context = data[:context] || data[:query].context
68
- timeout_state = query_context.namespace(self.class).fetch(:state)
71
+ def execute_field(query:, field:, **_rest)
72
+ timeout_state = query.context.namespace(@timeout).fetch(:state)
69
73
  # If the `:state` is `false`, then `max_seconds(query)` opted out of timeout for this query.
70
74
  if timeout_state != false && Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) > timeout_state.fetch(:timeout_at)
71
- error = if data[:context]
72
- GraphQL::Schema::Timeout::TimeoutError.new(query_context.parent_type, query_context.field)
73
- else
74
- field = data.fetch(:field)
75
- GraphQL::Schema::Timeout::TimeoutError.new(field.owner, field)
76
- end
77
-
75
+ error = GraphQL::Schema::Timeout::TimeoutError.new(field)
78
76
  # Only invoke the timeout callback for the first timeout
79
77
  if !timeout_state[:timed_out]
80
78
  timeout_state[:timed_out] = true
81
- handle_timeout(error, query_context.query)
79
+ @timeout.handle_timeout(error, query)
82
80
  end
83
81
 
84
82
  error
85
83
  else
86
84
  yield
87
85
  end
88
- else
89
- yield
90
86
  end
91
87
  end
92
88
 
@@ -114,8 +110,8 @@ module GraphQL
114
110
  # to take this error and raise a new one which _doesn't_ descend from {GraphQL::ExecutionError},
115
111
  # such as `RuntimeError`.
116
112
  class TimeoutError < GraphQL::ExecutionError
117
- def initialize(parent_type, field)
118
- super("Timeout on #{parent_type.graphql_name}.#{field.graphql_name}")
113
+ def initialize(field)
114
+ super("Timeout on #{field.path}")
119
115
  end
120
116
  end
121
117
  end
@@ -96,6 +96,13 @@ module GraphQL
96
96
  @subscription = @schema.subscription
97
97
  @context = context
98
98
  @visibility_cache = read_through { |m| filter.call(m, context) }
99
+ # Initialize all ivars to improve object shape consistency:
100
+ @types = @visible_types = @reachable_types = @visible_parent_fields =
101
+ @visible_possible_types = @visible_fields = @visible_arguments = @visible_enum_arrays =
102
+ @visible_enum_values = @visible_interfaces = @type_visibility = @type_memberships =
103
+ @visible_and_reachable_type = @unions = @unfiltered_interfaces = @references_to =
104
+ @reachable_type_set =
105
+ nil
99
106
  end
100
107
 
101
108
  # @return [Hash<String, GraphQL::BaseType>] Visible types in the schema
@@ -347,7 +354,7 @@ module GraphQL
347
354
  end
348
355
 
349
356
  def reachable_type_set
350
- return @reachable_type_set if defined?(@reachable_type_set)
357
+ return @reachable_type_set if @reachable_type_set
351
358
 
352
359
  @reachable_type_set = Set.new
353
360
  rt_hash = {}
@@ -143,6 +143,15 @@ module GraphQL
143
143
  @subscriptions = new_implementation
144
144
  end
145
145
 
146
+ def trace_class(new_class = nil)
147
+ if new_class
148
+ @trace_class = new_class
149
+ elsif !defined?(@trace_class)
150
+ @trace_class = Class.new(GraphQL::Tracing::Trace)
151
+ end
152
+ @trace_class
153
+ end
154
+
146
155
  # Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
147
156
  # @see {#as_json}
148
157
  # @return [String]
@@ -926,6 +935,12 @@ module GraphQL
926
935
  end
927
936
 
928
937
  def tracer(new_tracer)
938
+ if defined?(@trace_class) && !(@trace_class < GraphQL::Tracing::LegacyTrace)
939
+ raise ArgumentError, "Can't add tracer after configuring a `trace_class`, use GraphQL::Tracing::LegacyTrace to merge legacy tracers into a trace class instead."
940
+ elsif !defined?(@trace_class)
941
+ @trace_class = Class.new(GraphQL::Tracing::LegacyTrace)
942
+ end
943
+
929
944
  own_tracers << new_tracer
930
945
  end
931
946
 
@@ -933,6 +948,25 @@ module GraphQL
933
948
  find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers
934
949
  end
935
950
 
951
+ # Mix `trace_mod` into this schema's `Trace` class so that its methods
952
+ # will be called at runtime.
953
+ #
954
+ # @param trace_mod [Module] A module that implements tracing methods
955
+ # @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
956
+ # @return [void]
957
+ def trace_with(trace_mod, **options)
958
+ @trace_options ||= {}
959
+ @trace_options.merge!(options)
960
+ trace_class.include(trace_mod)
961
+ end
962
+
963
+ def new_trace(**options)
964
+ if defined?(@trace_options)
965
+ options = @trace_options.merge(options)
966
+ end
967
+ trace_class.new(**options)
968
+ end
969
+
936
970
  def query_analyzer(new_analyzer)
937
971
  own_query_analyzers << new_analyzer
938
972
  end
@@ -27,7 +27,7 @@ module GraphQL
27
27
  # @param max_errors [Integer] Maximum number of errors before aborting validation. Any positive number will limit the number of errors. Defaults to nil for no limit.
28
28
  # @return [Array<Hash>]
29
29
  def validate(query, validate: true, timeout: nil, max_errors: nil)
30
- query.trace("validate", { validate: validate, query: query }) do
30
+ query.current_trace.validate(validate: validate, query: query) do
31
31
  errors = if validate == false
32
32
  []
33
33
  else