graphql 1.12.17 → 1.12.21

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/object_generator.rb +2 -1
  3. data/lib/generators/graphql/relay.rb +19 -11
  4. data/lib/generators/graphql/templates/schema.erb +14 -2
  5. data/lib/graphql/analysis/ast/field_usage.rb +1 -1
  6. data/lib/graphql/dataloader/source.rb +32 -2
  7. data/lib/graphql/dataloader.rb +13 -0
  8. data/lib/graphql/deprecated_dsl.rb +11 -3
  9. data/lib/graphql/deprecation.rb +1 -5
  10. data/lib/graphql/integer_encoding_error.rb +18 -2
  11. data/lib/graphql/pagination/connections.rb +35 -16
  12. data/lib/graphql/query/validation_pipeline.rb +1 -1
  13. data/lib/graphql/schema/argument.rb +55 -26
  14. data/lib/graphql/schema/field.rb +14 -4
  15. data/lib/graphql/schema/input_object.rb +1 -5
  16. data/lib/graphql/schema/member/has_arguments.rb +90 -44
  17. data/lib/graphql/schema/resolver.rb +20 -57
  18. data/lib/graphql/schema/subscription.rb +4 -4
  19. data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
  20. data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
  21. data/lib/graphql/schema/validator/exclusion_validator.rb +3 -1
  22. data/lib/graphql/schema/validator/format_validator.rb +4 -1
  23. data/lib/graphql/schema/validator/inclusion_validator.rb +3 -1
  24. data/lib/graphql/schema/validator/length_validator.rb +5 -3
  25. data/lib/graphql/schema/validator/numericality_validator.rb +7 -1
  26. data/lib/graphql/schema/validator.rb +36 -25
  27. data/lib/graphql/schema.rb +18 -5
  28. data/lib/graphql/static_validation/base_visitor.rb +3 -0
  29. data/lib/graphql/static_validation/error.rb +3 -1
  30. data/lib/graphql/static_validation/rules/fields_will_merge.rb +40 -21
  31. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
  32. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
  33. data/lib/graphql/static_validation/validation_context.rb +8 -2
  34. data/lib/graphql/static_validation/validator.rb +15 -12
  35. data/lib/graphql/string_encoding_error.rb +13 -3
  36. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +7 -1
  37. data/lib/graphql/subscriptions/event.rb +47 -2
  38. data/lib/graphql/subscriptions/serialize.rb +1 -1
  39. data/lib/graphql/tracing/appsignal_tracing.rb +15 -0
  40. data/lib/graphql/types/int.rb +1 -1
  41. data/lib/graphql/types/string.rb +1 -1
  42. data/lib/graphql/unauthorized_error.rb +1 -1
  43. data/lib/graphql/version.rb +1 -1
  44. metadata +5 -3
@@ -205,6 +205,9 @@ module GraphQL
205
205
  private
206
206
 
207
207
  def add_error(error, path: nil)
208
+ if @context.too_many_errors?
209
+ throw :too_many_validation_errors
210
+ end
208
211
  error.path ||= (path || @path.dup)
209
212
  context.errors << error
210
213
  end
@@ -32,8 +32,10 @@ module GraphQL
32
32
 
33
33
  private
34
34
 
35
+ attr_reader :nodes
36
+
35
37
  def locations
36
- @nodes.map do |node|
38
+ nodes.map do |node|
37
39
  h = {"line" => node.line, "column" => node.col}
38
40
  h["filename"] = node.filename if node.filename
39
41
  h
@@ -10,6 +10,7 @@ module GraphQL
10
10
  #
11
11
  # Original Algorithm: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMerged.js
12
12
  NO_ARGS = {}.freeze
13
+
13
14
  Field = Struct.new(:node, :definition, :owner_type, :parents)
14
15
  FragmentSpread = Struct.new(:name, :parents)
15
16
 
@@ -17,20 +18,43 @@ module GraphQL
17
18
  super
18
19
  @visited_fragments = {}
19
20
  @compared_fragments = {}
21
+ @conflict_count = 0
20
22
  end
21
23
 
22
24
  def on_operation_definition(node, _parent)
23
- conflicts_within_selection_set(node, type_definition)
25
+ setting_errors { conflicts_within_selection_set(node, type_definition) }
24
26
  super
25
27
  end
26
28
 
27
29
  def on_field(node, _parent)
28
- conflicts_within_selection_set(node, type_definition)
30
+ setting_errors { conflicts_within_selection_set(node, type_definition) }
29
31
  super
