graphql 1.12.18 → 1.12.24

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.

Files changed (34) 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 +6 -2
  6. data/lib/graphql/dataloader/source.rb +30 -2
  7. data/lib/graphql/deprecation.rb +1 -5
  8. data/lib/graphql/integer_encoding_error.rb +18 -2
  9. data/lib/graphql/language/nodes.rb +10 -1
  10. data/lib/graphql/pagination/connections.rb +35 -16
  11. data/lib/graphql/query/validation_pipeline.rb +1 -1
  12. data/lib/graphql/schema/directive.rb +4 -0
  13. data/lib/graphql/schema/member/has_arguments.rb +7 -1
  14. data/lib/graphql/schema/resolver.rb +10 -0
  15. data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
  16. data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
  17. data/lib/graphql/schema/validator/exclusion_validator.rb +3 -1
  18. data/lib/graphql/schema/validator/format_validator.rb +4 -1
  19. data/lib/graphql/schema/validator/inclusion_validator.rb +3 -1
  20. data/lib/graphql/schema/validator/length_validator.rb +5 -3
  21. data/lib/graphql/schema/validator/numericality_validator.rb +7 -1
  22. data/lib/graphql/schema/validator.rb +36 -25
  23. data/lib/graphql/static_validation/error.rb +3 -1
  24. data/lib/graphql/static_validation/rules/fields_will_merge.rb +51 -25
  25. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
  26. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -0
  27. data/lib/graphql/static_validation/validation_context.rb +2 -1
  28. data/lib/graphql/string_encoding_error.rb +13 -3
  29. data/lib/graphql/subscriptions/event.rb +46 -2
  30. data/lib/graphql/types/int.rb +1 -1
  31. data/lib/graphql/types/string.rb +1 -1
  32. data/lib/graphql/unauthorized_error.rb +1 -1
  33. data/lib/graphql/version.rb +1 -1
  34. metadata +8 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d1cfa93dc97adf13e461a45a00899f5ecc3e27d66c54c33d1a319b027665b78
4
- data.tar.gz: 5d8cf2b669619dd15058400e15342ff225ba02716c5d8c9959c74260d4f51b32
3
+ metadata.gz: fee883bb45ad2391215f85a31ceec2f69b19c07c138708f002643c2fdf4c8a19
4
+ data.tar.gz: 925fb0e6f0acbb8ceddb79ad2eec4e8cfcd73bdf5cb51ad708dd1d7ebb491e2d
5
5
  SHA512:
6
- metadata.gz: d7ff04a88e0ae883c382aa6559b52c38dbbc7d513537aaf506e89dd7880c227b6b032f7329c1c9e7b0fb9692b3e0e6fdbaac9df9fc82e536fb1e12b9cff2f65c
7
- data.tar.gz: 3276674be3f8b0eb05d5c8904cad3ef20697bf5b9efcb133bd7c283d7bd704d52cd75f87a46f6c1fd988faf6c439c796d186280f4e397f8dc5184ae7d17c333b
6
+ metadata.gz: 480aba657ba456d83bc543132f730b56444068a8a14c054c5f7f98a3d7c87179d23d843cc862b4ba8388bdf020fcb3f231c4513a93c824cc7ce15e61a9160c67
7
+ data.tar.gz: 5744a84a14e922b2b26984986351cab87e41da464b985b1b28d53c7c42a921b5dd53cadca09c8a1679a2af1f7ee40dd42894e32bee373b88c8c2fe6fa917a1e9
@@ -12,7 +12,8 @@ module Graphql
12
12
  #
13
13
  # Add the Node interface with `--node`.
14
14
  class ObjectGenerator < TypeGeneratorBase
15
- desc "Create a GraphQL::ObjectType with the given name and fields"
15
+ desc "Create a GraphQL::ObjectType with the given name and fields." \
16
+ "If the given type name matches an existing ActiveRecord model, the generated type will automatically include fields for the models database columns."
16
17
  source_root File.expand_path('../templates', __FILE__)
17
18
 
18
19
  argument :custom_fields,
