graphql 1.10.3 → 1.10.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe0e22214a3e9457d34ff1cd9aa506abec6431d6dad443f87d297a8c88bbfb0d
4
- data.tar.gz: 194b1ce3ee16b95b880f7488ea7cfa44a977330f7d218d591a6a2d076aed049e
3
+ metadata.gz: abb84b7f4f94ae78b7d23d8726590780465d0018a3b225fb9b0cefbd557e6c1b
4
+ data.tar.gz: 78222841f7996f8449081d272169e6bd7e120f5937a08ea04a33c04217ff21dd
5
5
  SHA512:
6
- metadata.gz: 58a7a7149f8f36e42689aa746ecc18d2567ca8fde473dd152c785f26207d750cbc8f1e770d59fa4fdca87cb9d05948afa89a783419fc2accaeacdd29e5a36ab9
7
- data.tar.gz: 59baab3d2e8c20050ac3378b5f9749e4989e28086609717c406134e299bf7804ff0207c8d67646e36ac7fd572db1cf6b8b6cca62a88e0133cfd7f5071ab0c046
6
+ metadata.gz: d935f76873acb5bf0f15f9fa7c6d74a1a8ac5268d4885216698b206c66cda923b5cc269f90cf708ac296bf5d2973f9e45fb52558b5223cc3c345ae2650933e0c
7
+ data.tar.gz: 601450b86d9d6e9f9445060c24ba0dc74ce88ab2d69b2551b5e8836b4f02d35a0794e75e32c6db60eaefffc22878c87b07162c77a65b8ea1b5e241d6262881ca
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "graphql/execution/interpreter/arguments_cache"
2
3
  require "graphql/execution/interpreter/execution_errors"
3
4
  require "graphql/execution/interpreter/hash_response"
4
5
  require "graphql/execution/interpreter/runtime"
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Execution
5
+ class Interpreter
6
+ class ArgumentsCache
7
+ def initialize(query)
8
+ @query = query
9
+ @storage = Hash.new do |h, ast_node|
10
+ h[ast_node] = Hash.new do |h2, arg_owner|
11
+ h2[arg_owner] = Hash.new do |h3, parent_object|
12
+ # First, normalize all AST or Ruby values to a plain Ruby hash
13
+ args_hash = prepare_args_hash(ast_node)
14
+ # Then call into the schema to coerce those incoming values
15
+ arg_owner.coerce_arguments(parent_object, args_hash, query.context)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def fetch(ast_node, argument_owner, parent_object)
22
+ @storage[ast_node][argument_owner][parent_object]
23
+ end
24
+
25
+ private
26
+
27
+ NO_VALUE_GIVEN = Object.new
28
+
29
+ def prepare_args_hash(ast_arg_or_hash_or_value)
30
+ case ast_arg_or_hash_or_value
31
+ when Hash
32
+ args_hash = {}
33
+ ast_arg_or_hash_or_value.each do |k, v|
34
+ args_hash[k] = prepare_args_hash(v)
35
+ end
36
+ args_hash
37
+ when Array
38
+ ast_arg_or_hash_or_value.map { |v| prepare_args_hash(v) }
39
+ when GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::InputObject, GraphQL::Language::Nodes::Directive
40
+ args_hash = {}
41
+ ast_arg_or_hash_or_value.arguments.each do |arg|
42
+ v = prepare_args_hash(arg.value)
43
+ if v != NO_VALUE_GIVEN
44
+ args_hash[arg.name] = v
45
+ end
46
+ end
47
+ args_hash
48
+ when GraphQL::Language::Nodes::VariableIdentifier
49
+ if @query.variables.key?(ast_arg_or_hash_or_value.name)
50
+ variable_value = @query.variables[ast_arg_or_hash_or_value.name]
51
+ prepare_args_hash(variable_value)
52
+ else
53
+ NO_VALUE_GIVEN
54
+ end
55
+ when GraphQL::Language::Nodes::Enum
56
+ ast_arg_or_hash_or_value.name
57
+ when GraphQL::Language::Nodes::NullValue
58
+ nil
59
+ else
60
+ ast_arg_or_hash_or_value
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -436,48 +436,8 @@ module GraphQL
436
436
  end
437
437
  end
438
438
 