30
32
  end
31
33
 
32
34
  private
33
35
 
36
+ def field_conflicts
37
+ @field_conflicts ||= Hash.new do |errors, field|
38
+ errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :field, field_name: field)
39
+ end
40
+ end
41
+
42
+ def arg_conflicts
43
+ @arg_conflicts ||= Hash.new do |errors, field|
44
+ errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :argument, field_name: field)
45
+ end
46
+ end
47
+
48
+ def setting_errors
49
+ @field_conflicts = nil
50
+ @arg_conflicts = nil
51
+
52
+ yield
53
+
54
+ field_conflicts.each_value { |error| add_error(error) }
55
+ arg_conflicts.each_value { |error| add_error(error) }
56
+ end
57
+
34
58
  def conflicts_within_selection_set(node, parent_type)
35
59
  return if parent_type.nil?
36
60
 
@@ -183,6 +207,8 @@ module GraphQL
183
207
  end
184
208
 
185
209
  def find_conflict(response_key, field1, field2, mutually_exclusive: false)
210
+ return if @conflict_count >= context.max_errors
211
+
186
212
  node1 = field1.node
187
213
  node2 = field2.node
188
214
 
@@ -191,28 +217,21 @@ module GraphQL
191
217
 
192
218
  if !are_mutually_exclusive
193
219
  if node1.name != node2.name
194
- errored_nodes = [node1.name, node2.name].sort.join(" or ")
195
- msg = "Field '#{response_key}' has a field conflict: #{errored_nodes}?"
196
- context.errors << GraphQL::StaticValidation::FieldsWillMergeError.new(
197
- msg,
198
- nodes: [node1, node2],
199
- path: [],
200
- field_name: response_key,
201
- conflicts: errored_nodes
202
- )
220
+ conflict = field_conflicts[response_key]
221
+
222
+ conflict.add_conflict(node1, node1.name)
223
+ conflict.add_conflict(node2, node2.name)
224
+
225
+ @conflict_count += 1
203
226
  end
204
227
 
205
228
  if !same_arguments?(node1, node2)
206
- args = [serialize_field_args(node1), serialize_field_args(node2)]
207
- conflicts = args.map { |arg| GraphQL::Language.serialize(arg) }.join(" or ")
208
- msg = "Field '#{response_key}' has an argument conflict: #{conflicts}?"
209
- context.errors << GraphQL::StaticValidation::FieldsWillMergeError.new(
210
- msg,
211
- nodes: [node1, node2],
212
- path: [],
213
- field_name: response_key,
214
- conflicts: conflicts
215
- )
229
+ conflict = arg_conflicts[response_key]
230
+
231
+ conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1)))
232
+ conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2)))
233
+
234
+ @conflict_count += 1
216
235
  end
217
236
  end
218
237
 
@@ -3,12 +3,33 @@ module GraphQL
3
3
  module StaticValidation
4
4
  class FieldsWillMergeError < StaticValidation::Error
5
5
  attr_reader :field_name
6
- attr_reader :conflicts
6
+ attr_reader :kind
7
+
8
+ def initialize(kind:, field_name:)
9
+ super(nil)
7
10
 
8
- def initialize(message, path: nil, nodes: [], field_name:, conflicts:)
9
- super(message, path: path, nodes: nodes)
10
11
  @field_name = field_name
11
- @conflicts = conflicts
12
+ @kind = kind
13
+ @conflicts = []
14
+ end
15
+
16
+ def message
17
+ "Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?"
18
+ end
19
+
20
+ def path
21
+ []
22
+ end
23
+
24
+ def conflicts
25
+ @conflicts.join(' or ')
26
+ end
27
+
28
+ def add_conflict(node, conflict_str)
29
+ return if nodes.include?(node)
30
+
31
+ @nodes << node
32
+ @conflicts << conflict_str
12
33
  end
13
34
 
14
35
  # A hash representation of this Message
@@ -7,12 +7,12 @@ module GraphQL
7
7
  dependency_map = context.dependencies
8
8
  dependency_map.cyclical_definitions.each do |defn|
9
9
  if defn.node.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
10
- context.errors << GraphQL::StaticValidation::FragmentsAreFiniteError.new(
10
+ add_error(GraphQL::StaticValidation::FragmentsAreFiniteError.new(
11
11
  "Fragment #{defn.name} contains an infinite loop",
12
12
  nodes: defn.node,
13
13
  path: defn.path,
14
14
  name: defn.name
15
- )
15
+ ))
16
16
  end