@@ -32,20 +32,28 @@ module Graphql
32
32
 
33
33
  # Return a string UUID for `object`
34
34
  def self.id_from_object(object, type_definition, query_ctx)
35
- # Here's a simple implementation which:
36
- # - joins the type name & object.id
37
- # - encodes it with base64:
38
- # GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id)
35
+ # For example, use Rails' GlobalID library (https://github.com/rails/globalid):
36
+ object_id = object.to_global_id.to_s
37
+ # Remove this redundant prefix to make IDs shorter:
38
+ object_id = object_id.sub("gid://\#{GlobalID.app}/", "")
39
+ encoded_id = Base64.urlsafe_encode64(object_id)
40
+ # Remove the "=" padding
41
+ encoded_id = encoded_id.sub(/=+/, "")
42
+ # Add a type hint
43
+ type_hint = type_definition.graphql_name.first
44
+ "\#{type_hint}_\#{encoded_id}"
39
45
  end
40
46
 
41
47
  # Given a string UUID, find the object
42
- def self.object_from_id(id, query_ctx)
43
- # For example, to decode the UUIDs generated above:
44
- # type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id)
45
- #
46
- # Then, based on `type_name` and `id`
47
- # find an object in your application
48
- # ...
48
+ def self.object_from_id(encoded_id_with_hint, query_ctx)
49
+ # For example, use Rails' GlobalID library (https://github.com/rails/globalid):
50
+ # Split off the type hint
51
+ _type_hint, encoded_id = encoded_id_with_hint.split("_", 2)
52
+ # Decode the ID
53
+ id = Base64.urlsafe_decode64(encoded_id)
54
+ # Rebuild it for Rails then find the object:
55
+ full_global_id = "gid://\#{GlobalID.app}/\#{id}"
56
+ GlobalID::Locator.locate(full_global_id)
49
57
  end
50
58
  RUBY
51
59
  inject_into_file schema_file_path, schema_code, before: /^end\n/m, force: false
@@ -4,11 +4,23 @@ class <%= schema_name %> < GraphQL::Schema
4
4
  <% if options[:batch] %>
5
5
  # GraphQL::Batch setup:
6
6
  use GraphQL::Batch
7
+ <% else %>
8
+ # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
9
+ use GraphQL::Dataloader
7
10
  <% end %>
11
+ # GraphQL-Ruby calls this when something goes wrong while running a query:
12
+ def self.type_error(err, context)
13
+ # if err.is_a?(GraphQL::InvalidNullError)
14
+ # # report to your bug tracker here
15
+ # return nil
16
+ # end
17
+ super
18
+ end
19
+
8
20
  # Union and Interface Resolution
9
21
  def self.resolve_type(abstract_type, obj, ctx)
10
- # TODO: Implement this function
11
- # to return the correct object type for `obj`
22
+ # TODO: Implement this method
23
+ # to return the correct GraphQL object type for `obj`
12
24
  raise(GraphQL::RequiredImplementationMissingError)
13
25
  end
14
26
  end
@@ -15,8 +15,12 @@ module GraphQL
15
15
  field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}"
16
16
  @used_fields << field
17
17
  @used_deprecated_fields << field if field_defn.deprecation_reason
18
-
19
- extract_deprecated_arguments(visitor.query.arguments_for(node, visitor.field_definition).argument_values)
18
+ arguments = visitor.query.arguments_for(node, visitor.field_definition)
19
+ # If there was an error when preparing this argument object,
20
+ # then this might be an error or something:
21
+ if arguments.respond_to?(:argument_values)
22
+ extract_deprecated_arguments(arguments.argument_values)
23
+ end
20
24
  end
21
25
 
22
26
  def result
@@ -6,7 +6,11 @@ module GraphQL
6
6
  # Called by {Dataloader} to prepare the {Source}'s internal state
7
7
  # @api private
8
8
  def setup(dataloader)
9
+ # These keys have been requested but haven't been fetched yet
9
10
  @pending_keys = []
11
+ # These keys have been passed to `fetch` but haven't been finished yet
12
+ @fetching_keys = []
13
+ # { key => result }
10
14
  @results = {}
