graphql 1.13.2 → 1.13.3

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 381cd8cc5f2508805ccc69172566e28577652372b04e44b236ceaf903db1622f
4
- data.tar.gz: 56c03d754890c85d6a6c7d5df6a50874f3986a1284f2666a256ea51d90028a82
3
+ metadata.gz: b2638039468d2557513228b7bda7e148a91c02b818090fee27ddc92c9c3ea88a
4
+ data.tar.gz: 7ae7b8ba26ec64075dd226e3be092cd1216c4b137ce8935e8972e66df3c09c35
5
5
  SHA512:
6
- metadata.gz: 31f7ab242f8b5efa5f2e77a9c79ab3c4a6b3b9d319dcdc4215d676009f472dc59a1fdf3303a3af86ab557c5b7f53ff2610d30dc36a58eb0937962ebe1ce388f9
7
- data.tar.gz: 8c6295f3cdae55803aad1bb45abb97ec1f6a5b76d60f2150e449c5e8244619096c9462317358a048b9d5045a0246cfd2354b96d6c4de49968995e86dd6b2aafa
6
+ metadata.gz: 54c9be19ab4e29e59af43c251fc661b4d9ae47208eab4461361aef3f460f54422cef1215f4311bb6e10d18571629ea176b46aae78b9bdb9181670dcd72a0fa1d
7
+ data.tar.gz: b2c3ec10e2600b39d555946d9ba88092eba38f8dd9b7cfd3836ab5341ec59878a1df3d1d295d9889692ed32c9e6e07e1bf307928f8f3d8a199380709070ced4c
@@ -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
@@ -196,10 +196,14 @@ module GraphQL
196
196
  when "INPUT_OBJECT"
197
197
  GraphQL::Language::Nodes::InputObject.new(
198
198
  arguments: default_value.to_h.map do |arg_name, arg_value|
199
- arg_type = @warden.arguments(type).find { |a| a.graphql_name == arg_name.to_s }.type
199
+ args = @warden.arguments(type)
200
+ arg = args.find { |a| a.keyword.to_s == arg_name.to_s }
201
+ if arg.nil?
202
+ raise ArgumentError, "No argument definition on #{type.graphql_name} for argument: #{arg_name.inspect} (expected one of: #{args.map(&:keyword)})"
203
+ end
200
204
  GraphQL::Language::Nodes::Argument.new(
201
- name: arg_name.to_s,
202
- value: build_default_value(arg_value, arg_type)
205
+ name: arg.graphql_name.to_s,
206
+ value: build_default_value(arg_value, arg.type)
203
207
  )
204
208
  end
205
209
  )
@@ -341,7 +341,7 @@ module GraphQL
341
341
  # end
342
342
  # end
343
343
  #
344
- # document.to_query_string(printer: VariableSrubber.new)
344
+ # document.to_query_string(printer: VariableScrubber.new)
345
345
  #
346
346
  class Document < AbstractNode
347
347
  scalar_methods false
@@ -7,13 +7,18 @@ module GraphQL
7
7
  class ActiveRecordRelationConnection < Pagination::RelationConnection
8
8
  private
9
9
 
10
- def relation_larger_than(relation, size)
11
- initial_offset = relation.offset_value || 0
12
- relation.offset(initial_offset + size).exists?
10
+ def relation_larger_than(relation, initial_offset, size)
11
+ if already_loaded?(relation)
12
+ (relation.size + initial_offset) > size
13
+ else
14
+ set_offset(sliced_nodes, initial_offset + size).exists?
15
+ end
13
16
  end
14
17
 
15
18
  def relation_count(relation)
16
- int_or_hash = if relation.respond_to?(:unscope)
19
+ int_or_hash = if already_loaded?(relation)
20
+ relation.size
21
+ elsif relation.respond_to?(:unscope)
17
22
  relation.unscope(:order).count(:all)
18
23
  else
19
24
  # Rails 3
@@ -28,11 +33,19 @@ module GraphQL
28
33
  end
29
34
 
30
35
  def relation_limit(relation)
31
- relation.limit_value
36
+ if relation.is_a?(Array)
37
+ nil
38
+ else
39
+ relation.limit_value
40
+ end
32
41
  end
33
42
 
34
43
  def relation_offset(relation)
35
- relation.offset_value
44
+ if relation.is_a?(Array)
45
+ nil
46
+ else
47
+ relation.offset_value
48
+ end
36
49
  end
37
50
 
38
51
  def null_relation(relation)
@@ -43,6 +56,30 @@ module GraphQL
43
56
  relation.where("1=2")
44
57
  end
45
58
  end
59
+
60
+ def set_limit(nodes, limit)
61
+ if already_loaded?(nodes)
62
+ nodes.take(limit)
63
+ else
64
+ super
65
+ end
66
+ end
67
+
68
+ def set_offset(nodes, offset)
69
+ if already_loaded?(nodes)
70
+ # If the client sent a bogus cursor beyond the size of the relation,
71
+ # it might get `nil` from `#[...]`, so return an empty array in that case
72
+ nodes[offset..-1] || []
73
+ else
74
+ super
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def already_loaded?(relation)
81
+ relation.is_a?(Array) || relation.loaded?
82
+ end
46
83
  end
