graphql 2.0.6 → 2.0.9

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.

@@ -321,13 +321,13 @@ rule
321
321
 
322
322
  object_type_extension:
323
323
  /* TODO - This first one shouldn't be necessary but parser is getting confused */
324
- EXTEND TYPE name implements LCURLY field_definition_list RCURLY { result = make_node(:ObjectTypeExtension, name: val[2], interfaces: val[3], directives: [], fields: val[5], position_source: val[0]) }
325
- | EXTEND TYPE name implements_opt directives_list_opt LCURLY field_definition_list RCURLY { result = make_node(:ObjectTypeExtension, name: val[2], interfaces: val[3], directives: val[4], fields: val[6], position_source: val[0]) }
324
+ EXTEND TYPE name implements field_definition_list_opt { result = make_node(:ObjectTypeExtension, name: val[2], interfaces: val[3], directives: [], fields: val[4], position_source: val[0]) }
325
+ | EXTEND TYPE name implements_opt directives_list_opt field_definition_list_opt { result = make_node(:ObjectTypeExtension, name: val[2], interfaces: val[3], directives: val[4], fields: val[5], position_source: val[0]) }
326
326
  | EXTEND TYPE name implements_opt directives_list { result = make_node(:ObjectTypeExtension, name: val[2], interfaces: val[3], directives: val[4], fields: [], position_source: val[0]) }
327
327
  | EXTEND TYPE name implements { result = make_node(:ObjectTypeExtension, name: val[2], interfaces: val[3], directives: [], fields: [], position_source: val[0]) }
328
328
 
329
329
  interface_type_extension:
330
- EXTEND INTERFACE name implements_opt directives_list_opt LCURLY field_definition_list RCURLY { result = make_node(:InterfaceTypeExtension, name: val[2], interfaces: val[3], directives: val[4], fields: val[6], position_source: val[0]) }
330
+ EXTEND INTERFACE name implements_opt directives_list_opt field_definition_list_opt { result = make_node(:InterfaceTypeExtension, name: val[2], interfaces: val[3], directives: val[4], fields: val[5], position_source: val[0]) }
331
331
  | EXTEND INTERFACE name implements_opt directives_list { result = make_node(:InterfaceTypeExtension, name: val[2], interfaces: val[3], directives: val[4], fields: [], position_source: val[0]) }
332
332
  | EXTEND INTERFACE name implements { result = make_node(:InterfaceTypeExtension, name: val[2], interfaces: val[3], directives: [], fields: [], position_source: val[0]) }
333
333
 
@@ -355,8 +355,8 @@ rule
355
355
  }
356
356
 
357
357
  object_type_definition:
358
- description_opt TYPE name implements_opt directives_list_opt LCURLY field_definition_list RCURLY {
359
- result = make_node(:ObjectTypeDefinition, name: val[2], interfaces: val[3], directives: val[4], fields: val[6], description: val[0] || get_description(val[1]), definition_line: val[1].line, position_source: val[0] || val[1])
358
+ description_opt TYPE name implements_opt directives_list_opt field_definition_list_opt {
359
+ result = make_node(:ObjectTypeDefinition, name: val[2], interfaces: val[3], directives: val[4], fields: val[5], description: val[0] || get_description(val[1]), definition_line: val[1].line, position_source: val[0] || val[1])
360
360
  }
361
361
 
362
362
  implements_opt:
@@ -394,14 +394,18 @@ rule
394
394
  result = make_node(:FieldDefinition, name: val[1], arguments: val[2], type: val[4], directives: val[5], description: val[0] || get_description(val[1]), definition_line: val[1].line, position_source: val[0] || val[1])
395
395
  }
396
396
 
397
- field_definition_list:
397
+ field_definition_list_opt:
398
398
  /* none */ { result = EMPTY_ARRAY }
399
+ | LCURLY field_definition_list RCURLY { result = val[1] }
400
+
401
+ field_definition_list:
402
+ /* none - this is not actually valid but graphql-ruby used to print this */ { result = EMPTY_ARRAY }
399
403
  | field_definition { result = [val[0]] }