11
15
  @dataloader = dataloader
12
16
  end
@@ -64,29 +68,46 @@ module GraphQL
64
68
  # Then run the batch and update the cache.
65
69
  # @return [void]
66
70
  def sync
71
+ pending_keys = @pending_keys.dup
67
72
  @dataloader.yield
73
+ iterations = 0
74
+ while pending_keys.any? { |k| !@results.key?(k) }
75
+ iterations += 1
76
+ if iterations > 1000
77
+ raise "#{self.class}#sync tried 1000 times to load pending keys (#{pending_keys}), but they still weren't loaded. There is likely a circular dependency."
78
+ end
79
+ @dataloader.yield
80
+ end
81
+ nil
68
82
  end
69
83
 
70
84
  # @return [Boolean] True if this source has any pending requests for data.
71
85
  def pending?
72
- @pending_keys.any?
86
+ !@pending_keys.empty?
73
87
  end
74
88
 
75
89
  # Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
76
90
  # @api private
77
91
  # @return [void]
78
92
  def run_pending_keys
93
+ if !@fetching_keys.empty?
94
+ @pending_keys -= @fetching_keys
95
+ end
79
96
  return if @pending_keys.empty?
80
97
  fetch_keys = @pending_keys.uniq
98
+ @fetching_keys.concat(fetch_keys)
81
99
  @pending_keys = []
82
100
  results = fetch(fetch_keys)
83
101
  fetch_keys.each_with_index do |key, idx|
84
102
  @results[key] = results[idx]
85
103
  end
104
+ nil
86
105
  rescue StandardError => error
87
106
  fetch_keys.each { |key| @results[key] = error }
88
107
  ensure
89
- nil
108
+ if fetch_keys
109
+ @fetching_keys -= fetch_keys
110
+ end
90
111
  end
91
112
 
92
113
  # These arguments are given to `dataloader.with(source_class, ...)`. The object
@@ -116,6 +137,13 @@ module GraphQL
116
137
  # @return [Object] The result from {#fetch} for `key`.
117
138
  # @api private
118
139
  def result_for(key)
140
+ if !@results.key?(key)
141
+ raise <<-ERR
142
+ Invariant: fetching result for a key on #{self.class} that hasn't been loaded yet (#{key.inspect}, loaded: #{@results.keys})
143
+
144
+ This key should have been loaded already. This is a bug in GraphQL::Dataloader, please report it on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new.
145
+ ERR
146
+ end
119
147
  result = @results[key]
120
148
 
121
149
  raise result if result.class <= StandardError
@@ -3,11 +3,7 @@
3
3
  module GraphQL
4
4
  module Deprecation
5
5
  def self.warn(message)
6
- if defined?(ActiveSupport::Deprecation)
7
- ActiveSupport::Deprecation.warn(message)
8
- else
9
- Kernel.warn(message)
10
- end
6
+ Kernel.warn(message)
11
7
  end
12
8
  end
13
9
  end
@@ -12,9 +12,25 @@ module GraphQL
12
12
  # The value which couldn't be encoded
13
13
  attr_reader :integer_value
14
14
 
15
- def initialize(value)
15
+ # @return [GraphQL::Schema::Field] The field that returned a too-big integer
16
+ attr_reader :field
17
+
18
+ # @return [Array<String, Integer>] Where the field appeared in the GraphQL response
19
+ attr_reader :path
20
+
21
+ def initialize(value, context:)
16
22
  @integer_value = value
17
- super("Integer out of bounds: #{value}. \nConsider using ID or GraphQL::Types::BigInt instead.")
23
+ @field = context[:current_field]
24
+ @path = context[:current_path]
25
+ message = "Integer out of bounds: #{value}".dup
26
+ if @path
27
+ message << " @ #{@path.join(".")}"
28
+ end
29
+ if @field
30
+ message << " (#{@field.path})"
31
+ end
32
+ message << ". Consider using ID or GraphQL::Types::BigInt instead."
33
+ super(message)
18
34
  end
19
35
  end
20
36
  end