439
- NO_VALUE_GIVEN = Object.new
440
-
441
- def prepare_args_hash(ast_arg_or_hash_or_value)
442
- case ast_arg_or_hash_or_value
443
- when Hash
444
- args_hash = {}
445
- ast_arg_or_hash_or_value.each do |k, v|
446
- args_hash[k] = prepare_args_hash(v)
447
- end
448
- args_hash
449
- when Array
450
- ast_arg_or_hash_or_value.map { |v| prepare_args_hash(v) }
451
- when GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::InputObject, GraphQL::Language::Nodes::Directive
452
- args_hash = {}
453
- ast_arg_or_hash_or_value.arguments.each do |arg|
454
- v = prepare_args_hash(arg.value)
455
- if v != NO_VALUE_GIVEN
456
- args_hash[arg.name] = v
457
- end
458
- end
459
- args_hash
460
- when GraphQL::Language::Nodes::VariableIdentifier
461
- if query.variables.key?(ast_arg_or_hash_or_value.name)
462
- variable_value = query.variables[ast_arg_or_hash_or_value.name]
463
- prepare_args_hash(variable_value)
464
- else
465
- NO_VALUE_GIVEN
466
- end
467
- when GraphQL::Language::Nodes::Enum
468
- ast_arg_or_hash_or_value.name
469
- when GraphQL::Language::Nodes::NullValue
470
- nil
471
- else
472
- ast_arg_or_hash_or_value
473
- end
474
- end
475
-
476
- def arguments(graphql_object, arg_owner, ast_node_or_hash)
477
- # First, normalize all AST or Ruby values to a plain Ruby hash
478
- args_hash = prepare_args_hash(ast_node_or_hash)
479
- # Then call into the schema to coerce those incoming values
480
- arg_owner.coerce_arguments(graphql_object, args_hash, context)
439
+ def arguments(graphql_object, arg_owner, ast_node)
440
+ query.arguments_for(ast_node, arg_owner, parent_object: graphql_object)
481
441
  end
482
442
 
483
443
  def write_invalid_null_in_response(path, invalid_null_error)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/language/block_string"
3
3
  require "graphql/language/printer"
4
+ require "graphql/language/sanitized_printer"
4
5
  require "graphql/language/document_from_schema_definition"
5
6
  require "graphql/language/generation"
6
7
  require "graphql/language/lexer"
@@ -488,6 +488,8 @@ module GraphQL
488
488
 
489
489
  # @!attribute name
490
490
  # @return [String] The identifier for this variable, _without_ `$`
491
+
492
+ self.children_method_name = :variables
491
493
  end
492
494
 
493
495
  # A query, mutation or subscription.
@@ -93,7 +93,7 @@ module GraphQL
93
93
  end
94
94
 
95
95
  def print_input_object(input_object)
96
- "{#{input_object.arguments.map { |a| "#{a.name}: #{print(a.value)}" }.join(", ")}}"
96
+ "{#{input_object.arguments.map { |a| print_argument(a) }.join(", ")}}"
97
97
  end
98
98
 