400
404
  | field_definition_list field_definition { val[0] << val[1] }
401
405
 
402
406
  interface_type_definition:
403
- description_opt INTERFACE name implements_opt directives_list_opt LCURLY field_definition_list RCURLY {
404
- result = make_node(:InterfaceTypeDefinition, name: val[2], interfaces: val[3], directives: val[4], fields: val[6], description: val[0] || get_description(val[1]), definition_line: val[1].line, position_source: val[0] || val[1])
407
+ description_opt INTERFACE name implements_opt directives_list_opt field_definition_list_opt {
408
+ result = make_node(:InterfaceTypeDefinition, name: val[2], interfaces: val[3], directives: val[4], fields: val[5], description: val[0] || get_description(val[1]), definition_line: val[1].line, position_source: val[0] || val[1])
405
409
  }
406
410
 
407
411
  union_members:
@@ -267,12 +267,16 @@ module GraphQL
267
267
  end
268
268
 
269
269
  def print_field_definitions(fields)
270
- out = " {\n".dup
271
- fields.each.with_index do |field, i|
272
- out << print_description(field, indent: ' ', first_in_block: i == 0)
273
- out << " #{print_field_definition(field)}\n"
270
+ if fields.empty?
271
+ ""
272
+ else
273
+ out = " {\n".dup
274
+ fields.each.with_index do |field, i|
275
+ out << print_description(field, indent: ' ', first_in_block: i == 0)
276
+ out << " #{print_field_definition(field)}\n"
277
+ end
278
+ out << "}"
274
279
  end
275
- out << "}"
276
280
  end
277
281
 
278
282
  def print_directives(directives)
@@ -56,8 +56,9 @@ module GraphQL
56
56
  # @param last [Integer, nil] Limit parameter from the client, if provided
57
57
  # @param before [String, nil] A cursor for pagination, if the client provided one.
58
58
  # @param arguments [Hash] The arguments to the field that returned the collection wrapped by this connection
59
- # @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given.
60
- def initialize(items, parent: nil, field: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil, edge_class: nil, arguments: nil)
59
+ # @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given and no `default_page_size` is set.
60
+ # @param default_page_size [Integer, nil] A configured value to determine the result size when neither first or last are given.
61
+ def initialize(items, parent: nil, field: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, default_page_size: :not_given, last: nil, before: nil, edge_class: nil, arguments: nil)
61
62
  @items = items
62
63
  @parent = parent
63
64
  @context = context
@@ -76,6 +77,12 @@ module GraphQL
76
77
  else
77
78
  max_page_size
78
79
  end
80
+ @has_default_page_size_override = default_page_size != :not_given
81
+ @default_page_size = if default_page_size == :not_given
82
+ nil
83
+ else
84
+ default_page_size
85
+ end
79
86
  end
80
87
 
81
88
  def max_page_size=(new_value)
@@ -95,16 +102,36 @@ module GraphQL
95
102
  @has_max_page_size_override
96
103
  end
97
104
 
105
+ def default_page_size=(new_value)
106
+ @has_default_page_size_override = true
107
+ @default_page_size = new_value
108
+ end
109
+
110
+ def default_page_size
111
+ if @has_default_page_size_override
112
+ @default_page_size
113
+ else
114
+ context.schema.default_page_size
115
+ end
116
+ end
117
+
118
+ def has_default_page_size_override?
119
+ @has_default_page_size_override
120
+ end
121
+
98
122
  attr_writer :first
99
123
  # @return [Integer, nil]
100
124
  # A clamped `first` value.
101
125
  # (The underlying instance variable doesn't have limits on it.)
102
- # If neither `first` nor `last` is given, but `max_page_size` is present, max_page_size is used for first.
126
+ # If neither `first` nor `last` is given, but `default_page_size` is
127
+ # present, default_page_size is used for first. If `default_page_size`
128
+ # is greater than `max_page_size``, it'll be clamped down to
129
+ # `max_page_size`. If `default_page_size` is nil, use `max_page_size`.
103
130
  def first
104
131
  @first ||= begin
105
132
  capped = limit_pagination_argument(@first_value, max_page_size)
106
133
  if capped.nil? && last.nil?
107
- capped = max_page_size
134
+ capped = limit_pagination_argument(default_page_size, max_page_size) || max_page_size
108
135
  end
109
136
  capped
110
137
  end
@@ -70,6 +70,7 @@ module GraphQL
70
70
  parent: parent,
71
71
  field: field,
72
72
  max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
73
+ default_page_size: field.has_default_page_size? ? field.default_page_size : context.schema.default_page_size,
73
74
  first: arguments[:first],
74
75
  after: arguments[:after],
75
76
  last: arguments[:last],
@@ -47,6 +47,9 @@ module GraphQL
47
47
  if field.has_max_page_size? && !value.has_max_page_size_override?
48
48
  value.max_page_size = field.max_page_size
49
49
  end
50
+ if field.has_default_page_size? && !value.has_default_page_size_override?
51
+ value.default_page_size = field.default_page_size
52
+ end
50
53
  if context.schema.new_connections? && (custom_t = context.schema.connections.edge_class_for_field(@field))
51
54
  value.edge_class = custom_t
52
55
  end
@@ -64,6 +67,7 @@ module GraphQL
64
67
  original_arguments,
65
68
  field: field,
66
69
  max_page_size: field.max_page_size,
70
+ default_page_size: field.default_page_size,
67
71
  parent: object,
68
72
  context: context,
69
73
  )