@@ -197,7 +197,16 @@ module GraphQL
197
197
  else
198
198
  module_eval <<-RUBY, __FILE__, __LINE__
199
199
  def children
200
- @children ||= (#{children_of_type.keys.map { |k| "@#{k}" }.join(" + ")}).freeze
200
+ @children ||= begin
201
+ if #{children_of_type.keys.map { |k| "@#{k}.any?" }.join(" || ")}
202
+ new_children = []
203
+ #{children_of_type.keys.map { |k| "new_children.concat(@#{k})" }.join("; ")}
204
+ new_children.freeze
205
+ new_children
206
+ else
207
+ NO_CHILDREN
208
+ end
209
+ end
201
210
  end
202
211
  RUBY
203
212
  end
@@ -70,23 +70,42 @@ module GraphQL
70
70
  wrappers = context ? context.namespace(:connections)[:all_wrappers] : all_wrappers
71
71
  impl = wrapper_for(items, wrappers: wrappers)
72
72
 
73
- if impl.nil?
74
- raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})"
73
+ if impl
74
+ impl.new(
75
+ items,
76
+ context: context,
77
+ parent: parent,
78
+ field: field,
79
+ max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
80
+ first: arguments[:first],
81
+ after: arguments[:after],
82
+ last: arguments[:last],
83
+ before: arguments[:before],
84
+ arguments: arguments,
85
+ edge_class: edge_class_for_field(field),
86
+ )
87
+ else
88
+ begin
89
+ connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(items)
90
+ if parent.is_a?(GraphQL::Schema::Object)
91
+ parent = parent.object
92
+ end
93
+ connection_class.new(
94
+ items,
95
+ arguments,
96
+ field: field,
97
+ max_page_size: field.max_page_size,
98
+ parent: parent,
99
+ context: context,
100
+ )
101
+ rescue RuntimeError => err
102
+ if err.message.include?("No connection implementation to wrap")
103
+ raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})"
104
+ else
105
+ raise err
106
+ end
107
+ end
75
108
  end
76
-
77
- impl.new(
78
- items,
79
- context: context,
80
- parent: parent,
81
- field: field,
82
- max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
83
- first: arguments[:first],
84
- after: arguments[:after],
85
- last: arguments[:last],
86
- before: arguments[:before],
87
- arguments: arguments,
88
- edge_class: edge_class_for_field(field),
89
- )
90
109
  end
91
110
 
92
111
  # use an override if there is one
@@ -72,7 +72,7 @@ module GraphQL
72
72
  elsif @operation_name_error
73
73
  @validation_errors << @operation_name_error
74
74
  else
75
- validation_result = @schema.static_validator.validate(@query, validate: @validate, timeout: @schema.validate_timeout)
75
+ validation_result = @schema.static_validator.validate(@query, validate: @validate, timeout: @schema.validate_timeout, max_errors: @schema.validate_max_errors)
76
76
  @validation_errors.concat(validation_result[:errors])
77
77
  @internal_representation = validation_result[:irep]
78
78
 
@@ -116,6 +116,10 @@ module GraphQL
116
116
  @arguments = self.class.coerce_arguments(nil, arguments, Query::NullContext)
117
117
  end
118
118
 