17
17
  end
18
18
  end
@@ -15,14 +15,16 @@ module GraphQL
15
15
  extend Forwardable
16
16
 
17
17
  attr_reader :query, :errors, :visitor,
18
- :on_dependency_resolve_handlers
18
+ :on_dependency_resolve_handlers,
19
+ :max_errors
19
20
 
20
21
  def_delegators :@query, :schema, :document, :fragments, :operations, :warden
21
22
 
22
- def initialize(query, visitor_class)
23
+ def initialize(query, visitor_class, max_errors)
23
24
  @query = query
24
25
  @literal_validator = LiteralValidator.new(context: query.context)
25
26
  @errors = []
27
+ @max_errors = max_errors || Float::INFINITY
26
28
  @on_dependency_resolve_handlers = []
27
29
  @visitor = visitor_class.new(document, self)
28
30
  end
@@ -38,6 +40,10 @@ module GraphQL
38
40
  def validate_literal(ast_value, type)
39
41
  @literal_validator.validate(ast_value, type)
40
42
  end
43
+
44
+ def too_many_errors?
45
+ @errors.length >= @max_errors
46
+ end
41
47
  end
42
48
  end
43
49
  end
@@ -24,8 +24,9 @@ module GraphQL
24
24
  # @param query [GraphQL::Query]
25
25
  # @param validate [Boolean]
26
26
  # @param timeout [Float] Number of seconds to wait before aborting validation. Any positive number may be used, including Floats to specify fractional seconds.
27
+ # @param max_errors [Integer] Maximum number of errors before aborting validation. Any positive number will limit the number of errors. Defaults to nil for no limit.
27
28
  # @return [Array<Hash>]
28
- def validate(query, validate: true, timeout: nil)
29
+ def validate(query, validate: true, timeout: nil, max_errors: nil)
29
30
  query.trace("validate", { validate: validate, query: query }) do
30
31
  can_skip_rewrite = query.context.interpreter? && query.schema.using_ast_analysis? && query.schema.is_a?(Class)
31
32
  errors = if validate == false && can_skip_rewrite
@@ -34,25 +35,27 @@ module GraphQL
34
35
  rules_to_use = validate ? @rules : []
35
36
  visitor_class = BaseVisitor.including_rules(rules_to_use, rewrite: !can_skip_rewrite)
36
37
 
37
- context = GraphQL::StaticValidation::ValidationContext.new(query, visitor_class)
38
+ context = GraphQL::StaticValidation::ValidationContext.new(query, visitor_class, max_errors)
38
39
 
39
40
  begin
40
41
  # CAUTION: Usage of the timeout module makes the assumption that validation rules are stateless Ruby code that requires no cleanup if process was interrupted. This means no blocking IO calls, native gems, locks, or `rescue` clauses that must be reached.
41
42
  # A timeout value of 0 or nil will execute the block without any timeout.
42
43
  Timeout::timeout(timeout) do
43
- # Attach legacy-style rules.
44
- # Only loop through rules if it has legacy-style rules
45
- unless (legacy_rules = rules_to_use - GraphQL::StaticValidation::ALL_RULES).empty?
46
- legacy_rules.each do |rule_class_or_module|
47
- if rule_class_or_module.method_defined?(:validate)
48
- GraphQL::Deprecation.warn "Legacy validator rules will be removed from GraphQL-Ruby 2.0, use a module instead (see the built-in rules: https://github.com/rmosolgo/graphql-ruby/tree/master/lib/graphql/static_validation/rules)"
49
- GraphQL::Deprecation.warn " -> Legacy validator: #{rule_class_or_module}"
50
- rule_class_or_module.new.validate(context)
44
+ catch(:too_many_validation_errors) do
45
+ # Attach legacy-style rules.
46
+ # Only loop through rules if it has legacy-style rules
47
+ unless (legacy_rules = rules_to_use - GraphQL::StaticValidation::ALL_RULES).empty?
48
+ legacy_rules.each do |rule_class_or_module|
49
+ if rule_class_or_module.method_defined?(:validate)
50
+ GraphQL::Deprecation.warn "Legacy validator rules will be removed from GraphQL-Ruby 2.0, use a module instead (see the built-in rules: https://github.com/rmosolgo/graphql-ruby/tree/master/lib/graphql/static_validation/rules)"
51
+ GraphQL::Deprecation.warn " -> Legacy validator: #{rule_class_or_module}"
52
+ rule_class_or_module.new.validate(context)
53
+ end
51
54
  end