@@ -200,6 +200,7 @@ module GraphQL
200
200
  # @param connection [Boolean] `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
201
201
  # @param connection_extension [Class] The extension to add, to implement connections. If `nil`, no extension is added.
202
202
  # @param max_page_size [Integer, nil] For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
203
+ # @param default_page_size [Integer, nil] For connections, the default number of items to return from this field, or `nil` to return unlimited results.
203
204
  # @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__`
204
205
  # @param resolver_class [Class] (Private) A {Schema::Resolver} which this field was derived from. Use `resolver:` to create a field with a resolver.
205
206
  # @param arguments [{String=>GraphQL::Schema::Argument, Hash}] Arguments for this field (may be added in the block, also)
@@ -214,7 +215,8 @@ module GraphQL
214
215
  # @param ast_node [Language::Nodes::FieldDefinition, nil] If this schema was parsed from definition, this AST node defined the field
215
216
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
216
217
  # @param validates [Array<Hash>] Configurations for validating this field
217
- 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, 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, &definition_block)
218
+ # @fallback_value [Object] A fallback value if the method is not defined
219
+ 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)
218
220
  if name.nil?
219
221
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
220
222
  end
@@ -246,9 +248,12 @@ module GraphQL
246
248
  end
247
249
  end
248
250
 
249
- method_name = method || name_s
251
+ method_name = method || hash_key || name_s
250
252
  @dig_keys = dig
251
- @hash_key = hash_key
253
+ if hash_key
254
+ @hash_key = hash_key
255
+ @hash_key_str = hash_key.to_s
256
+ end
252
257
 
253
258
  @method_str = -method_name.to_s
254
259
  @method_sym = method_name.to_sym
@@ -265,6 +270,8 @@ module GraphQL
265
270
  @connection = connection
266
271
  @has_max_page_size = max_page_size != :not_given
267
272
  @max_page_size = max_page_size == :not_given ? nil : max_page_size
273
+ @has_default_page_size = default_page_size != :not_given
274
+ @default_page_size = default_page_size == :not_given ? nil : default_page_size
268
275
  @introspection = introspection
269
276
  @extras = extras
270
277
  if !broadcastable.nil?
@@ -277,6 +284,7 @@ module GraphQL
277
284
  @relay_nodes_field = relay_nodes_field