99
99
  def print_list_type(list_type)
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Language
4
+ # A custom printer used to print sanitized queries. It inlines provided variables
5
+ # within the query for facilitate logging and analysis of queries.
6
+ #
7
+ # The printer returns `nil` if the query is invalid.
8
+ #
9
+ # Since the GraphQL Ruby AST for a GraphQL query doesnt contain any reference
10
+ # on the type of fields or arguments, we have to track the current object, field
11
+ # and input type while printing the query.
12
+ #
13
+ # @example Printing a scrubbed string
14
+ # printer = QueryPrinter.new(query)
15
+ # puts printer.sanitized_query_string
16
+ #
17
+ # @see {Query#sanitized_query_string}
18
+ class SanitizedPrinter < GraphQL::Language::Printer
19
+
20
+ REDACTED = "\"<REDACTED>\""
21
+
22
+ def initialize(query)
23
+ @query = query
24
+ @current_type = nil
25
+ @current_field = nil
26
+ @current_input_type = nil
27
+ end
28
+
29
+ # @return [String, nil] A scrubbed query string, if the query was valid.
30
+ def sanitized_query_string
31
+ if query.valid?
32
+ print(query.document)
33
+ else
34
+ nil
35
+ end
36
+ end
37
+
38
+ def print_node(node, indent: "")
39
+ if node.is_a?(String)
40
+ type = @current_input_type.unwrap
41
+ # Replace any strings that aren't IDs or Enum values with REDACTED
42
+ if type.kind.enum? || type.graphql_name == "ID"
43
+ super
44
+ else
45
+ REDACTED
46
+ end
47
+ elsif node.is_a?(Array)
48
+ old_input_type = @current_input_type
49
+ if @current_input_type && @current_input_type.list?
50
+ @current_input_type = @current_input_type.of_type
51
+ @current_input_type = @current_input_type.of_type if @current_input_type.non_null?
52
+ end
53
+
54
+ res = super
55
+ @current_input_type = old_input_type
56
+ res
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def print_argument(argument)
63
+ arg_owner = @current_input_type || @current_directive || @current_field
64
+ arg_def = arg_owner.arguments[argument.name]
65
+
66
+ old_input_type = @current_input_type
67
+ @current_input_type = arg_def.type.non_null? ? arg_def.type.of_type : arg_def.type
68
+ res = super
69
+ @current_input_type = old_input_type
70
+ res
71
+ end
72
+
73
+ def print_list_type(list_type)
74
+ old_input_type = @current_input_type
75
+ @current_input_type = old_input_type.of_type
76
+ res = super
77
+ @current_input_type = old_input_type
78
+ res
79
+ end
80
+
81
+ def print_variable_identifier(variable_id)
82
+ variable_value = query.variables[variable_id.name]
83
+ print_node(value_to_ast(variable_value, @current_input_type))
84
+ end
85
+
86
+ def print_field(field, indent: "")
87
+ @current_field = query.schema.get_field(@current_type, field.name)
88
+ old_type = @current_type
89
+ @current_type = @current_field.type.unwrap
90
+ res = super
91
+ @current_type = old_type
92
+ res
93
+ end
94
+
95
+ def print_inline_fragment(inline_fragment, indent: "")
96
+ old_type = @current_type
97
+
98
+ if inline_fragment.type
99
+ @current_type = query.schema.types[inline_fragment.type.name]
100
+ end
101
+
102
+ res = super
103
+
104
+ @current_type = old_type
105
+
106
+ res
107
+ end
108
+
109
+ def print_fragment_definition(fragment_def, indent: "")
110
+ old_type = @current_type
111
+ @current_type = query.schema.types[fragment_def.type.name]
112
+
113
+ res = super
114
+
115
+ @current_type = old_type
116
+
117
+ res
118
+ end
119
+
120
+ def print_directive(directive)
121
+ @current_directive = query.schema.directives[directive.name]
122
+
123
+ res = super
124
+
125
+ @current_directive = nil
126
+ res
127
+ end
128
+
129
+ # Print the operation definition but do not include the variable
130
+ # definitions since we will inline them within the query
131
+ def print_operation_definition(operation_definition, indent: "")
132
+ old_type = @current_type
133
+ @current_type = query.schema.public_send(operation_definition.operation_type)
134
+
135
+ out = "#{indent}#{operation_definition.operation_type}".dup
136
+ out << " #{operation_definition.name}" if operation_definition.name
137
+ out << print_directives(operation_definition.directives)
138
+ out << print_selections(operation_definition.selections, indent: indent)
139
+
140
+ @current_type = old_type
141
+ out
142
+ end
143
+
144
+ private
145
+
146
+ def value_to_ast(value, type)
147
+ type = type.of_type if type.non_null?
148
+
149
+ if value.nil?
150
+ return GraphQL::Language::Nodes::NullValue.new(name: "null")
151
+ end
152
+
153
+ case type.kind.name
154
+ when "INPUT_OBJECT"
155
+ value = if value.respond_to?(:to_unsafe_h)
156
+ # for ActionController::Parameters
157
+ value.to_unsafe_h
158
+ else
159
+ value.to_h
160
+ end
161
+
162
+ arguments = value.map do |key, val|
163
+ sub_type = type.arguments[key.to_s].type
164
+
165
+ GraphQL::Language::Nodes::Argument.new(
166
+ name: key.to_s,
167
+ value: value_to_ast(val, sub_type)
168
+ )
169
+ end
170
+ GraphQL::Language::Nodes::InputObject.new(
171
+ arguments: arguments
172
+ )
173
+ when "LIST"
174
+ if value.respond_to?(:each)
175
+ value.each { |v| value_to_ast(v, type.of_type) }
176
+ else
177
+ [value].each { |v| value_to_ast(v, type.of_type) }
178
+ end
179
+ when "ENUM"
180
+ GraphQL::Language::Nodes::Enum.new(name: value)
181
+ else
182
+ value
183
+ end
184
+ end
185
+
186
+ attr_reader :query
187
+ end
188
+ end
189
+ end
@@ -124,8 +124,6 @@ module GraphQL
124
124
  end
125
125
  end
126
126
 