47
84
  end
48
85
  end
@@ -35,7 +35,7 @@ module GraphQL
35
35
  if @nodes && @nodes.count < first
36
36
  false
37
37
  else
38
- relation_larger_than(sliced_nodes, first)
38
+ relation_larger_than(sliced_nodes, @sliced_nodes_offset, first)
39
39
  end
40
40
  else
41
41
  false
@@ -54,9 +54,10 @@ module GraphQL
54
54
  private
55
55
 
56
56
  # @param relation [Object] A database query object
57
+ # @param _initial_offset [Integer] The number of items already excluded from the relation
57
58
  # @param size [Integer] The value against which we check the relation size
58
59
  # @return [Boolean] True if the number of items in this relation is larger than `size`
59
- def relation_larger_than(relation, size)
60
+ def relation_larger_than(relation, _initial_offset, size)
60
61
  relation_count(set_limit(relation, size + 1)) == size + 1
61
62
  end
62
63
 
@@ -111,30 +112,51 @@ module GraphQL
111
112
  end
112
113
  end
113
114
 
114
- # Apply `before` and `after` to the underlying `items`,
115
- # returning a new relation.
116
- def sliced_nodes
117
- @sliced_nodes ||= begin
118
- paginated_nodes = items
119
-
115
+ def calculate_sliced_nodes_parameters
116
+ if defined?(@sliced_nodes_limit)
117
+ return
118
+ else
120
119
  if after_offset
121
120
  previous_offset = relation_offset(items) || 0
122
- paginated_nodes = set_offset(paginated_nodes, previous_offset + after_offset)
121
+ relation_offset = previous_offset + after_offset
123
122
  end
124
123
 
125
124
  if before_offset && after_offset
126
125
  if after_offset < before_offset
127
126
  # Get the number of items between the two cursors
128
127
  space_between = before_offset - after_offset - 1
129
- paginated_nodes = set_limit(paginated_nodes, space_between)
128
+ relation_limit = space_between
130
129
  else
131
- # TODO I think this is untested
132
130
  # The cursors overextend one another to an empty set
133
- paginated_nodes = null_relation(paginated_nodes)
131
+ @sliced_nodes_null_relation = true
134
132
  end
135
133
  elsif before_offset
136
134
  # Use limit to cut off the tail of the relation
137
- paginated_nodes = set_limit(paginated_nodes, before_offset - 1)
135
+ relation_limit = before_offset - 1
136
+ end
137
+
138
+ @sliced_nodes_limit = relation_limit
139
+ @sliced_nodes_offset = relation_offset || 0
140
+ end
141
+ end
142
+
143
+ # Apply `before` and `after` to the underlying `items`,
144
+ # returning a new relation.
145
+ def sliced_nodes
146
+ @sliced_nodes ||= begin
147
+ calculate_sliced_nodes_parameters
148
+ paginated_nodes = items
149
+
150
+ if @sliced_nodes_null_relation
151
+ paginated_nodes = null_relation(paginated_nodes)
152
+ else
153
+ if @sliced_nodes_limit
154
+ paginated_nodes = set_limit(paginated_nodes, @sliced_nodes_limit)
155
+ end
156
+
157
+ if @sliced_nodes_offset
158
+ paginated_nodes = set_offset(paginated_nodes, @sliced_nodes_offset)
159
+ end
138
160
  end
139
161
 
140
162
  paginated_nodes
@@ -155,33 +177,38 @@ module GraphQL
155
177
  # returning a new relation
156
178
  def limited_nodes
157
179
  @limited_nodes ||= begin
158
- paginated_nodes = sliced_nodes
159
- previous_limit = relation_limit(paginated_nodes)
180
+ calculate_sliced_nodes_parameters
181
+ if @sliced_nodes_null_relation
182
+ # it's an empty set
183
+ return sliced_nodes
184
+ end
185
+ relation_limit = @sliced_nodes_limit
186
+ relation_offset = @sliced_nodes_offset
160
187
 
161
- if first && (previous_limit.nil? || previous_limit > first)
188
+ if first && (relation_limit.nil? || relation_limit > first)
162
189
  # `first` would create a stricter limit that the one already applied, so add it
163
- paginated_nodes = set_limit(paginated_nodes, first)
190
+ relation_limit = first
164
191
  end
165
192
 
166
193
  if last
167
- if (lv = relation_limit(paginated_nodes))
168
- if last <= lv
194
+ if relation_limit
195
+ if last <= relation_limit
169
196
  # `last` is a smaller slice than the current limit, so apply it
170
- offset = (relation_offset(paginated_nodes) || 0) + (lv - last)
171
- paginated_nodes = set_offset(paginated_nodes, offset)
172
- paginated_nodes = set_limit(paginated_nodes, last)
197
+ relation_offset += (relation_limit - last)
198
+ relation_limit = last
173
199
  end