278
285
  @ast_node = ast_node
279
286
  @method_conflict_warning = method_conflict_warning
287
+ @fallback_value = fallback_value
280
288
 
281
289
  arguments.each do |name, arg|
282
290
  case arg
@@ -459,11 +467,11 @@ module GraphQL
459
467
  end
460
468
 
461
469
  if max_possible_page_size.nil?
462
- max_possible_page_size = max_page_size || query.schema.default_max_page_size
470
+ max_possible_page_size = default_page_size || query.schema.default_page_size || max_page_size || query.schema.default_max_page_size
463
471
  end
464
472
 
465
473
  if max_possible_page_size.nil?
466
- raise GraphQL::Error, "Can't calculate complexity for #{path}, no `first:`, `last:`, `max_page_size` or `default_max_page_size`"
474
+ raise GraphQL::Error, "Can't calculate complexity for #{path}, no `first:`, `last:`, `default_page_size`, `max_page_size` or `default_max_page_size`"
467
475
  else
468
476
  metadata_complexity = 0
469
477
  lookahead = GraphQL::Execution::Lookahead.new(query: query, field: self, ast_nodes: nodes, owner_type: owner)
@@ -491,7 +499,13 @@ module GraphQL
491
499
  case defined_complexity
492
500
  when Proc
493
501
  arguments = query.arguments_for(nodes.first, self)
494
- defined_complexity.call(query.context, arguments.keyword_arguments, child_complexity)
502
+ if arguments.is_a?(GraphQL::ExecutionError)
503
+ return child_complexity
504
+ elsif arguments.respond_to?(:keyword_arguments)
505
+ arguments = arguments.keyword_arguments
506
+ end
507
+
508
+ defined_complexity.call(query.context, arguments, child_complexity)
495
509
  when Numeric
496
510
  defined_complexity + child_complexity
497
511
  else
@@ -534,6 +548,16 @@ module GraphQL
534
548
  @max_page_size || (@resolver_class && @resolver_class.max_page_size)
535
549
  end
536
550
 
551
+ # @return [Boolean] True if this field's {#default_page_size} should override the schema default.
552
+ def has_default_page_size?
553
+ @has_default_page_size || (@resolver_class && @resolver_class.has_default_page_size?)
554
+ end
555
+
556
+ # @return [Integer, nil] Applied to connections if {#has_default_page_size?}
557
+ def default_page_size
558
+ @default_page_size || (@resolver_class && @resolver_class.default_page_size)
559
+ end
560
+
537
561
  class MissingReturnTypeError < GraphQL::Error; end
538
562
  attr_writer :type
539
563
 
@@ -639,10 +663,8 @@ module GraphQL
639
663
 
640
664
  inner_object = obj.object
641
665
 
642
- if @hash_key
643
- inner_object[@hash_key]
644
- elsif @dig_keys
645
- inner_object.dig(*@dig_keys)
666
+ if defined?(@hash_key)
667
+ inner_object[@hash_key] || inner_object[@hash_key_str] || (@fallback_value != :not_given ? @fallback_value : nil)
646
668
  elsif obj.respond_to?(resolver_method)
647
669
  method_to_call = resolver_method
648
670
  method_receiver = obj
@@ -653,10 +675,26 @@ module GraphQL
653
675
  obj.public_send(resolver_method)
654
676
  end
655
677
  elsif inner_object.is_a?(Hash)
656
- if inner_object.key?(@method_sym)
678
+ if @dig_keys
679
+ inner_object.dig(*@dig_keys)
680
+ elsif defined?(@hash_key)
681
+ if inner_object.key?(@hash_key)
682
+ inner_object[@hash_key]
683
+ elsif inner_object.key?(@hash_key_str)
684
+ inner_object[@hash_key_str]
685
+ elsif @fallback_value != :not_given
686
+ @fallback_value
687
+ else
688
+ nil
689
+ end
690
+ elsif inner_object.key?(@method_sym)
657
691
  inner_object[@method_sym]