119
+ def graphql_name
120
+ self.class.graphql_name
121
+ end
122
+
119
123
  LOCATIONS = [
120
124
  QUERY = :QUERY,
121
125
  MUTATION = :MUTATION,
@@ -254,11 +254,17 @@ module GraphQL
254
254
  if authed
255
255
  application_object
256
256
  else
257
- raise GraphQL::UnauthorizedError.new(
257
+ err = GraphQL::UnauthorizedError.new(
258
258
  object: application_object,
259
259
  type: class_based_type,
260
260
  context: context,
261
261
  )
262
+ if self.respond_to?(:unauthorized_object)
263
+ err.set_backtrace(caller)
264
+ unauthorized_object(err)
265
+ else
266
+ raise err
267
+ end
262
268
  end
263
269
  end
264
270
  else
@@ -160,6 +160,16 @@ module GraphQL
160
160
  end
161
161
  end
162
162
 
163
+ # Called when an object loaded by `loads:` fails the `.authorized?` check for its resolved GraphQL object type.
164
+ #
165
+ # By default, the error is re-raised and passed along to {{Schema.unauthorized_object}}.
166
+ #
167
+ # Any value returned here will be used _instead of_ of the loaded object.
168
+ # @param err [GraphQL::UnauthorizedError]
169
+ def unauthorized_object(err)
170
+ raise err
171
+ end
172
+
163
173
  private
164
174
 
165
175
  def load_arguments(args)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ class Validator
6
+ # Use this to specifically reject values that respond to `.blank?` and respond truthy for that method.
7
+ #
8
+ # @example Require a non-empty string for an argument
9
+ # argument :name, String, required: true, validate: { allow_blank: false }
10
+ class AllowBlankValidator < Validator
11
+ def initialize(allow_blank_positional, allow_blank: nil, message: "%{validated} can't be blank", **default_options)
12
+ @message = message
13
+ super(**default_options)
14
+ @allow_blank = allow_blank.nil? ? allow_blank_positional : allow_blank
15
+ end
16
+
17
+ def validate(_object, _context, value)
18
+ if value.respond_to?(:blank?) && value.blank?
19
+ if (value.nil? && @allow_null) || @allow_blank
20
+ # pass
21
+ else
22
+ @message
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ class Validator
6
+ # Use this to specifically reject or permit `nil` values (given as `null` from GraphQL).
7
+ #
8
+ # @example require a non-null value for an argument if it is provided
9
+ # argument :name, String, required: false, validates: { allow_null: false }
10
+ class AllowNullValidator < Validator
11
+ MESSAGE = "%{validated} can't be null"
12
+ def initialize(allow_null_positional, allow_null: nil, message: MESSAGE, **default_options)
13
+ @message = message
14
+ super(**default_options)
15
+ @allow_null = allow_null.nil? ? allow_null_positional : allow_null
16
+ end
17
+
18
+ def validate(_object, _context, value)
19
+ if value.nil? && !@allow_null
20
+ @message
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -21,7 +21,9 @@ module GraphQL
21
21
  end
22
22
 
23
23
  def validate(_object, _context, value)
24
- if @in_list.include?(value)
24
+ if permitted_empty_value?(value)
25
+ # pass
26
+ elsif @in_list.include?(value)
25
27
  @message
26
28
  end
27
29
  end
@@ -38,7 +38,10 @@ module GraphQL
38
38
  end
39
39
 
40
40
  def validate(_object, _context, value)
41
- if (@with_pattern && !value.match?(@with_pattern)) ||
41
+ if permitted_empty_value?(value)
42
+ # Do nothing
43
+ elsif value.nil? ||
44
+ (@with_pattern && !value.match?(@with_pattern)) ||
42
45
  (@without_pattern && value.match?(@without_pattern))
43
46
  @message
44
47
  end
@@ -23,7 +23,9 @@ module GraphQL
23
23
  end
24
24
 
25
25
  def validate(_object, _context, value)
26
- if !@in_list.include?(value)
26
+ if permitted_empty_value?(value)
27
+ # pass
28
+ elsif !@in_list.include?(value)
27
29
  @message
28
30
  end
29
31
  end
@@ -43,11 +43,13 @@ module GraphQL
43
43
  end
44
44
 
45
45
  def validate(_object, _context, value)
46
- if @maximum && value.length > @maximum
46
+ return if permitted_empty_value?(value) # pass in this case
47
+ length = value.nil? ? 0 : value.length
48
+ if @maximum && length > @maximum
47
49
  partial_format(@too_long, { count: @maximum })
48
- elsif @minimum && value.length < @minimum
50
+ elsif @minimum && length < @minimum
49
51
  partial_format(@too_short, { count: @minimum })
50
- elsif @is && value.length != @is
52
+ elsif @is && length != @is
51
53
  partial_format(@wrong_length, { count: @is })
52
54
  end
53
55
  end
@@ -32,6 +32,7 @@ module GraphQL
32
32
  equal_to: nil, other_than: nil,
33
33
  odd: nil, even: nil, within: nil,
34
34
  message: "%{validated} must be %{comparison} %{target}",
35
+ null_message: Validator::AllowNullValidator::MESSAGE,
35
36
  **default_options
36
37
  )
37
38
 
@@ -45,11 +46,16 @@ module GraphQL
45
46
  @even = even
46
47
  @within = within
47
48
  @message = message
49
+ @null_message = null_message
48
50
  super(**default_options)
49
51
  end
50
52
 
51
53
  def validate(object, context, value)
52
- if @greater_than && value <= @greater_than
54
+ if permitted_empty_value?(value)
55
+ # pass in this case
56
+ elsif value.nil? # @allow_null is handled in the parent class
57
+ @null_message
58
+ elsif @greater_than && value <= @greater_than
53
59
  partial_format(@message, { comparison: "greater than", target: @greater_than })
54
60
  elsif @greater_than_or_equal_to && value < @greater_than_or_equal_to
55
61
  partial_format(@message, { comparison: "greater than or equal to", target: @greater_than_or_equal_to })
@@ -7,7 +7,6 @@ module GraphQL
7
7
  # @return [GraphQL::Schema::Argument, GraphQL::Schema::Field, GraphQL::Schema::Resolver, Class<GraphQL::Schema::InputObject>]
8
8
  attr_reader :validated
9
9
 
10
- # TODO should this implement `if:` and `unless:` ?
11
10
  # @param validated [GraphQL::Schema::Argument, GraphQL::Schema::Field, GraphQL::Schema::Resolver, Class<GraphQL::Schema::InputObject>] The argument or argument owner this validator is attached to
12
11
  # @param allow_blank [Boolean] if `true`, then objects that respond to `.blank?` and return true for `.blank?` will skip this validation
13
12
  # @param allow_null [Boolean] if `true`, then incoming `null`s will skip this validation
@@ -25,26 +24,6 @@ module GraphQL
25
24
  raise GraphQL::RequiredImplementationMissingError, "Validator classes should implement #validate"
26
25
  end
27
26
 
28
- # This is called by the validation system and eventually calls {#validate}.
29
- # @api private
30
- def apply(object, context, value)
31
- if value.nil?
32
- if @allow_null
33
- nil # skip this
34
- else
35
- "%{validated} can't be null"
36
- end
37
- elsif value.respond_to?(:blank?) && value.blank?
38
- if @allow_blank
39
- nil # skip this
40
- else
41
- "%{validated} can't be blank"
42
- end
43
- else
44
- validate(object, context, value)
45
- end
46
- end
47
-
48
27
  # This is like `String#%`, but it supports the case that only some of `string`'s
49
28
  # values are present in `substitutions`
50
29
  def partial_format(string, substitutions)
@@ -55,6 +34,12 @@ module GraphQL
55
34
  string
56
35
  end
57
36
 
37
+ # @return [Boolean] `true` if `value` is `nil` and this validator has `allow_null: true` or if value is `.blank?` and this validator has `allow_blank: true`
38
+ def permitted_empty_value?(value)
39
+ (value.nil? && @allow_null) ||
40
+ (@allow_blank && value.respond_to?(:blank?) && value.blank?)
41
+ end
42
+
58
43
  # @param schema_member [GraphQL::Schema::Field, GraphQL::Schema::Argument, Class<GraphQL::Schema::InputObject>]
59
44
  # @param validates_hash [Hash{Symbol => Hash}, Hash{Class => Hash} nil] A configuration passed as `validates:`
60
45
  # @return [Array<Validator>]
@@ -62,6 +47,24 @@ module GraphQL
62
47
  if validates_hash.nil? || validates_hash.empty?
63
48
  EMPTY_ARRAY
64
49
  else
50
+ validates_hash = validates_hash.dup
51
+ allow_null = validates_hash.delete(:allow_null)
52
+ allow_blank = validates_hash.delete(:allow_blank)
53
+
54
+ # This could be {...}.compact on Ruby 2.4+
55
+ default_options = {}
56
+ if !allow_null.nil?
57
+ default_options[:allow_null] = allow_null
58
+ end
59
+ if !allow_blank.nil?
60
+ default_options[:allow_blank] = allow_blank
61
+ end
62
+
63
+ # allow_nil or allow_blank are the _only_ validations:
64
+ if validates_hash.empty?
65
+ validates_hash = default_options
66
+ end
67
+
65
68
  validates_hash.map do |validator_name, options|
66
69
  validator_class = case validator_name
67
70
  when Class
@@ -69,7 +72,11 @@ module GraphQL
69
72
  else
70
73
  all_validators[validator_name] || raise(ArgumentError, "unknown validation: #{validator_name.inspect}")
71
74
  end
72
- validator_class.new(validated: schema_member, **options)
75
+ if options.is_a?(Hash)
76
+ validator_class.new(validated: schema_member, **(default_options.merge(options)))
77
+ else
78
+ validator_class.new(options, validated: schema_member, **default_options)
79
+ end
73
80
  end
74
81
  end
75
82
  end
@@ -122,10 +129,10 @@ module GraphQL
122
129
 
123
130
  validators.each do |validator|
124
131
  validated = as || validator.validated
125
- errors = validator.apply(object, context, value)
132
+ errors = validator.validate(object, context, value)
126
133
  if errors &&
127
- (errors.is_a?(Array) && errors != EMPTY_ARRAY) ||
128
- (errors.is_a?(String))
134
+ (errors.is_a?(Array) && errors != EMPTY_ARRAY) ||
135
+ (errors.is_a?(String))
129
136
  if all_errors.frozen? # It's empty
130
137
  all_errors = []
131
138
  end
@@ -161,3 +168,7 @@ require "graphql/schema/validator/exclusion_validator"
161
168
  GraphQL::Schema::Validator.install(:exclusion, GraphQL::Schema::Validator::ExclusionValidator)
162
169
  require "graphql/schema/validator/required_validator"
163
170
  GraphQL::Schema::Validator.install(:required, GraphQL::Schema::Validator::RequiredValidator)
171
+ require "graphql/schema/validator/allow_null_validator"
172
+ GraphQL::Schema::Validator.install(:allow_null, GraphQL::Schema::Validator::AllowNullValidator)
173
+ require "graphql/schema/validator/allow_blank_validator"
174
+ GraphQL::Schema::Validator.install(:allow_blank, GraphQL::Schema::Validator::AllowBlankValidator)
@@ -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,24 +18,47 @@ 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
+ # don't initialize these if they weren't initialized in the block:
54
+ @field_conflicts && @field_conflicts.each_value { |error| add_error(error) }
55
+ @arg_conflicts && @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
 
37
- fields, fragment_spreads = fields_and_fragments_from_selection(node, owner_type: parent_type, parents: [])
61
+ fields, fragment_spreads = fields_and_fragments_from_selection(node, owner_type: parent_type, parents: nil)
38
62
 
39
63
  # (A) Find find all conflicts "within" the fields of this selection set.
40
64
  find_conflicts_within(fields)
@@ -174,15 +198,21 @@ module GraphQL
174
198
  response_keys.each do |key, fields|
175
199
  next if fields.size < 2
176
200
  # find conflicts within nodes
177
- for i in 0..fields.size - 1
178
- for j in i + 1..fields.size - 1
201
+ i = 0
202
+ while i < fields.size
203
+ j = i + 1
204
+ while j < fields.size
179
205
  find_conflict(key, fields[i], fields[j])
206
+ j += 1
180
207
  end
208
+ i += 1
181
209
  end
182
210
  end
183
211
  end
184
212
 
185
213
  def find_conflict(response_key, field1, field2, mutually_exclusive: false)
214
+ return if @conflict_count >= context.max_errors
215
+
186
216
  node1 = field1.node
187
217
  node2 = field2.node
188
218
 
@@ -191,28 +221,21 @@ module GraphQL
191
221
 
192
222
  if !are_mutually_exclusive
193
223
  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
- add_error(GraphQL::StaticValidation::FieldsWillMergeError.new(
197
- msg,
198
- nodes: [node1, node2],
199
- path: [],
200
- field_name: response_key,
201
- conflicts: errored_nodes
202
- ))
224
+ conflict = field_conflicts[response_key]
225
+
226
+ conflict.add_conflict(node1, node1.name)
227
+ conflict.add_conflict(node2, node2.name)
228
+
229
+ @conflict_count += 1
203
230
  end
204
231
 
205
232
  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
- add_error(GraphQL::StaticValidation::FieldsWillMergeError.new(
210
- msg,
211
- nodes: [node1, node2],
212
- path: [],
213
- field_name: response_key,
214
- conflicts: conflicts
215
- ))
233
+ conflict = arg_conflicts[response_key]
234
+
235
+ conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1)))
236
+ conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2)))
237
+
238
+ @conflict_count += 1
216
239
  end