174
200
  else
175
201
  # No limit, so get the last items
176
- sliced_nodes_count = relation_count(@sliced_nodes)
177
- offset = (relation_offset(paginated_nodes) || 0) + sliced_nodes_count - [last, sliced_nodes_count].min
178
- paginated_nodes = set_offset(paginated_nodes, offset)
179
- paginated_nodes = set_limit(paginated_nodes, last)
202
+ sliced_nodes_count = relation_count(sliced_nodes)
203
+ relation_offset += (sliced_nodes_count - [last, sliced_nodes_count].min)
204
+ relation_limit = last
180
205
  end
181
206
  end
182
207
 
183
- @paged_nodes_offset = relation_offset(paginated_nodes)
184
- paginated_nodes
208
+ @paged_nodes_offset = relation_offset
209
+ paginated_nodes = items
210
+ paginated_nodes = set_offset(paginated_nodes, relation_offset)
211
+ set_limit(paginated_nodes, relation_limit)
185
212
  end
186
213
  end
187
214
 
@@ -37,7 +37,7 @@ module GraphQL
37
37
  # @param arg_name [Symbol]
38
38
  # @param type_expr
39
39
  # @param desc [String]
40
- # @param required [Boolean] if true, this argument is non-null; if false, this argument is nullable
40
+ # @param required [Boolean, :nullable] if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`.
41
41
  # @param description [String]
42
42
  # @param default_value [Object]
43
43
  # @param as [Symbol] Override the keyword name when passed to a method
@@ -53,7 +53,7 @@ module GraphQL
53
53
  @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
54
54
  @type_expr = type_expr || type
55
55
  @description = desc || description
56
- @null = !required
56
+ @null = required != true
57
57
  @default_value = default_value
58
58
  @owner = owner
59
59
  @as = as
@@ -72,6 +72,9 @@ module GraphQL
72
72
  end
73
73
 
74
74
  self.validates(validates)
75
+ if required == :nullable
76
+ self.owner.validates(required: { argument: arg_name })
77
+ end
75
78
 
76
79
  if definition_block
77
80
  if definition_block.arity == 1
@@ -147,14 +150,7 @@ module GraphQL
147
150
  end
148
151
  end
149
152
  elsif as_type.kind.input_object?
150
- as_type.arguments(ctx).each do |_name, input_obj_arg|
151
- input_obj_arg = input_obj_arg.type_class
152
- # TODO: this skips input objects whose values were alread replaced with application objects.
153
- # See: https://github.com/rmosolgo/graphql-ruby/issues/2633
154
- if value.is_a?(InputObject) && value.key?(input_obj_arg.keyword) && !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
155
- return false
156
- end
157
- end
153
+ return as_type.authorized?(obj, value, ctx)
158
154
  end
159
155
  # None of the early-return conditions were activated,
160
156
  # so this is authorized.
@@ -16,6 +16,8 @@ module GraphQL
16
16
  include GraphQL::Schema::Member::HasDirectives
17
17
  include GraphQL::Schema::Member::HasDeprecationReason
18
18
 
19
+ class FieldImplementationFailed < GraphQL::Error; end
20
+
19
21
  # @return [String] the GraphQL name for this field, camelized unless `camelize: false` is provided
20
22
  attr_reader :name
21
23
  alias :graphql_name :name
@@ -792,51 +794,103 @@ module GraphQL
792
794
 
793
795
  def public_send_field(unextended_obj, unextended_ruby_kwargs, query_ctx)
794
796
  with_extensions(unextended_obj, unextended_ruby_kwargs, query_ctx) do |obj, ruby_kwargs|
795
- if @resolver_class
796
- if obj.is_a?(GraphQL::Schema::Object)
797
- obj = obj.object
798
- end
799
- obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
800
- end
801
-
802
- # Find a way to resolve this field, checking:
803
- #
804
- # - A method on the type instance;
805
- # - Hash keys, if the wrapped object is a hash;
806
- # - A method on the wrapped object;
807
- # - Or, raise not implemented.
808
- #
809
- if obj.respond_to?(@resolver_method)
810
- # Call the method with kwargs, if there are any
811
- if ruby_kwargs.any?
812
- obj.public_send(@resolver_method, **ruby_kwargs)
813
- else
814
- obj.public_send(@resolver_method)
797
+ begin
798
+ method_receiver = nil
799
+ method_to_call = nil
800
+ if @resolver_class
801
+ if obj.is_a?(GraphQL::Schema::Object)
802
+ obj = obj.object
803
+ end
804
+ obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
815
805
  end