658
- else
692
+ elsif inner_object.key?(@method_str)
659
693
  inner_object[@method_str]
694
+ elsif @fallback_value != :not_given
695
+ @fallback_value
696
+ else
697
+ nil
660
698
  end
661
699
  elsif inner_object.respond_to?(@method_sym)
662
700
  method_to_call = @method_sym
@@ -666,6 +704,8 @@ module GraphQL
666
704
  else
667
705
  inner_object.public_send(@method_sym)
668
706
  end
707
+ elsif @fallback_value != :not_given
708
+ @fallback_value
669
709
  else
670
710
  raise <<-ERR
671
711
  Failed to implement #{@owner.graphql_name}.#{@name}, tried:
@@ -674,7 +714,7 @@ module GraphQL
674
714
  - `#{inner_object.class}##{@method_sym}`, which did not exist
675
715
  - Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{inner_object}`, but it wasn't a Hash
676
716
 
677
- To implement this field, define one of the methods above (and check for typos)
717
+ To implement this field, define one of the methods above (and check for typos), or supply a `fallback_value`.
678
718
  ERR
679
719
  end
680
720
  end
@@ -746,7 +786,7 @@ module GraphQL
746
786
 
747
787
  if unsatisfied_ruby_kwargs.any? || unsatisfied_method_params.any?
748
788
  raise FieldImplementationFailed.new, <<-ERR
749
- Failed to call #{method_name} on #{receiver.inspect} because the Ruby method params were incompatible with the GraphQL arguments:
789
+ Failed to call `#{method_name.inspect}` on #{receiver.inspect} because the Ruby method params were incompatible with the GraphQL arguments:
750
790
 
751
791
  #{ unsatisfied_ruby_kwargs
752
792
  .map { |key, value| "- `#{key}: #{value}` was given by GraphQL but not defined in the Ruby method. Add `#{key}:` to the method parameters." }
@@ -328,6 +328,27 @@ module GraphQL
328
328
  (!!defined?(@max_page_size)) || (superclass.respond_to?(:has_max_page_size?) && superclass.has_max_page_size?)
329
329
  end
330
330
 
331
+ # Get or set the `default_page_size:` which will be configured for fields using this resolver
332
+ # (`nil` means "unlimited default page size".)
333
+ # @param default_page_size [Integer, nil] Set a new value
334
+ # @return [Integer, nil] The `default_page_size` assigned to fields that use this resolver
335
+ def default_page_size(new_default_page_size = :not_given)
336
+ if new_default_page_size != :not_given
337
+ @default_page_size = new_default_page_size
338
+ elsif defined?(@default_page_size)
339
+ @default_page_size
340
+ elsif superclass.respond_to?(:default_page_size)
341
+ superclass.default_page_size
342
+ else
343
+ nil
344
+ end
345
+ end
346
+
347
+ # @return [Boolean] `true` if this resolver or a superclass has an assigned `default_page_size`
348
+ def has_default_page_size?
349
+ (!!defined?(@default_page_size)) || (superclass.respond_to?(:has_default_page_size?) && superclass.has_default_page_size?)
350
+ end
351
+
331
352
  # A non-normalized type configuration, without `null` applied
332
353
  def type_expr
333
354
  @type_expr || (superclass.respond_to?(:type_expr) ? superclass.type_expr : nil)
@@ -55,7 +55,9 @@ module GraphQL
55
55
  if visible_item.nil?
56
56
  visible_item = item
57
57
  else
58
- raise Schema::DuplicateNamesError, "Found two visible definitions for `#{item.path}`: #{visible_item.inspect}, #{item.inspect}"
58
+ raise DuplicateNamesError.new(
59
+ duplicated_name: item.path, duplicated_definition_1: visible_item.inspect, duplicated_definition_2: item.inspect
60
+ )
59
61
  end
60
62
  end
61
63
  end