127
- @arguments_cache = ArgumentsCache.build(self)
128
-
129
127
  # Trying to execute a document
130
128
  # with no operations returns an empty hash
131
129
  @ast_variables = []
@@ -243,10 +241,28 @@ module GraphQL
243
241
  end
244
242
 
245
243
  # Node-level cache for calculating arguments. Used during execution and query analysis.
246
- # @api private
247
- # @return [GraphQL::Query::Arguments] Arguments for this node, merging default values, literal values and query variables
248
- def arguments_for(irep_or_ast_node, definition)
249
- @arguments_cache[irep_or_ast_node][definition]
244
+ # @param ast_node [GraphQL::Language::Nodes::AbstractNode]
245
+ # @param definition [GraphQL::Schema::Field]
246
+ # @param parent_object [GraphQL::Schema::Object]
247
+ # @return Hash{Symbol => Object}
248
+ def arguments_for(ast_node, definition, parent_object: nil)
249
+ if interpreter?
250
+ @arguments_cache ||= Execution::Interpreter::ArgumentsCache.new(self)
251
+ @arguments_cache.fetch(ast_node, definition, parent_object)
252
+ else
253
+ @arguments_cache ||= ArgumentsCache.build(self)
254
+ @arguments_cache[ast_node][definition]
255
+ end
256
+ end
257
+
258
+ # A version of the given query string, with:
259
+ # - Variables inlined to the query
260
+ # - Strings replaced with `<REDACTED>`
261
+ # @return [String, nil] Returns nil if the query is invalid.
262
+ def sanitized_query_string
263
+ with_prepared_ast {
264
+ GraphQL::Language::SanitizedPrinter.new(self).sanitized_query_string
265
+ }
250
266
  end
251
267
 
252
268
  def validation_pipeline
@@ -82,6 +82,12 @@ module GraphQL
82
82
  include GraphQL::Define::InstanceDefinable
83
83
  extend GraphQL::Schema::FindInheritedValue
84
84
 
85
+ class DuplicateTypeNamesError < GraphQL::Error
86
+ def initialize(type_name:, first_definition:, second_definition:, path:)
87
+ 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(".")}")
88
+ end
89
+ end
90
+
85
91
  class UnresolvedLateBoundTypeError < GraphQL::Error
86
92
  attr_reader :type
87
93
  def initialize(type:)
@@ -715,14 +721,20 @@ module GraphQL
715
721
  # @param using [Hash] Plugins to attach to the created schema with `use(key, value)`
716
722
  # @param interpreter [Boolean] If false, the legacy {Execution::Execute} runtime will be used
717
723
  # @return [Class] the schema described by `document`
718
- def self.from_definition(definition_or_path, default_resolve: BuildFromDefinition::DefaultResolve, interpreter: true, parser: BuildFromDefinition::DefaultParser, using: {})
724
+ def self.from_definition(definition_or_path, default_resolve: nil, interpreter: true, parser: BuildFromDefinition::DefaultParser, using: {})
719
725
  # If the file ends in `.graphql`, treat it like a filepath
720
726
  definition = if definition_or_path.end_with?(".graphql")
721
727
  File.read(definition_or_path)
722
728
  else
723
729
  definition_or_path
724
730
  end
725
- GraphQL::Schema::BuildFromDefinition.from_definition(definition, default_resolve: default_resolve, parser: parser, using: using, interpreter: interpreter)
731
+ GraphQL::Schema::BuildFromDefinition.from_definition(
732
+ definition,
733
+ default_resolve: default_resolve,
734
+ parser: parser,
735
+ using: using,
736
+ interpreter: interpreter,
737
+ )
726
738
  end
727
739
 
728
740
  # Error that is raised when [#Schema#from_definition] is passed an invalid schema definition string.
@@ -1686,7 +1698,7 @@ module GraphQL
1686
1698
  end
1687
1699
  late_types = []
1688
1700
  new_types = Array(t)
1689
- new_types.each { |t| add_type(t, owner: nil, late_types: late_types) }
1701
+ new_types.each { |t| add_type(t, owner: nil, late_types: late_types, path: [t.graphql_name]) }
1690
1702
  missed_late_types = 0
1691
1703
  while (late_type_vals = late_types.shift)
1692
1704
  type_owner, lt = late_type_vals
@@ -1695,13 +1707,13 @@ module GraphQL
1695
1707
  # Reset the counter, since we might succeed next go-round
1696
1708
  missed_late_types = 0