816
- elsif obj.object.is_a?(Hash)
817
- inner_object = obj.object
818
- if inner_object.key?(@method_sym)
819
- inner_object[@method_sym]
806
+
807
+ # Find a way to resolve this field, checking:
808
+ #
809
+ # - A method on the type instance;
810
+ # - Hash keys, if the wrapped object is a hash;
811
+ # - A method on the wrapped object;
812
+ # - Or, raise not implemented.
813
+ #
814
+ if obj.respond_to?(@resolver_method)
815
+ method_to_call = @resolver_method
816
+ method_receiver = obj
817
+ # Call the method with kwargs, if there are any
818
+ if ruby_kwargs.any?
819
+ obj.public_send(@resolver_method, **ruby_kwargs)
820
+ else
821
+ obj.public_send(@resolver_method)
822
+ end
823
+ elsif obj.object.is_a?(Hash)
824
+ inner_object = obj.object
825
+ if inner_object.key?(@method_sym)
826
+ inner_object[@method_sym]
827
+ else
828
+ inner_object[@method_str]
829
+ end
830
+ elsif obj.object.respond_to?(@method_sym)
831
+ method_to_call = @method_sym
832
+ method_receiver = obj.object
833
+ if ruby_kwargs.any?
834
+ obj.object.public_send(@method_sym, **ruby_kwargs)
835
+ else
836
+ obj.object.public_send(@method_sym)
837
+ end
820
838
  else
821
- inner_object[@method_str]
839
+ raise <<-ERR
840
+ Failed to implement #{@owner.graphql_name}.#{@name}, tried:
841
+
842
+ - `#{obj.class}##{@resolver_method}`, which did not exist
843
+ - `#{obj.object.class}##{@method_sym}`, which did not exist
844
+ - Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
845
+
846
+ To implement this field, define one of the methods above (and check for typos)
847
+ ERR
822
848
  end
823
- elsif obj.object.respond_to?(@method_sym)
824
- if ruby_kwargs.any?
825
- obj.object.public_send(@method_sym, **ruby_kwargs)
849
+ rescue ArgumentError
850
+ assert_satisfactory_implementation(method_receiver, method_to_call, ruby_kwargs)
851
+ # if the line above doesn't raise, re-raise
852
+ raise
853
+ end
854
+ end
855
+ end
856
+
857
+ def assert_satisfactory_implementation(receiver, method_name, ruby_kwargs)
858
+ method_defn = receiver.method(method_name)
859
+ unsatisfied_ruby_kwargs = ruby_kwargs.dup
860
+ unsatisfied_method_params = []
861
+ encountered_keyrest = false
862
+ method_defn.parameters.each do |(param_type, param_name)|
863
+ case param_type
864
+ when :key
865
+ unsatisfied_ruby_kwargs.delete(param_name)
866
+ when :keyreq
867
+ if unsatisfied_ruby_kwargs.key?(param_name)
868
+ unsatisfied_ruby_kwargs.delete(param_name)
826
869
  else
827
- obj.object.public_send(@method_sym)
870
+ unsatisfied_method_params << "- `#{param_name}:` is required by Ruby, but not by GraphQL. Consider `#{param_name}: nil` instead, or making this argument required in GraphQL."
828
871
  end
829
- else
830
- raise <<-ERR
831
- Failed to implement #{@owner.graphql_name}.#{@name}, tried:
872
+ when :keyrest
873
+ encountered_keyrest = true
874
+ when :req
875
+ unsatisfied_method_params << "- `#{param_name}` is required by Ruby, but GraphQL doesn't pass positional arguments. If it's meant to be a GraphQL argument, use `#{param_name}:` instead. Otherwise, remove it."
876
+ when :opt, :rest
877
+ # This is fine, although it will never be present
878
+ end
879
+ end
832
880
 
833
- - `#{obj.class}##{@resolver_method}`, which did not exist
834
- - `#{obj.object.class}##{@method_sym}`, which did not exist
835
- - Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
881
+ if encountered_keyrest
882
+ unsatisfied_ruby_kwargs.clear
883
+ end
836
884
 
837
- To implement this field, define one of the methods above (and check for typos)
838
- ERR
839
- end
885
+ if unsatisfied_ruby_kwargs.any? || unsatisfied_method_params.any?
886
+ raise FieldImplementationFailed.new, <<-ERR
887
+ Failed to call #{method_name} on #{receiver.inspect} because the Ruby method params were incompatible with the GraphQL arguments:
888
+
889
+ #{ unsatisfied_ruby_kwargs
890
+ .map { |key, value| "- `#{key}: #{value}` was given by GraphQL but not defined in the Ruby method. Add `#{key}:` to the method parameters." }
891
+ .concat(unsatisfied_method_params)
892
+ .join("\n") }
893
+ ERR
840
894
  end
841
895
  end
842
896
 
@@ -850,8 +904,12 @@ module GraphQL
850
904
  # This is a hack to get the _last_ value for extended obj and args,
851
905
  # in case one of the extensions doesn't `yield`.
852
906
  # (There's another implementation that uses multiple-return, but I'm wary of the perf cost of the extra arrays)
853
- extended = { args: args, obj: obj, memos: nil }
907
+ extended = { args: args, obj: obj, memos: nil, added_extras: nil }
854
908
  value = run_extensions_before_resolve(obj, args, ctx, extended) do |obj, args|
909
+ if (added_extras = extended[:added_extras])
910
+ args = args.dup
911
+ added_extras.each { |e| args.delete(e) }
912
+ end
855
913
  yield(obj, args)