@@ -362,7 +364,9 @@ module GraphQL
362
364
  if @reachable_type_set.add?(type)
363
365
  type_by_name = rt_hash[type.graphql_name] ||= type
364
366
  if type_by_name != type
365
- raise DuplicateNamesError, "Found two visible type definitions for `#{type.graphql_name}`: #{type.inspect}, #{type_by_name.inspect}"
367
+ raise DuplicateNamesError.new(
368
+ duplicated_name: type.graphql_name, duplicated_definition_1: type.inspect, duplicated_definition_2: type_by_name.inspect
369
+ )
366
370
  end
367
371
  if type.kind.input_object?
368
372
  # recurse into visible arguments
@@ -73,14 +73,16 @@ module GraphQL
73
73
  extend GraphQL::Schema::Member::HasAstNode
74
74
  extend GraphQL::Schema::FindInheritedValue
75
75
 
76
- class DuplicateTypeNamesError < GraphQL::Error
77
- def initialize(type_name:, first_definition:, second_definition:, path:)
78
- super("Multiple definitions for `#{type_name}`. Previously found #{first_definition.inspect} (#{first_definition.class}), then found #{second_definition.inspect} (#{second_definition.class}) at #{path.join(".")}")
76
+ class DuplicateNamesError < GraphQL::Error
77
+ attr_reader :duplicated_name
78
+ def initialize(duplicated_name:, duplicated_definition_1:, duplicated_definition_2:)
79
+ @duplicated_name = duplicated_name
80
+ super(
81
+ "Found two visible definitions for `#{duplicated_name}`: #{duplicated_definition_1}, #{duplicated_definition_2}"
82
+ )
79
83
  end
80
84
  end
81
85
 
82
- class DuplicateNamesError < GraphQL::Error; end
83
-
84
86
  class UnresolvedLateBoundTypeError < GraphQL::Error
85
87
  attr_reader :type
86
88
  def initialize(type:)
@@ -225,7 +227,9 @@ module GraphQL
225
227
  if visible_t.nil?
226
228
  visible_t = t
227
229
  else
228
- raise DuplicateNamesError, "Found two visible type definitions for `#{k}`: #{visible_t.inspect}, #{t.inspect}"
230
+ raise DuplicateNamesError.new(
231
+ duplicated_name: k, duplicated_definition_1: visible_t.inspect, duplicated_definition_2: t.inspect
232
+ )
229
233
  end
230
234
  end
231
235
  end
@@ -252,7 +256,9 @@ module GraphQL
252
256
  if visible_t.nil?
253
257
  visible_t = t
254
258
  else
255
- raise DuplicateNamesError, "Found two visible type definitions for `#{type_name}`: #{visible_t.inspect}, #{t.inspect}"
259
+ raise DuplicateNamesError.new(
260
+ duplicated_name: type_name, duplicated_definition_1: visible_t.inspect, duplicated_definition_2: t.inspect
261
+ )
256
262
  end
257
263
  end
258
264
  end
@@ -500,6 +506,14 @@ module GraphQL
500
506
  end
501
507
  end
502
508
 
509
+ def default_page_size(new_default_page_size = nil)
510
+ if new_default_page_size
511
+ @default_page_size = new_default_page_size
512
+ else
513
+ @default_page_size || find_inherited_value(:default_page_size)
514
+ end
515
+ end
516
+
503
517
  def query_execution_strategy(new_query_execution_strategy = nil)
504
518
  if new_query_execution_strategy
505
519
  @query_execution_strategy = new_query_execution_strategy
@@ -977,7 +991,9 @@ module GraphQL
977
991
  if !defined?(@subscription_extension_added) && subscription && self.subscriptions
978
992
  @subscription_extension_added = true
979
993
  subscription.all_field_definitions.each do |field|
980
- field.extension(Subscriptions::DefaultSubscriptionResolveExtension)
994
+ if !field.extensions.any? { |ext| ext.is_a?(Subscriptions::DefaultSubscriptionResolveExtension) }
995
+ field.extension(Subscriptions::DefaultSubscriptionResolveExtension)
996
+ end
981
997
  end