217
240
  end
218
241
 
@@ -224,7 +247,9 @@ module GraphQL
224
247
  end
225
248
 
226
249
  def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
227
- return if field1.definition.nil? || field2.definition.nil?
250
+ return if field1.definition.nil? ||
251
+ field2.definition.nil? ||
252
+ (field1.node.selections.empty? && field2.node.selections.empty?)
228
253
 
229
254
  return_type1 = field1.definition.type.unwrap
230
255
  return_type2 = field2.definition.type.unwrap
@@ -304,6 +329,7 @@ module GraphQL
304
329
  if node.selections.empty?
305
330
  NO_SELECTIONS
306
331
  else
332
+ parents ||= []
307
333
  fields, fragment_spreads = find_fields_and_fragments(node.selections, owner_type: owner_type, parents: parents, fields: [], fragment_spreads: [])
308
334
  response_keys = fields.group_by { |f| f.node.alias || f.node.name }
309
335
  [response_keys, fragment_spreads]
@@ -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
@@ -16,6 +16,8 @@ module GraphQL
16
16
  private
17
17
 
18
18
  def assert_required_args(ast_node, defn)
19
+ args = defn.arguments
20
+ return if args.empty?
19
21
  present_argument_names = ast_node.arguments.map(&:name)
20
22
  required_argument_names = defn.arguments.each_value