856
914
  end
857
915
 
@@ -880,6 +938,12 @@ module GraphQL
880
938
  memos = extended[:memos] ||= {}
881
939
  memos[idx] = memo
882
940
  end
941
+
942
+ if (extras = extension.added_extras)
943
+ ae = extended[:added_extras] ||= []
944
+ ae.concat(extras)
945
+ end
946
+
883
947
  extended[:obj] = extended_obj
884
948
  extended[:args] = extended_args
885
949
  run_extensions_before_resolve(extended_obj, extended_args, ctx, extended, idx: idx + 1) { |o, a| yield(o, a) }
@@ -49,8 +49,36 @@ module GraphQL
49
49
  configs = @own_default_argument_configurations ||= []
50
50
  configs << [argument_args, argument_kwargs]
51
51
  end
52
+
53
+ # If configured, these `extras` will be added to the field if they aren't already present,
54
+ # but removed by from `arguments` before the field's `resolve` is called.
55
+ # (The extras _will_ be present for other extensions, though.)
56
+ #
57
+ # @param new_extras [Array<Symbol>] If provided, assign extras used by this extension
58
+ # @return [Array<Symbol>] any extras assigned to this extension
59
+ def extras(new_extras = nil)
60
+ if new_extras
61
+ @own_extras = new_extras
62
+ end
63
+
64
+ inherited_extras = self.superclass.respond_to?(:extras) ? superclass.extras : nil
65
+ if @own_extras
66
+ if inherited_extras
67
+ inherited_extras + @own_extras
68
+ else
69
+ @own_extras
70
+ end
71
+ elsif inherited_extras
72
+ inherited_extras
73
+ else
74
+ NO_EXTRAS
75
+ end
76
+ end
52
77
  end
53
78
 
79
+ NO_EXTRAS = [].freeze
80
+ private_constant :NO_EXTRAS
81
+
54
82
  # Called when this extension is attached to a field.
55
83
  # The field definition may be extended during this method.
56
84
  # @return [void]
@@ -79,9 +107,18 @@ module GraphQL
79
107
  end
80
108
  end
81
109
  end
110
+ if (extras = self.class.extras).any?
111
+ @added_extras = extras - field.extras
112
+ field.extras(@added_extras)
113
+ else
114
+ @added_extras = nil
115
+ end
82
116
  freeze
83
117
  end
84
118
 
119
+ # @api private
120
+ attr_reader :added_extras
121
+
85
122
  # Called before resolving {#field}. It should either:
86
123
  #
87
124
  # - `yield` values to continue execution; OR
@@ -79,6 +79,21 @@ module GraphQL
79
79
  end
80
80
  end
81
81
 
82
+ def self.authorized?(obj, value, ctx)
83
+ # Authorize each argument (but this doesn't apply if `prepare` is implemented):
84
+ if value.is_a?(InputObject)
85
+ arguments(ctx).each do |_name, input_obj_arg|
86
+ input_obj_arg = input_obj_arg.type_class
87
+ if value.key?(input_obj_arg.keyword) &&
88
+ !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
89
+ return false
90
+ end
91
+ end
92
+ end
93
+ # It didn't early-return false:
94
+ true
95
+ end
96
+
82
97
  def unwrap_value(value)
83
98
  case value
84
99
  when Array
@@ -53,6 +53,10 @@ module GraphQL
53
53
  end
54
54
 
55
55
  def coerce_input(value, ctx)
56
+ # `.validate_input` above is used for variables, but this method is used for arguments
57
+ if value.nil?
58
+ raise GraphQL::ExecutionError, "`null` is not a valid input for `#{to_type_signature}`, please provide a value for this argument."
59
+ end
56
60
  of_type.coerce_input(value, ctx)
57
61
  end
58
62
 
@@ -14,7 +14,7 @@ module GraphQL
14
14
  # argument :ingredient_id, ID, required: true
15
15
  # argument :cups, Integer, required: false
16
16
  # argument :tablespoons, Integer, required: false
17
- # argument :teaspoons, Integer, required: true
17
+ # argument :teaspoons, Integer, required: false
18
18
  # validates required: { one_of: [:cups, :tablespoons, :teaspoons] }
19
19
  # end
20
20
  #
@@ -28,11 +28,23 @@ module GraphQL
28
28
  # validates required: { one_of: [:node_id, [:object_type, :object_id]] }
29
29
  # end
30
30
  #
31
+ # @example require _some_ value for an argument, even if it's null
32
+ # field :update_settings, AccountSettings do
33
+ # # `required: :nullable` means this argument must be given, but may be `null`
34
+ # argument :age, Integer, required: :nullable
35
+ # end
36
+ #
31
37
  class RequiredValidator < Validator
32
38
  # @param one_of [Symbol, Array<Symbol>] An argument, or a list of arguments, that represents a valid set of inputs for this field
33
39
  # @param message [String]