982
998
  end
983
999
  end
@@ -39,12 +39,14 @@ module GraphQL
39
39
  end
40
40
 
41
41
  # @param schema [Class] the GraphQL schema this manager belongs to
42
- def initialize(schema:, broadcast: false, default_broadcastable: false, **rest)
42
+ # @param validate_update [Boolean] If false, then validation is skipped when executing updates
43
+ def initialize(schema:, validate_update: true, broadcast: false, default_broadcastable: false, **rest)
43
44
  if broadcast
44
45
  schema.query_analyzer(Subscriptions::BroadcastAnalyzer)
45
46
  end
46
47
  @default_broadcastable = default_broadcastable
47
48
  @schema = schema
49
+ @validate_update = validate_update
48
50
  end
49
51
 
50
52
  # @return [Boolean] Used when fields don't have `broadcastable:` explicitly set
@@ -117,14 +119,16 @@ module GraphQL
117
119
  variables = query_data.fetch(:variables)
118
120
  context = query_data.fetch(:context)
119
121
  operation_name = query_data.fetch(:operation_name)
120
- result = @schema.execute(
122
+ execute_options = {
121
123
  query: query_string,
122
124
  context: context,
123
125
  subscription_topic: event.topic,
124
126
  operation_name: operation_name,
125
127
  variables: variables,
126
128
  root_value: object,
127
- )
129
+ }
130
+ execute_options[:validate] = validate_update?(**execute_options)
131
+ result = @schema.execute(**execute_options)
128
132
  subscriptions_context = result.context.namespace(:subscriptions)
129
133
  if subscriptions_context[:no_update]
130
134
  result = nil
@@ -142,6 +146,14 @@ module GraphQL
142
146
  result
143
147
  end
144
148
 
149
+ # Define this method to customize whether to validate
150
+ # this subscription when executing an update.
151
+ #
152
+ # @return [Boolean] defaults to `true`, or false if `validate: false` is provided.
153
+ def validate_update?(query:, context:, root_value:, subscription_topic:, operation_name:, variables:)
154
+ @validate_update
155
+ end
156
+
145
157
  # Run the update query for this subscription and deliver it
146
158
  # @see {#execute_update}
147
159
  # @see {#deliver}
@@ -17,15 +17,21 @@ module GraphQL
17
17
  def platform_trace(platform_key, key, data)
18
18
  tracer.trace(platform_key, service: service_name) do |span|
19
19
  span.span_type = 'custom'
20
+ if defined?(Datadog::Tracing::Metadata::Ext) # Introduced in ddtrace 1.0
21
+ span.set_tag(Datadog::Tracing::Metadata::Ext::TAG_COMPONENT, 'graphql')
22
+ span.set_tag(Datadog::Tracing::Metadata::Ext::TAG_OPERATION, key)
23
+ end
20
24
 
21
25
  if key == 'execute_multiplex'
22
26
  operations = data[:multiplex].queries.map(&:selected_operation_name).join(', ')
23
- span.resource = if operations.empty?
27
+
28
+ resource = if operations.empty?
24
29
  first_query = data[:multiplex].queries.first
25
30
  fallback_transaction_name(first_query && first_query.context)
26
31
  else
27
32
  operations
28
33
  end
34
+ span.resource = resource if resource
29
35
 
30
36
  # For top span of query, set the analytics sample rate tag, if available.
31
37
  if analytics_enabled?
@@ -39,6 +45,8 @@ module GraphQL
39
45
  span.set_tag(:query_string, data[:query].query_string)
40
46
  end
41
47
 
48
+ prepare_span(key, data, span)
49
+
42
50
  yield
43
51
  end
44
52
  end
@@ -47,6 +55,13 @@ module GraphQL
47
55
  options.fetch(:service, 'ruby-graphql')
48
56
  end