52
55
  end
53
- end
54
56
 
55
- context.visitor.visit
57
+ context.visitor.visit
58
+ end
56
59
  end
57
60
  rescue Timeout::Error
58
61
  handle_timeout(query, context)
@@ -1,10 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
3
  class StringEncodingError < GraphQL::RuntimeTypeError
4
- attr_reader :string
5
- def initialize(str)
4
+ attr_reader :string, :field, :path
5
+ def initialize(str, context:)
6
6
  @string = str
7
- super("String \"#{str}\" was encoded as #{str.encoding}! GraphQL requires an encoding compatible with UTF-8.")
7
+ @field = context[:current_field]
8
+ @path = context[:current_path]
9
+ message = "String #{str.inspect} was encoded as #{str.encoding}".dup
10
+ if @path
11
+ message << " @ #{@path.join(".")}"
12
+ end
13
+ if @field
14
+ message << " (#{@field.path})"
15
+ end
16
+ message << ". GraphQL requires an encoding compatible with UTF-8."
17
+ super(message)
8
18
  end
9
19
  end
10
20
  end
@@ -127,7 +127,13 @@ module GraphQL
127
127
  # It will receive notifications when events come in
128
128
  # and re-evaluate the query locally.
129
129
  def write_subscription(query, events)
130
- channel = query.context.fetch(:channel)
130
+ unless (channel = query.context[:channel])
131
+ raise GraphQL::Error, "This GraphQL Subscription client does not support the transport protocol expected"\
132
+ "by the backend Subscription Server implementation (graphql-ruby ActionCableSubscriptions in this case)."\
133
+ "Some official client implementation including Apollo (https://graphql-ruby.org/javascript_client/apollo_subscriptions.html), "\
134
+ "Relay Modern (https://graphql-ruby.org/javascript_client/relay_subscriptions.html#actioncable)."\
135
+ "GraphiQL via `graphiql-rails` may not work out of box (#1051)."
136
+ end
131
137
  subscription_id = query.context[:subscription_id] ||= build_id
132
138
  stream = stream_subscription_name(subscription_id)
133
139
  channel.stream_from(stream)
@@ -53,7 +53,38 @@ module GraphQL
53
53
 
54
54
  class << self
55
55
  private
56
+
57
+ # This method does not support cyclic references in the Hash,
58
+ # nor does it support Hashes whose keys are not sortable
59
+ # with respect to their peers ( cases where a <=> b might throw an error )
60
+ def deep_sort_hash_keys(hash_to_sort)
61
+ raise ArgumentError.new("Argument must be a Hash") unless hash_to_sort.is_a?(Hash)
62
+ hash_to_sort.keys.sort.map do |k|
63
+ if hash_to_sort[k].is_a?(Hash)
64
+ [k, deep_sort_hash_keys(hash_to_sort[k])]
65
+ elsif hash_to_sort[k].is_a?(Array)
66
+ [k, deep_sort_array_hashes(hash_to_sort[k])]
67
+ else
68
+ [k, hash_to_sort[k]]
69
+ end
70
+ end.to_h
71
+ end
72
+
73
+ def deep_sort_array_hashes(array_to_inspect)
74
+ raise ArgumentError.new("Argument must be an Array") unless array_to_inspect.is_a?(Array)
75
+ array_to_inspect.map do |v|
76
+ if v.is_a?(Hash)
77
+ deep_sort_hash_keys(v)
78
+ elsif v.is_a?(Array)
79
+ deep_sort_array_hashes(v)
80
+ else
81
+ v
82
+ end
83
+ end
84
+ end
85
+
56
86
  def stringify_args(arg_owner, args)
87
+ arg_owner = arg_owner.respond_to?(:unwrap) ? arg_owner.unwrap : arg_owner # remove list and non-null wrappers
57
88
  case args
58
89
  when Hash
59
90
  next_args = {}
@@ -68,8 +99,22 @@ module GraphQL
68
99
  normalized_arg_name = arg_name
69
100
  arg_defn = get_arg_definition(arg_owner, normalized_arg_name)
70
101
  end