34
- def initialize(one_of:, message: "%{validated} has the wrong arguments", **default_options)
35
- @one_of = one_of
40
+ def initialize(one_of: nil, argument: nil, message: "%{validated} has the wrong arguments", **default_options)
41
+ @one_of = if one_of
42
+ one_of
43
+ elsif argument
44
+ [argument]
45
+ else
46
+ raise ArgumentError, "`one_of:` or `argument:` must be given in `validates required: {...}`"
47
+ end
36
48
  @message = message
37
49
  super(**default_options)
38
50
  end
@@ -40,19 +52,21 @@ module GraphQL
40
52
  def validate(_object, _context, value)
41
53
  matched_conditions = 0
42
54
 
43
- @one_of.each do |one_of_condition|
44
- case one_of_condition
45
- when Symbol
46
- if value.key?(one_of_condition)
47
- matched_conditions += 1
48
- end
49
- when Array
50
- if one_of_condition.all? { |k| value.key?(k) }
51
- matched_conditions += 1
52
- break
55
+ if !value.nil?
56
+ @one_of.each do |one_of_condition|
57
+ case one_of_condition
58
+ when Symbol
59
+ if value.key?(one_of_condition)
60
+ matched_conditions += 1
61
+ end
62
+ when Array
63
+ if one_of_condition.all? { |k| value.key?(k) }
64
+ matched_conditions += 1
65
+ break
66
+ end
67
+ else
68
+ raise ArgumentError, "Unknown one_of condition: #{one_of_condition.inspect}"
53
69
  end
54
- else
55
- raise ArgumentError, "Unknown one_of condition: #{one_of_condition.inspect}"
56
70
  end
57
71
  end
58
72
 
@@ -1247,7 +1247,11 @@ module GraphQL
1247
1247
  when Module
1248
1248
  type_or_name
1249
1249
  else
1250
- raise ArgumentError, "unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})"
1250
+ raise ArgumentError, <<-ERR
1251
+ Invariant: unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})
1252
+
1253
+ This is probably a bug in GraphQL-Ruby, please report this error on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new?template=bug_report.md
1254
+ ERR
1251
1255
  end
1252
1256
 
1253
1257
  if parent_type.kind.fields? && (field = parent_type.get_field(field_name, context))
@@ -33,6 +33,7 @@ module GraphQL
33
33
  GraphQL::StaticValidation::VariablesAreUsedAndDefined,
34
34
  GraphQL::StaticValidation::VariableUsagesAreAllowed,
35
35
  GraphQL::StaticValidation::MutationRootExists,
36
+ GraphQL::StaticValidation::QueryRootExists,
36
37
  GraphQL::StaticValidation::SubscriptionRootExists,
37
38
  GraphQL::StaticValidation::InputObjectNamesAreUnique,
38
39
  ]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ module QueryRootExists
5
+ def on_operation_definition(node, _parent)
6
+ if (node.operation_type == 'query' || node.operation_type.nil?) && context.warden.root_type_for_operation("query").nil?
7
+ add_error(GraphQL::StaticValidation::QueryRootExistsError.new(
8
+ 'Schema is not configured for queries',
9
+ nodes: node
10
+ ))
11
+ else
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ class QueryRootExistsError < StaticValidation::Error
5
+
6
+ def initialize(message, path: nil, nodes: [])
7
+ super(message, path: path, nodes: nodes)
8
+ end
9
+
10
+ # A hash representation of this Message
11
+ def to_h
12
+ extensions = {
13
+ "code" => code,
14
+ }
15
+
16
+ super.merge({
17
+ "extensions" => extensions
18
+ })
19
+ end
20
+
21
+ def code
22
+ "missingQueryConfiguration"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -68,6 +68,12 @@ module GraphQL
68
68
  arg_defn = context.warden.get_argument(argument_owner, arg_node.name)
69
69
  arg_defn_type = arg_defn.type
70
70
 
71
+ # If the argument is non-null, but it was given a default value,
72
+ # then treat it as nullable in practice, see https://github.com/rmosolgo/graphql-ruby/issues/3793
73
+ if arg_defn_type.non_null? && arg_defn.default_value?
74
+ arg_defn_type = arg_defn_type.of_type
75
+ end
76
+
71
77
  var_inner_type = var_type.unwrap
72
78
  arg_inner_type = arg_defn_type.unwrap
73
79
 
@@ -71,9 +71,17 @@ module GraphQL
71
71
  when SYMBOL_KEY
72
72
  value[SYMBOL_KEY].to_sym
73
73
  when TIMESTAMP_KEY
74
- timestamp_class_name, timestamp_s = value[TIMESTAMP_KEY]
74
+ timestamp_class_name, *timestamp_args = value[TIMESTAMP_KEY]
75
75
  timestamp_class = Object.const_get(timestamp_class_name)