49
57
 
58
+ # Implement this method in a subclass to apply custom tags to datadog spans
59
+ # @param key [String] The event being traced
60
+ # @param data [Hash] The runtime data for this event (@see GraphQL::Tracing for keys for each event)
61
+ # @param span [Datadog::Tracing::SpanOperation] The datadog span for this event
62
+ def prepare_span(key, data, span)
63
+ end
64
+
50
65
  def tracer
51
66
  default_tracer = defined?(Datadog::Tracing) ? Datadog::Tracing : Datadog.tracer
52
67
 
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ class OpenTelemetryTracing < PlatformTracing
6
+ self.platform_keys = {
7
+ 'lex' => 'graphql.lex',
8
+ 'parse' => 'graphql.parse',
9
+ 'validate' => 'graphql.validate',
10
+ 'analyze_query' => 'graphql.analyze_query',
11
+ 'analyze_multiplex' => 'graphql.analyze_multiplex',
12
+ 'execute_query' => 'graphql.execute_query',
13
+ 'execute_query_lazy' => 'graphql.execute_query_lazy',
14
+ 'execute_multiplex' => 'graphql.execute_multiplex'
15
+ }
16
+
17
+ def platform_trace(platform_key, key, data)
18
+ return yield if platform_key.nil?
19
+
20
+ tracer.in_span(platform_key, attributes: attributes_for(key, data)) do |span, _context|
21
+ yield.tap do |response|
22
+ errors = response[:errors]&.compact&.map { |e| e.to_h }&.to_json if key == 'validate'
23
+ unless errors.nil?
24
+ span.add_event(
25
+ 'graphql.validation.error',
26
+ attributes: {
27
+ 'message' => errors
28
+ }
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def platform_field_key(type, field)
36
+ "#{type.graphql_name}.#{field.graphql_name}"
37
+ end
38
+
39
+ def platform_authorized_key(type)
40
+ "#{type.graphql_name}.authorized"
41
+ end
42
+
43
+ def platform_resolve_type_key(type)
44
+ "#{type.graphql_name}.resolve_type"
45
+ end
46
+
47
+ private
48
+
49
+ def tracer
50
+ OpenTelemetry::Instrumentation::GraphQL::Instrumentation.instance.tracer
51
+ end
52
+
53
+ def config
54
+ OpenTelemetry::Instrumentation::GraphQL::Instrumentation.instance.config
55
+ end
56
+
57
+ def platform_key_enabled?(ctx, key)
58
+ return false unless config[key]
59
+
60
+ ns = ctx.namespace(:opentelemetry)
61
+ return true if ns.empty? # restores original behavior so that keys are returned if tracing is not set in context.
62
+ return false unless ns.key?(key) && ns[key]
63
+
64
+ return true
65
+ end
66
+
67
+ def attributes_for(key, data)
68
+ attributes = {}
69
+ case key
70
+ when 'execute_query'
71
+ attributes['selected_operation_name'] = data[:query].selected_operation_name if data[:query].selected_operation_name
72
+ attributes['selected_operation_type'] = data[:query].selected_operation.operation_type
73
+ attributes['query_string'] = data[:query].query_string
74
+ end
75
+ attributes
76
+ end
77
+
78
+ def cached_platform_key(ctx, key, trace_phase)
79
+ cache = ctx.namespace(self.class)[:platform_key_cache] ||= {}
80
+
81
+ cache.fetch(key) do
82
+ cache[key] = if trace_phase == :field
83
+ return unless platform_key_enabled?(ctx, :enable_platform_field)
84
+
85
+ yield
86
+ elsif trace_phase == :authorized
87
+ return unless platform_key_enabled?(ctx, :enable_platform_authorized)
88
+
89
+ yield
90
+ elsif trace_phase == :resolve_type
91
+ return unless platform_key_enabled?(ctx, :enable_platform_resolve_type)
92
+
93
+ yield
94
+ else
95
+ raise "Unknown trace phase"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end