graphql 2.0.17 → 2.0.18

Sign up to get free protection for your applications and to get access to all the features.
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