76
- timestamp_class.strptime(timestamp_s, TIMESTAMP_FORMAT)
76
+ if defined?(ActiveSupport::TimeWithZone) && timestamp_class <= ActiveSupport::TimeWithZone
77
+ zone_name, timestamp_s = timestamp_args
78
+ zone = ActiveSupport::TimeZone[zone_name]
79
+ raise "Zone #{zone_name} not found, unable to deserialize" unless zone
80
+ zone.strptime(timestamp_s, TIMESTAMP_FORMAT)
81
+ else
82
+ timestamp_s = timestamp_args.first
83
+ timestamp_class.strptime(timestamp_s, TIMESTAMP_FORMAT)
84
+ end
77
85
  when OPEN_STRUCT_KEY
78
86
  ostruct_values = load_value(value[OPEN_STRUCT_KEY])
79
87
  OpenStruct.new(ostruct_values)
@@ -123,6 +131,18 @@ module GraphQL
123
131
  { SYMBOL_KEY => obj.to_s }
124
132
  elsif obj.respond_to?(:to_gid_param)
125
133
  {GLOBALID_KEY => obj.to_gid_param}
134
+ elsif defined?(ActiveSupport::TimeWithZone) && obj.is_a?(ActiveSupport::TimeWithZone) && obj.class.name != Time.name
135
+ # This handles a case where Rails prior to 7 would
136
+ # make the class ActiveSupport::TimeWithZone return "Time" for
137
+ # its name. In Rails 7, it will now return "ActiveSupport::TimeWithZone",
138
+ # which happens to be incompatible with expectations we have
139
+ # with what a Time class supports ( notably, strptime in `load_value` ).
140
+ #
141
+ # This now passes along the name of the zone, such that a future deserialization
142
+ # of this string will use the correct time zone from the ActiveSupport TimeZone
143
+ # list to produce the time.
144
+ #
145
+ { TIMESTAMP_KEY => [obj.class.name, obj.time_zone.name, obj.strftime(TIMESTAMP_FORMAT)] }
126
146
  elsif obj.is_a?(Date) || obj.is_a?(Time)
127
147
  # DateTime extends Date; for TimeWithZone, call `.utc` first.