71
-
72
- next_args[normalized_arg_name] = stringify_args(arg_defn.type, v)
102
+ arg_base_type = arg_defn.type.unwrap
103
+ # In the case where the value being emitted is seen as a "JSON"
104
+ # type, treat the value as one atomic unit of serialization
105
+ is_json_definition = arg_base_type && arg_base_type <= GraphQL::Types::JSON
106
+ if is_json_definition
107
+ sorted_value = if v.is_a?(Hash)
108
+ deep_sort_hash_keys(v)
109
+ elsif v.is_a?(Array)
110
+ deep_sort_array_hashes(v)
111
+ else
112
+ v
113
+ end
114
+ next_args[normalized_arg_name] = sorted_value.respond_to?(:to_json) ? sorted_value.to_json : sorted_value
115
+ else
116
+ next_args[normalized_arg_name] = stringify_args(arg_base_type, v)
117
+ end
73
118
  end
74
119
  # Make sure they're deeply sorted
75
120
  next_args.sort.to_h
@@ -9,7 +9,7 @@ module GraphQL
9
9
  SYMBOL_KEY = "__sym__"
10
10
  SYMBOL_KEYS_KEY = "__sym_keys__"
11
11
  TIMESTAMP_KEY = "__timestamp__"
12
- TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%Z" # eg '2020-01-01 23:59:59.123456789+05:00'
12
+ TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%z" # eg '2020-01-01 23:59:59.123456789+05:00'
13
13
  OPEN_STRUCT_KEY = "__ostruct__"
14
14
 
15
15
  module_function
@@ -14,7 +14,22 @@ module GraphQL
14
14
  "execute_query_lazy" => "execute.graphql",
15
15
  }
16
16
 
17
+ # @param set_action_name [Boolean] If true, the GraphQL operation name will be used as the transaction name.
18
+ # This is not advised if you run more than one query per HTTP request, for example, with `graphql-client` or multiplexing.
19
+ # It can also be specified per-query with `context[:set_appsignal_action_name]`.
20
+ def initialize(options = {})
21
+ @set_action_name = options.fetch(:set_action_name, false)
22
+ super
23
+ end
24
+
17
25
  def platform_trace(platform_key, key, data)
26
+ if key == "execute_query"
27
+ set_this_txn_name = data[:query].context[:set_appsignal_action_name]
28
+ if set_this_txn_name == true || (set_this_txn_name.nil? && @set_action_name)
29
+ Appsignal::Transaction.current.set_action(transaction_name(data[:query]))
30
+ end
31
+ end
32
+
18
33
  Appsignal.instrument(platform_key) do
19
34
  yield
20
35
  end
@@ -25,7 +25,7 @@ module GraphQL
25
25
  if value >= MIN && value <= MAX
26
26
  value
27
27
  else
28
- err = GraphQL::IntegerEncodingError.new(value)
28
+ err = GraphQL::IntegerEncodingError.new(value, context: ctx)
29
29
  ctx.schema.type_error(err, ctx)
30
30
  end
31
31
  end
@@ -15,7 +15,7 @@ module GraphQL
15
15
  str.encode!(Encoding::UTF_8)
16
16
  end
17
17
  rescue EncodingError
18
- err = GraphQL::StringEncodingError.new(str)
18
+ err = GraphQL::StringEncodingError.new(str, context: ctx)
19
19
  ctx.schema.type_error(err, ctx)
20
20
  end
21
21
 
@@ -12,7 +12,7 @@ module GraphQL
12
12
  attr_reader :type
13
13
 
14
14
  # @return [GraphQL::Query::Context] the context for the current query
15
- attr_reader :context
15
+ attr_accessor :context
16
16
 
17
17
  def initialize(message = nil, object: nil, type: nil, context: nil)
18
18
  if message.nil? && object.nil? && type.nil?
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.12.17"
3
+ VERSION = "1.12.21"
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.12.17
4
+ version: 1.12.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-15 00:00:00.000000000 Z
11
+ date: 2021-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -544,6 +544,8 @@ files:
544
544
  - lib/graphql/schema/unique_within_type.rb
545
545
  - lib/graphql/schema/validation.rb
546
546
  - lib/graphql/schema/validator.rb
547
+ - lib/graphql/schema/validator/allow_blank_validator.rb
548
+ - lib/graphql/schema/validator/allow_null_validator.rb
547
549
  - lib/graphql/schema/validator/exclusion_validator.rb
548
550
  - lib/graphql/schema/validator/format_validator.rb
549
551
  - lib/graphql/schema/validator/inclusion_validator.rb
@@ -700,7 +702,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
700
702
  - !ruby/object:Gem::Version
701
703
  version: '0'
702
704
  requirements: []
703
- rubygems_version: 3.2.15
705
+ rubygems_version: 3.2.22
704
706
  signing_key:
705
707
  specification_version: 4
706
708
  summary: A GraphQL language and runtime for Ruby