1697
1709
  update_type_owner(type_owner, type)
1698
- add_type(type, owner: type_owner, late_types: late_types)
1710
+ add_type(type, owner: type_owner, late_types: late_types, path: [type.graphql_name])
1699
1711
  elsif lt.is_a?(LateBoundType)
1700
1712
  if (type = get_type(lt.graphql_name))
1701
1713
  # Reset the counter, since we might succeed next go-round
1702
1714
  missed_late_types = 0
1703
1715
  update_type_owner(type_owner, type)
1704
- add_type(type, owner: type_owner, late_types: late_types)
1716
+ add_type(type, owner: type_owner, late_types: late_types, path: [type.graphql_name])
1705
1717
  else
1706
1718
  missed_late_types += 1
1707
1719
  # Add it back to the list, maybe we'll be able to resolve it later.
@@ -1776,7 +1788,7 @@ module GraphQL
1776
1788
  end
1777
1789
  end
1778
1790
 
1779
- def add_type(type, owner:, late_types:)
1791
+ def add_type(type, owner:, late_types:, path:)
1780
1792
  if type.respond_to?(:metadata) && type.metadata.is_a?(Hash)
1781
1793
  type_class = type.metadata[:type_class]
1782
1794
  if type_class.nil?
@@ -1796,46 +1808,52 @@ module GraphQL
1796
1808
 
1797
1809
  if (prev_type = own_types[type.graphql_name])
1798
1810
  if prev_type != type
1799
- raise ArgumentError, "Conflicting type definitions for `#{type.graphql_name}`: #{prev_type} (#{prev_type.class}), #{type} #{type.class}"
1811
+ raise DuplicateTypeNamesError.new(
1812
+ type_name: type.graphql_name,
1813
+ first_definition: prev_type,
1814
+ second_definition: type,
1815
+ path: path,
1816
+ )
1800
1817
  else
1801
1818
  # This type was already added
1802
1819
  end
1803
1820
  elsif type.is_a?(Class) && type < GraphQL::Schema::Directive
1804
- type.arguments.each do |_name, arg|
1821
+ type.arguments.each do |name, arg|
1805
1822
  arg_type = arg.type.unwrap
1806
1823
  references_to(arg_type, from: arg)
1807
- add_type(arg_type, owner: arg, late_types: late_types)
1824
+ add_type(arg_type, owner: arg, late_types: late_types, path: path + [name])
1808
1825
  end
1809
1826
  else
1810
1827
  own_types[type.graphql_name] = type
1811
1828
  if type.kind.fields?
1812
- type.fields.each do |_name, field|
1829
+ type.fields.each do |name, field|
1813
1830
  field_type = field.type.unwrap
1814
1831
  references_to(field_type, from: field)
1815
- add_type(field_type, owner: field, late_types: late_types)
1816
- field.arguments.each do |_name, arg|
1832
+ field_path = path + [name]
1833
+ add_type(field_type, owner: field, late_types: late_types, path: field_path)
1834
+ field.arguments.each do |arg_name, arg|
1817
1835
  arg_type = arg.type.unwrap
1818
1836
  references_to(arg_type, from: arg)
1819
- add_type(arg_type, owner: arg, late_types: late_types)
1837
+ add_type(arg_type, owner: arg, late_types: late_types, path: field_path + [arg_name])
1820
1838
  end
1821
1839
  end
1822
1840
  end
1823
1841
  if type.kind.input_object?
1824
- type.arguments.each do |_name, arg|
1842
+ type.arguments.each do |arg_name, arg|
1825
1843
  arg_type = arg.type.unwrap
1826
1844
  references_to(arg_type, from: arg)
1827
- add_type(arg_type, owner: arg, late_types: late_types)
1845
+ add_type(arg_type, owner: arg, late_types: late_types, path: path + [arg_name])
1828
1846
  end
1829
1847
  end
1830
1848
  if type.kind.union?
1831
1849
  own_possible_types[type.graphql_name] = type.possible_types
1832
1850
  type.possible_types.each do |t|
1833
- add_type(t, owner: type, late_types: late_types)
1851
+ add_type(t, owner: type, late_types: late_types, path: path + ["possible_types"])
1834
1852
  end
1835
1853
  end
1836
1854
  if type.kind.interface?
1837
1855
  type.orphan_types.each do |t|
1838
- add_type(t, owner: type, late_types: late_types)
1856
+ add_type(t, owner: type, late_types: late_types, path: path + ["orphan_types"])
1839
1857
  end