128
148
  { TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'graphql/tracing/notifications_tracing'
4
+
3
5
  module GraphQL
4
6
  module Tracing
5
7
  # This implementation forwards events to ActiveSupport::Notifications
@@ -8,27 +10,11 @@ module GraphQL
8
10
  # @see KEYS for event names
9
11
  module ActiveSupportNotificationsTracing
10
12
  # A cache of frequently-used keys to avoid needless string allocations
11
- KEYS = {
12
- "lex" => "lex.graphql",
13
- "parse" => "parse.graphql",
14
- "validate" => "validate.graphql",
15
- "analyze_multiplex" => "analyze_multiplex.graphql",
16
- "analyze_query" => "analyze_query.graphql",
17
- "execute_query" => "execute_query.graphql",
18
- "execute_query_lazy" => "execute_query_lazy.graphql",
19
- "execute_field" => "execute_field.graphql",
20
- "execute_field_lazy" => "execute_field_lazy.graphql",
21
- "authorized" => "authorized.graphql",
22
- "authorized_lazy" => "authorized_lazy.graphql",
23
- "resolve_type" => "resolve_type.graphql",
24
- "resolve_type_lazy" => "resolve_type.graphql",
25
- }
13
+ KEYS = NotificationsTracing::KEYS
14
+ NOTIFICATIONS_ENGINE = NotificationsTracing.new(ActiveSupport::Notifications) if defined?(ActiveSupport)
26
15
 
27
- def self.trace(key, metadata)
28
- prefixed_key = KEYS[key] || "#{key}.graphql"
29
- ActiveSupport::Notifications.instrument(prefixed_key, metadata) do
30
- yield
31
- end
16
+ def self.trace(key, metadata, &blk)
17
+ NOTIFICATIONS_ENGINE.trace(key, metadata, &blk)
32
18
  end
33
19
  end
34
20
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ # This implementation forwards events to a notification handler (i.e.
6
+ # ActiveSupport::Notifications or Dry::Monitor::Notifications)
7
+ # with a `graphql` suffix.
8
+ #
9
+ # @see KEYS for event names
10
+ class NotificationsTracing
11
+ # A cache of frequently-used keys to avoid needless string allocations
12
+ KEYS = {
13
+ "lex" => "lex.graphql",
14
+ "parse" => "parse.graphql",
15
+ "validate" => "validate.graphql",
16
+ "analyze_multiplex" => "analyze_multiplex.graphql",
17
+ "analyze_query" => "analyze_query.graphql",
18
+ "execute_query" => "execute_query.graphql",
19
+ "execute_query_lazy" => "execute_query_lazy.graphql",
20
+ "execute_field" => "execute_field.graphql",
21
+ "execute_field_lazy" => "execute_field_lazy.graphql",
22
+ "authorized" => "authorized.graphql",
23
+ "authorized_lazy" => "authorized_lazy.graphql",
24
+ "resolve_type" => "resolve_type.graphql",
25
+ "resolve_type_lazy" => "resolve_type.graphql",
26
+ }
27
+
28
+ MAX_KEYS_SIZE = 100
29
+
30
+ # Initialize a new NotificationsTracing instance
31
+ #
32
+ # @param [Object] notifications_engine The notifications engine to use
33
+ def initialize(notifications_engine)
34
+ @notifications_engine = notifications_engine
35
+ end
36
+
37
+ # Sends a GraphQL tracing event to the notification handler
38
+ #
39
+ # @example
40
+ # . notifications_engine = Dry::Monitor::Notifications.new(:graphql)
41
+ # . tracer = GraphQL::Tracing::NotificationsTracing.new(notifications_engine)
42
+ # . tracer.trace("lex") { ... }
43
+ #
44
+ # @param [string] key The key for the event
45
+ # @param [Hash] metadata The metadata for the event
46
+ # @yield The block to execute for the event
47
+ def trace(key, metadata, &blk)
48
+ prefixed_key = KEYS[key] || "#{key}.graphql"
49
+
50
+ # Cache the new keys while making sure not to induce a memory leak
51
+ if KEYS.size < MAX_KEYS_SIZE
52
+ KEYS[key] ||= prefixed_key
53
+ end
54
+
55
+ @notifications_engine.instrument(prefixed_key, metadata, &blk)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -2,8 +2,7 @@
2
2
  module GraphQL
3
3
  module Types
4
4
  module Relay
5
- # This can be used for implementing `Query.node(id: ...)`,
6
- # or use it for inspiration for your own field definition.
5
+ # Don't use this field directly, instead, use one of these approaches:
7
6
  #
8
7
  # @example Adding this field directly
9
8
  # include GraphQL::Types::Relay::HasNodeField
@@ -19,7 +18,19 @@ module GraphQL
19
18
  # context.schema.object_from_id(id, context)
20
19
  # end
21
20
  #
22
- NodeField = GraphQL::Schema::Field.new(owner: nil, **HasNodeField.field_options, &HasNodeField.field_block)
21
+ def self.const_missing(const_name)
22
+ if const_name == :NodeField
23
+ message = "NodeField is deprecated, use `include GraphQL::Types::Relay::HasNodeField` instead."
24
+ message += "\n(referenced from #{caller(1, 1).first})"
25
+ GraphQL::Deprecation.warn(message)
26
+
27
+ DeprecatedNodeField
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ DeprecatedNodeField = GraphQL::Schema::Field.new(owner: nil, **HasNodeField.field_options, &HasNodeField.field_block)
23
34
  end
24
35
  end
25
36
  end
@@ -2,8 +2,7 @@
2
2
  module GraphQL
3
3
  module Types
4
4
  module Relay
5
- # This can be used for implementing `Query.nodes(ids: ...)`,
6
- # or use it for inspiration for your own field definition.
5
+ # Don't use this directly, instead, use one of these:
7
6
  #
8
7
  # @example Adding this field directly
9
8
  # include GraphQL::Types::Relay::HasNodesField
@@ -21,7 +20,18 @@ module GraphQL
21
20
  # end
22
21
  # end
23
22
  #
24
- NodesField = GraphQL::Schema::Field.new(owner: nil, **HasNodesField.field_options, &HasNodesField.field_block)
23
+ def self.const_missing(const_name)
24
+ if const_name == :NodesField
25
+ message = "NodesField is deprecated, use `include GraphQL::Types::Relay::HasNodesField` instead."
26
+ message += "\n(referenced from #{caller(1, 1).first})"
27
+ GraphQL::Deprecation.warn(message)
28
+
29
+ DeprecatedNodesField
30
+ else
31
+ super
32
+ end
33
+ end
34
+ DeprecatedNodesField = GraphQL::Schema::Field.new(owner: nil, **HasNodesField.field_options, &HasNodesField.field_block)
25
35
  end
26
36
  end
27
37
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.13.2"
3
+ VERSION = "1.13.3"
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.13.2
4
+ version: 1.13.3
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-12-15 00:00:00.000000000 Z
11
+ date: 2022-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -606,6 +606,8 @@ files:
606
606
  - lib/graphql/static_validation/rules/no_definitions_are_present_error.rb
607
607
  - lib/graphql/static_validation/rules/operation_names_are_valid.rb
608
608
  - lib/graphql/static_validation/rules/operation_names_are_valid_error.rb
609
+ - lib/graphql/static_validation/rules/query_root_exists.rb
610
+ - lib/graphql/static_validation/rules/query_root_exists_error.rb
609
611
  - lib/graphql/static_validation/rules/required_arguments_are_present.rb
610
612
  - lib/graphql/static_validation/rules/required_arguments_are_present_error.rb
611
613
  - lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb
@@ -644,6 +646,7 @@ files:
644
646
  - lib/graphql/tracing/appsignal_tracing.rb
645
647
  - lib/graphql/tracing/data_dog_tracing.rb
646
648
  - lib/graphql/tracing/new_relic_tracing.rb
649
+ - lib/graphql/tracing/notifications_tracing.rb
647
650
  - lib/graphql/tracing/platform_tracing.rb
648
651
  - lib/graphql/tracing/prometheus_tracing.rb
649
652
  - lib/graphql/tracing/prometheus_tracing/graphql_collector.rb