21
23
  .select { |a| a.type.kind.non_null? && !a.default_value? && context.warden.get_argument(defn, a.name) }
@@ -15,7 +15,8 @@ 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
 
@@ -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
@@ -53,6 +53,36 @@ 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)
57
87
  arg_owner = arg_owner.respond_to?(:unwrap) ? arg_owner.unwrap : arg_owner # remove list and non-null wrappers
58
88
  case args
@@ -65,12 +95,26 @@ module GraphQL
65
95
 
66
96
  if arg_defn
67
97
  normalized_arg_name = camelized_arg_name
68
- next_args[normalized_arg_name] = stringify_args(arg_defn.type, v)
69
98
  else
70
99
  normalized_arg_name = arg_name
71
100
  arg_defn = get_arg_definition(arg_owner, normalized_arg_name)
72
101
  end
73
- 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
74
118
  end
75
119
  # Make sure they're deeply sorted
76
120
  next_args.sort.to_h
@@ -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.18"
3
+ VERSION = "1.12.24"
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.18
4
+ version: 1.12.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-02 00:00:00.000000000 Z
11
+ date: 2022-02-04 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
@@ -685,7 +687,7 @@ metadata:
685
687
  source_code_uri: https://github.com/rmosolgo/graphql-ruby
686
688
  bug_tracker_uri: https://github.com/rmosolgo/graphql-ruby/issues
687
689
  mailing_list_uri: https://tinyletter.com/graphql-ruby
688
- post_install_message:
690
+ post_install_message:
689
691
  rdoc_options: []
690
692
  require_paths:
691
693
  - lib
@@ -700,8 +702,8 @@ 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.22
704
- signing_key:
705
+ rubygems_version: 3.1.6
706
+ signing_key:
705
707
  specification_version: 4
706
708
  summary: A GraphQL language and runtime for Ruby
707
709
  test_files: []