1840
1858
  end
1841
1859
  if type.kind.object?
@@ -1843,7 +1861,7 @@ module GraphQL
1843
1861
  type.interfaces.each do |i|
1844
1862
  implementers = own_possible_types[i.graphql_name] ||= []
1845
1863
  implementers << type
1846
- add_type(i, owner: type, late_types: late_types)
1864
+ add_type(i, owner: type, late_types: late_types, path: path + ["implements"])
1847
1865
  end
1848
1866
  end
1849
1867
  end
@@ -55,7 +55,7 @@ module GraphQL
55
55
  @owner = owner
56
56
  @as = as
57
57
  @loads = loads
58
- @keyword = as || Schema::Member::BuildType.underscore(@name).to_sym
58
+ @keyword = as || (arg_name.is_a?(Symbol) ? arg_name : Schema::Member::BuildType.underscore(@name).to_sym)
59
59
  @prepare = prepare
60
60
  @ast_node = ast_node
61
61
  @from_resolver = from_resolver
@@ -5,31 +5,22 @@ module GraphQL
5
5
  class Schema
6
6
  module BuildFromDefinition
7
7
  class << self
8
- def from_definition(definition_string, default_resolve:, using: {}, interpreter: true, parser: DefaultParser)
8
+ # @see {Schema.from_definition}
9
+ def from_definition(definition_string, default_resolve:, using: {}, relay: false, interpreter: true, parser: DefaultParser)
9
10
  document = parser.parse(definition_string)
10
- Builder.build(document, default_resolve: default_resolve, using: using, interpreter: interpreter)
11
+ default_resolve ||= {}
12
+ Builder.build(document, default_resolve: default_resolve, relay: relay, using: using, interpreter: interpreter)
11
13
  end
12
14
  end
13
15
 
14
16
  # @api private
15
17
  DefaultParser = GraphQL::Language::Parser
16
18
 
17
- # @api private
18
- module DefaultResolve
19
- def self.call(type, field, obj, args, ctx)
20
- if field.arguments.any?
21
- obj.public_send(field.name, args, ctx)
22
- else
23
- obj.public_send(field.name)
24
- end
25
- end
26
- end
27
-
28
19
  # @api private
29
20
  module Builder
30
21
  extend self
31
22
 
32
- def build(document, default_resolve: DefaultResolve, using: {}, interpreter: true)
23
+ def build(document, default_resolve:, using: {}, interpreter: true, relay:)
33
24
  raise InvalidDocumentError.new('Must provide a document ast.') if !document || !document.is_a?(GraphQL::Language::Nodes::Document)
34
25
 
35
26
  if default_resolve.is_a?(Hash)
@@ -316,6 +307,7 @@ module GraphQL
316
307
  type: type_resolver.call(field_definition.type),
317
308
  null: true,
318
309
  connection: type_name.end_with?("Connection"),
310
+ connection_extension: nil,
319
311
  deprecation_reason: build_deprecation_reason(field_definition.directives),
320
312
  ast_node: field_definition,
321
313
  method_conflict_warning: false,
@@ -172,6 +172,7 @@ module GraphQL
172
172
  # @param hash_key [String, Symbol] The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
173
173
  # @param resolver_method [Symbol] The method on the type to call to resolve this field (defaults to `name`)
174
174
  # @param connection [Boolean] `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
175
+ # @param connection_extension [Class] The extension to add, to implement connections. If `nil`, no extension is added.
175
176
  # @param max_page_size [Integer] For connections, the maximum number of items to return from this field
176
177
  # @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__`
177
178
  # @param resolve [<#call(obj, args, ctx)>] **deprecated** for compatibility with <1.8.0
@@ -187,7 +188,7 @@ module GraphQL
187
188
  # @param trace [Boolean] If true, a {GraphQL::Tracing} tracer will measure this scalar field
188
189
  # @param ast_node [Language::Nodes::FieldDefinition, nil] If this schema was parsed from definition, this AST node defined the field
189
190
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
190
- def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: nil, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: [], extensions: EMPTY_ARRAY, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, arguments: EMPTY_HASH, &definition_block)
191
+ def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: nil, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: [], 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, arguments: EMPTY_HASH, &definition_block)
191
192
  if name.nil?
192
193
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
193
194
  end
@@ -275,8 +276,8 @@ module GraphQL
275
276
  end
276
277
  # The problem with putting this after the definition_block
277
278
  # is that it would override arguments
