graphql 1.10.3 → 1.10.4

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.
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