278
- if connection?
279
- self.extension(self.class.connection_extension)
279
+ if connection? && connection_extension
280
+ self.extension(connection_extension)
280
281
  end
281
282
 
282
283
  if definition_block
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/tracing/active_support_notifications_tracing"
3
3
  require "graphql/tracing/platform_tracing"
4
+ require "graphql/tracing/appoptics_tracing"
4
5
  require "graphql/tracing/appsignal_tracing"
5
6
  require "graphql/tracing/data_dog_tracing"
6
7
  require "graphql/tracing/new_relic_tracing"
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+
6
+ # This class uses the AppopticsAPM SDK from the appoptics_apm gem to create
7
+ # traces for GraphQL.
8
+ #
9
+ # There are 4 configurations available. They can be set in the
10
+ # appoptics_apm config file or in code. Please see:
11
+ # {https://docs.appoptics.com/kb/apm_tracing/ruby/configure}
12
+ #
13
+ # AppOpticsAPM::Config[:graphql][:enabled] = true|false
14
+ # AppOpticsAPM::Config[:graphql][:transaction_name] = true|false
15
+ # AppOpticsAPM::Config[:graphql][:sanitize_query] = true|false
16
+ # AppOpticsAPM::Config[:graphql][:remove_comments] = true|false
17
+ class AppOpticsTracing < GraphQL::Tracing::PlatformTracing
18
+ # These GraphQL events will show up as 'graphql.prep' spans
19
+ PREP_KEYS = ['lex', 'parse', 'validate', 'analyze_query', 'analyze_multiplex'].freeze
20
+ # These GraphQL events will show up as 'graphql.execute' spans
21
+ EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze
22
+
23
+ # During auto-instrumentation this version of AppOpticsTracing is compared
24
+ # with the version provided in the appoptics_apm gem, so that the newer
25
+ # version of the class can be used
26
+
27
+ def self.version
28
+ Gem::Version.new('1.0.0')
29
+ end
30
+
31
+ self.platform_keys = {
32
+ 'lex' => 'lex',
33
+ 'parse' => 'parse',
34
+ 'validate' => 'validate',
35
+ 'analyze_query' => 'analyze_query',
36
+ 'analyze_multiplex' => 'analyze_multiplex',
37
+ 'execute_multiplex' => 'execute_multiplex',
38
+ 'execute_query' => 'execute_query',
39
+ 'execute_query_lazy' => 'execute_query_lazy'
40
+ }
41
+
42
+ def platform_trace(platform_key, _key, data)
43
+ return yield if !defined?(AppOpticsAPM) || gql_config[:enabled] == false
44
+
45
+ layer = span_name(platform_key)
46
+ kvs = metadata(data, layer)
47
+ kvs[:Key] = platform_key if (PREP_KEYS + EXEC_KEYS).include?(platform_key)
48
+
49
+ transaction_name(kvs[:InboundQuery]) if kvs[:InboundQuery] && layer == 'graphql.execute'
50
+
51
+ ::AppOpticsAPM::SDK.trace(layer, kvs) do
52
+ kvs.clear # we don't have to send them twice
53
+ yield
54
+ end
55
+ end
56
+
57
+ def platform_field_key(type, field)
58
+ "graphql.#{type.name}.#{field.name}"
59
+ end
60
+
61
+ private
62
+
63
+ def gql_config
64
+ ::AppOpticsAPM::Config[:graphql] ||= {}
65
+ end
66
+
67
+ def transaction_name(query)
68
+ return if gql_config[:transaction_name] == false ||
69
+ ::AppOpticsAPM::SDK.get_transaction_name
70
+
71
+ split_query = query.strip.split(/\W+/, 3)
72
+ split_query[0] = 'query' if split_query[0].empty?
73
+ name = "graphql.#{split_query[0..1].join('.')}"
74
+
75
+ ::AppOpticsAPM::SDK.set_transaction_name(name)
76
+ end
77
+
78
+ def multiplex_transaction_name(names)
79
+ return if gql_config[:transaction_name] == false ||
80
+ ::AppOpticsAPM::SDK.get_transaction_name
81
+
82
+ name = "graphql.multiplex.#{names.join('.')}"
83
+ name = "#{name[0..251]}..." if name.length > 254
84
+
85
+ ::AppOpticsAPM::SDK.set_transaction_name(name)
86
+ end
87
+
88
+ def span_name(key)
89
+ return 'graphql.prep' if PREP_KEYS.include?(key)
90
+ return 'graphql.execute' if EXEC_KEYS.include?(key)
91
+
92
+ key[/^graphql\./] ? key : "graphql.#{key}"
93
+ end
94
+
95
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
96
+ def metadata(data, layer)
97
+ data.keys.map do |key|
98
+ case key
99
+ when :context
100
+ graphql_context(data[key], layer)
101
+ when :query
102
+ graphql_query(data[key])
103
+ when :query_string
104
+ graphql_query_string(data[key])
105
+ when :multiplex
106
+ graphql_multiplex(data[key])
107
+ else
108
+ [key, data[key]]
109
+ end
110
+ end.flatten.each_slice(2).to_h.merge(Spec: 'graphql')
111
+ end
112
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
113
+
114
+ def graphql_context(context, layer)
115
+ context.errors && context.errors.each do |err|
116
+ AppOpticsAPM::API.log_exception(layer, err)
117
+ end
118
+
119
+ [[:Path, context.path.join('.')]]
120
+ end
121
+
122
+ def graphql_query(query)
123
+ return [] unless query
124
+
125
+ query_string = query.query_string
126
+ query_string = remove_comments(query_string) if gql_config[:remove_comments] != false
127
+ query_string = sanitize(query_string) if gql_config[:sanitize_query] != false
128
+
129
+ [[:InboundQuery, query_string],
130
+ [:Operation, query.selected_operation_name]]
131
+ end
132
+
133
+ def graphql_query_string(query_string)
134
+ query_string = remove_comments(query_string) if gql_config[:remove_comments] != false
135
+ query_string = sanitize(query_string) if gql_config[:sanitize_query] != false
136
+
137
+ [:InboundQuery, query_string]
138
+ end
139
+
140
+ def graphql_multiplex(data)
141
+ names = data.queries.map(&:operations).map(&:keys).flatten.compact
142
+ multiplex_transaction_name(names) if names.size > 1
143
+
144
+ [:Operations, names.join(', ')]
145
+ end
146
+
147
+ def sanitize(query)
148
+ return unless query
149
+
150
+ # remove arguments
151
+ query.gsub(/"[^"]*"/, '"?"') # strings
152
+ .gsub(/-?[0-9]*\.?[0-9]+e?[0-9]*/, '?') # ints + floats
153
+ .gsub(/\[[^\]]*\]/, '[?]') # arrays
154
+ end
155
+
156
+ def remove_comments(query)
157
+ return unless query
158
+
159
+ query.gsub(/#[^\n\r]*/, '')
160
+ end
161
+ end
162
+ end
163
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.10.3"
3
+ VERSION = "1.10.4"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.3
4
+ version: 1.10.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-17 00:00:00.000000000 Z
11
+ date: 2020-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -422,6 +422,7 @@ files:
422
422
  - lib/graphql/execution/flatten.rb
423
423
  - lib/graphql/execution/instrumentation.rb
424
424
  - lib/graphql/execution/interpreter.rb
425
+ - lib/graphql/execution/interpreter/arguments_cache.rb
425
426
  - lib/graphql/execution/interpreter/execution_errors.rb
426
427
  - lib/graphql/execution/interpreter/handles_raw_value.rb
427
428
  - lib/graphql/execution/interpreter/hash_response.rb
@@ -477,6 +478,7 @@ files:
477
478
  - lib/graphql/language/parser.rb
478
479
  - lib/graphql/language/parser.y
479
480
  - lib/graphql/language/printer.rb
481
+ - lib/graphql/language/sanitized_printer.rb
480
482
  - lib/graphql/language/token.rb
481
483
  - lib/graphql/language/visitor.rb
482
484
  - lib/graphql/list_type.rb
@@ -682,6 +684,7 @@ files:
682
684
  - lib/graphql/subscriptions/subscription_root.rb
683
685
  - lib/graphql/tracing.rb
684
686
  - lib/graphql/tracing/active_support_notifications_tracing.rb
687
+ - lib/graphql/tracing/appoptics_tracing.rb
685
688
  - lib/graphql/tracing/appsignal_tracing.rb
686
689
  - lib/graphql/tracing/data_dog_tracing.rb
687
690
  - lib/graphql/tracing/new_relic_tracing.rb
@@ -743,7 +746,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
743
746
  - !ruby/object:Gem::Version
744
747
  version: '0'
745
748
  requirements: []
746
- rubygems_version: 3.1.2
749
+ rubygems_version: 3.0.3
747
750
  signing_key:
748
751
  specification_version: 4
749
752
  summary: A GraphQL language and runtime for Ruby