graphql 1.12.6 → 1.12.11

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +1 -1
  3. data/lib/generators/graphql/templates/graphql_controller.erb +2 -2
  4. data/lib/graphql.rb +10 -10
  5. data/lib/graphql/backtrace/table.rb +14 -2
  6. data/lib/graphql/dataloader.rb +44 -15
  7. data/lib/graphql/execution/errors.rb +109 -11
  8. data/lib/graphql/execution/execute.rb +1 -1
  9. data/lib/graphql/execution/interpreter.rb +4 -8
  10. data/lib/graphql/execution/interpreter/runtime.rb +207 -188
  11. data/lib/graphql/introspection.rb +1 -1
  12. data/lib/graphql/introspection/directive_type.rb +7 -3
  13. data/lib/graphql/introspection/schema_type.rb +1 -1
  14. data/lib/graphql/pagination/active_record_relation_connection.rb +7 -0
  15. data/lib/graphql/pagination/connections.rb +1 -1
  16. data/lib/graphql/pagination/relation_connection.rb +8 -1
  17. data/lib/graphql/query.rb +1 -3
  18. data/lib/graphql/query/null_context.rb +7 -1
  19. data/lib/graphql/query/validation_pipeline.rb +1 -1
  20. data/lib/graphql/rake_task.rb +3 -0
  21. data/lib/graphql/schema.rb +49 -237
  22. data/lib/graphql/schema/addition.rb +238 -0
  23. data/lib/graphql/schema/argument.rb +55 -36
  24. data/lib/graphql/schema/directive/transform.rb +13 -1
  25. data/lib/graphql/schema/input_object.rb +2 -2
  26. data/lib/graphql/schema/loader.rb +8 -0
  27. data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
  28. data/lib/graphql/schema/object.rb +19 -5
  29. data/lib/graphql/schema/resolver.rb +46 -24
  30. data/lib/graphql/schema/scalar.rb +3 -1
  31. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +3 -1
  32. data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +6 -2
  33. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  34. data/lib/graphql/static_validation/rules/arguments_are_defined_error.rb +4 -2
  35. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
  36. data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
  37. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  38. data/lib/graphql/static_validation/validator.rb +5 -0
  39. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
  40. data/lib/graphql/subscriptions/broadcast_analyzer.rb +0 -3
  41. data/lib/graphql/subscriptions/serialize.rb +11 -1
  42. data/lib/graphql/types/relay/base_connection.rb +4 -0
  43. data/lib/graphql/types/relay/connection_behaviors.rb +21 -10
  44. data/lib/graphql/types/relay/edge_behaviors.rb +12 -1
  45. data/lib/graphql/version.rb +1 -1
  46. metadata +3 -3
  47. data/lib/graphql/execution/interpreter/hash_response.rb +0 -46
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ class Addition
6
+ attr_reader :directives, :possible_types, :types, :union_memberships, :references, :arguments_with_default_values
7
+
8
+ def initialize(schema:, own_types:, new_types:)
9
+ @schema = schema
10
+ @own_types = own_types
11
+ @directives = Set.new
12
+ @possible_types = {}
13
+ @types = {}
14
+ @union_memberships = {}
15
+ @references = Hash.new { |h, k| h[k] = [] }
16
+ @arguments_with_default_values = []
17
+ add_type_and_traverse(new_types)
18
+ end
19
+
20
+ private
21
+
22
+ def references_to(thing, from:)
23
+ @references[thing] << from
24
+ end
25
+
26
+ def get_type(name)
27
+ @types[name] || @schema.get_type(name)
28
+ end
29
+
30
+ # Lookup using `own_types` here because it's ok to override
31
+ # inherited types by name
32
+ def get_local_type(name)
33
+ @types[name] || @own_types[name]
34
+ end
35
+
36
+ def add_directives_from(owner)
37
+ dirs = owner.directives.map(&:class)
38
+ @directives.merge(dirs)
39
+ add_type_and_traverse(dirs)
40
+ end
41
+
42
+ def add_type_and_traverse(new_types)
43
+ late_types = []
44
+ new_types.each { |t| add_type(t, owner: nil, late_types: late_types, path: [t.graphql_name]) }
45
+ missed_late_types = 0
46
+ while (late_type_vals = late_types.shift)
47
+ type_owner, lt = late_type_vals
48
+ if lt.is_a?(String)
49
+ type = Member::BuildType.constantize(lt)
50
+ # Reset the counter, since we might succeed next go-round
51
+ missed_late_types = 0
52
+ update_type_owner(type_owner, type)
53
+ add_type(type, owner: type_owner, late_types: late_types, path: [type.graphql_name])
54
+ elsif lt.is_a?(LateBoundType)
55
+ if (type = get_type(lt.name))
56
+ # Reset the counter, since we might succeed next go-round
57
+ missed_late_types = 0
58
+ update_type_owner(type_owner, type)
59
+ add_type(type, owner: type_owner, late_types: late_types, path: [type.graphql_name])
60
+ else
61
+ missed_late_types += 1
62
+ # Add it back to the list, maybe we'll be able to resolve it later.
63
+ late_types << [type_owner, lt]
64
+ if missed_late_types == late_types.size
65
+ # We've looked at all of them and haven't resolved one.
66
+ raise UnresolvedLateBoundTypeError.new(type: lt)
67
+ else
68
+ # Try the next one
69
+ end
70
+ end
71
+ else
72
+ raise ArgumentError, "Unexpected late type: #{lt.inspect}"
73
+ end
74
+ end
75
+ nil
76
+ end
77
+
78
+ def update_type_owner(owner, type)
79
+ case owner
80
+ when Class
81
+ if owner.kind.union?
82
+ # It's a union with possible_types
83
+ # Replace the item by class name
84
+ owner.assign_type_membership_object_type(type)
85
+ @possible_types[owner.graphql_name] = owner.possible_types
86
+ elsif type.kind.interface? && owner.kind.object?
87
+ new_interfaces = []
88
+ owner.interfaces.each do |int_t|
89
+ if int_t.is_a?(String) && int_t == type.graphql_name
90
+ new_interfaces << type
91
+ elsif int_t.is_a?(LateBoundType) && int_t.graphql_name == type.graphql_name
92
+ new_interfaces << type
93
+ else
94
+ # Don't re-add proper interface definitions,
95
+ # they were probably already added, maybe with options.
96
+ end
97
+ end
98
+ owner.implements(*new_interfaces)
99
+ new_interfaces.each do |int|
100
+ pt = @possible_types[int.graphql_name] ||= []
101
+ if !pt.include?(owner)
102
+ pt << owner
103
+ end
104
+ end
105
+ end
106
+
107
+ when nil
108
+ # It's a root type
109
+ @types[type.graphql_name] = type
110
+ when GraphQL::Schema::Field, GraphQL::Schema::Argument
111
+ orig_type = owner.type
112
+ # Apply list/non-null wrapper as needed
113
+ if orig_type.respond_to?(:of_type)
114
+ transforms = []
115
+ while (orig_type.respond_to?(:of_type))
116
+ if orig_type.kind.non_null?
117
+ transforms << :to_non_null_type
118
+ elsif orig_type.kind.list?
119
+ transforms << :to_list_type
120
+ else
121
+ raise "Invariant: :of_type isn't non-null or list"
122
+ end
123
+ orig_type = orig_type.of_type
124
+ end
125
+ transforms.reverse_each { |t| type = type.public_send(t) }
126
+ end
127
+ owner.type = type
128
+ else
129
+ raise "Unexpected update: #{owner.inspect} #{type.inspect}"
130
+ end
131
+ end
132
+
133
+ def add_type(type, owner:, late_types:, path:)
134
+ if type.respond_to?(:metadata) && type.metadata.is_a?(Hash)
135
+ type_class = type.metadata[:type_class]
136
+ if type_class.nil?
137
+ raise ArgumentError, "Can't add legacy type: #{type} (#{type.class})"
138
+ else
139
+ type = type_class
140
+ end
141
+ elsif type.is_a?(String) || type.is_a?(GraphQL::Schema::LateBoundType)
142
+ late_types << [owner, type]
143
+ return
144
+ end
145
+
146
+ if owner.is_a?(Class) && owner < GraphQL::Schema::Union
147
+ um = @union_memberships[type.graphql_name] ||= []
148
+ um << owner
149
+ end
150
+
151
+ if (prev_type = get_local_type(type.graphql_name))
152
+ if prev_type != type
153
+ raise DuplicateTypeNamesError.new(
154
+ type_name: type.graphql_name,
155
+ first_definition: prev_type,
156
+ second_definition: type,
157
+ path: path,
158
+ )
159
+ else
160
+ # This type was already added
161
+ end
162
+ elsif type.is_a?(Class) && type < GraphQL::Schema::Directive
163
+ @directives << type
164
+ type.arguments.each do |name, arg|
165
+ arg_type = arg.type.unwrap
166
+ references_to(arg_type, from: arg)
167
+ add_type(arg_type, owner: arg, late_types: late_types, path: path + [name])
168
+ if arg.default_value?
169
+ @arguments_with_default_values << arg
170
+ end
171
+ end
172
+ else
173
+ @types[type.graphql_name] = type
174
+ add_directives_from(type)
175
+ if type.kind.fields?
176
+ type.fields.each do |name, field|
177
+ field_type = field.type.unwrap
178
+ references_to(field_type, from: field)
179
+ field_path = path + [name]
180
+ add_type(field_type, owner: field, late_types: late_types, path: field_path)
181
+ add_directives_from(field)
182
+ field.arguments.each do |arg_name, arg|
183
+ add_directives_from(arg)
184
+ arg_type = arg.type.unwrap
185
+ references_to(arg_type, from: arg)
186
+ add_type(arg_type, owner: arg, late_types: late_types, path: field_path + [arg_name])
187
+ if arg.default_value?
188
+ @arguments_with_default_values << arg
189
+ end
190
+ end
191
+ end
192
+ end
193
+ if type.kind.input_object?
194
+ type.arguments.each do |arg_name, arg|
195
+ add_directives_from(arg)
196
+ arg_type = arg.type.unwrap
197
+ references_to(arg_type, from: arg)
198
+ add_type(arg_type, owner: arg, late_types: late_types, path: path + [arg_name])
199
+ if arg.default_value?
200
+ @arguments_with_default_values << arg
201
+ end
202
+ end
203
+ end
204
+ if type.kind.union?
205
+ @possible_types[type.graphql_name] = type.possible_types
206
+ type.possible_types.each do |t|
207
+ add_type(t, owner: type, late_types: late_types, path: path + ["possible_types"])
208
+ end
209
+ end
210
+ if type.kind.interface?
211
+ type.orphan_types.each do |t|
212
+ add_type(t, owner: type, late_types: late_types, path: path + ["orphan_types"])
213
+ end
214
+ end
215
+ if type.kind.object?
216
+ @possible_types[type.graphql_name] = [type]
217
+ type.interface_type_memberships.each do |interface_type_membership|
218
+ case interface_type_membership
219
+ when Schema::TypeMembership
220
+ interface_type = interface_type_membership.abstract_type
221
+ # We can get these now; we'll have to get late-bound types later
222
+ if interface_type.is_a?(Module)
223
+ implementers = @possible_types[interface_type.graphql_name] ||= []
224
+ implementers << type
225
+ end
226
+ when String, Schema::LateBoundType
227
+ interface_type = interface_type_membership
228
+ else
229
+ raise ArgumentError, "Invariant: unexpected type membership for #{type.graphql_name}: #{interface_type_membership.class} (#{interface_type_membership.inspect})"
230
+ end
231
+ add_type(interface_type, owner: type, late_types: late_types, path: path + ["implements"])
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -240,60 +240,79 @@ module GraphQL
240
240
  def coerce_into_values(parent_object, values, context, argument_values)
241
241
  arg_name = graphql_name
242
242
  arg_key = keyword
243
- has_value = false
244
243
  default_used = false
244
+
245
245
  if values.key?(arg_name)
246
- has_value = true
247
246
  value = values[arg_name]
248
247
  elsif values.key?(arg_key)
249
- has_value = true
250
248
  value = values[arg_key]
251
249
  elsif default_value?
252
- has_value = true
253
250
  value = default_value
254
251
  default_used = true
252
+ else
253
+ # no value at all
254
+ owner.validate_directive_argument(self, nil)
255
+ return
255
256
  end
256
257
 
257
- if has_value
258
- loaded_value = nil
259
- coerced_value = context.schema.error_handler.with_error_handling(context) do
260
- type.coerce_input(value, context)
261
- end
258
+ loaded_value = nil
259
+ coerced_value = context.schema.error_handler.with_error_handling(context) do
260
+ type.coerce_input(value, context)
261
+ end
262
262
 
263
- # TODO this should probably be inside after_lazy
264
- if loads && !from_resolver?
265
- loaded_value = if type.list?
266
- loaded_values = coerced_value.map { |val| owner.load_application_object(self, loads, val, context) }
267
- context.schema.after_any_lazies(loaded_values) { |result| result }
268
- else
263
+ # TODO this should probably be inside after_lazy
264
+ if loads && !from_resolver?
265
+ loaded_value = if type.list?
266
+ loaded_values = coerced_value.map { |val| owner.load_application_object(self, loads, val, context) }
267
+ context.schema.after_any_lazies(loaded_values) { |result| result }
268
+ else
269
+ context.query.with_error_handling do
269
270
  owner.load_application_object(self, loads, coerced_value, context)
270
271
  end
271
272
  end
273
+ end
272
274
 
273
- coerced_value = if loaded_value
274
- loaded_value
275
- else
276
- coerced_value
275
+ coerced_value = if loaded_value
276
+ loaded_value
277
+ else
278
+ coerced_value
279
+ end
280
+
281
+ # If this isn't lazy, then the block returns eagerly and assigns the result here
282
+ # If it _is_ lazy, then we write the lazy to the hash, then update it later
283
+ argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |coerced_value|
284
+ owner.validate_directive_argument(self, coerced_value)
285
+ prepared_value = context.schema.error_handler.with_error_handling(context) do
286
+ prepare_value(parent_object, coerced_value, context: context)
277
287
  end
278
288
 
279
- # If this isn't lazy, then the block returns eagerly and assigns the result here
280
- # If it _is_ lazy, then we write the lazy to the hash, then update it later
281
- argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |coerced_value|
282
- owner.validate_directive_argument(self, coerced_value)
283
- prepared_value = context.schema.error_handler.with_error_handling(context) do
284
- prepare_value(parent_object, coerced_value, context: context)
285
- end
289
+ # TODO code smell to access such a deeply-nested constant in a distant module
290
+ argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
291
+ value: prepared_value,
292
+ definition: self,
293
+ default_used: default_used,
294
+ )
295
+ end
296
+ end
286
297
 
287
- # TODO code smell to access such a deeply-nested constant in a distant module
288
- argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
289
- value: prepared_value,
290
- definition: self,
291
- default_used: default_used,
292
- )
293
- end
294
- else
295
- # has_value is false
296
- owner.validate_directive_argument(self, nil)
298
+ # @api private
299
+ def validate_default_value
300
+ coerced_default_value = begin
301
+ type.coerce_isolated_result(default_value) unless default_value.nil?
302
+ rescue GraphQL::Schema::Enum::UnresolvedValueError
303
+ # It raises this, which is helpful at runtime, but not here...
304
+ default_value
305
+ end
306
+ res = type.valid_isolated_input?(coerced_default_value)
307
+ if !res
308
+ raise InvalidDefaultValueError.new(self)
309
+ end
310
+ end
311
+
312
+ class InvalidDefaultValueError < GraphQL::Error
313
+ def initialize(argument)
314
+ message = "`#{argument.path}` has an invalid default value: `#{argument.default_value.inspect}` isn't accepted by `#{argument.type.to_type_signature}`; update the default value or the argument type."
315
+ super(message)
297
316
  end
298
317
  end
299
318
 
@@ -39,7 +39,19 @@ module GraphQL
39
39
  transform_name = arguments[:by]
40
40
  if TRANSFORMS.include?(transform_name) && return_value.respond_to?(transform_name)
41
41
  return_value = return_value.public_send(transform_name)
42
- context.namespace(:interpreter)[:runtime].write_in_response(path, return_value)
42
+ response = context.namespace(:interpreter)[:runtime].response
43
+ *keys, last = path
44
+ keys.each do |key|
45
+ if response && (response = response[key])
46
+ next
47
+ else
48
+ break
49
+ end
50
+ end
51
+ if response
52
+ response[last] = return_value
53
+ end
54
+ nil
43
55
  end
44
56
  end
45
57
  end
@@ -226,8 +226,8 @@ module GraphQL
226
226
  # It's funny to think of a _result_ of an input object.
227
227
  # This is used for rendering the default value in introspection responses.
228
228
  def coerce_result(value, ctx)
229
- # Allow the application to provide values as :symbols, and convert them to the strings
230
- value = value.reduce({}) { |memo, (k, v)| memo[k.to_s] = v; memo }
229
+ # Allow the application to provide values as :snake_symbols, and convert them to the camelStrings
230
+ value = value.reduce({}) { |memo, (k, v)| memo[Member::BuildType.camelize(k.to_s)] = v; memo }
231
231
 
232
232
  result = {}
233
233
 
@@ -169,6 +169,12 @@ module GraphQL
169
169
  def build_fields(type_defn, fields, type_resolver)
170
170
  loader = self
171
171
  fields.each do |field_hash|
172
+ unwrapped_field_hash = field_hash
173
+ while (of_type = unwrapped_field_hash["ofType"])
174
+ unwrapped_field_hash = of_type
175
+ end
176
+ type_name = unwrapped_field_hash["name"]
177
+
172
178
  type_defn.field(
173
179
  field_hash["name"],
174
180
  type: type_resolver.call(field_hash["type"]),
@@ -176,6 +182,8 @@ module GraphQL
176
182
  deprecation_reason: field_hash["deprecationReason"],
177
183
  null: true,
178
184
  camelize: false,
185
+ connection_extension: nil,
186
+ connection: type_name.end_with?("Connection"),
179
187
  ) do
180
188
  if field_hash["args"].any?
181
189
  loader.build_arguments(self, field_hash["args"], type_resolver)
@@ -113,27 +113,15 @@ module GraphQL
113
113
  end
114
114
 
115
115
  def visible?(context)
116
- if @mutation
117
- @mutation.visible?(context)
118
- else
119
- true
120
- end
116
+ true
121
117
  end
122
118
 
123
119
  def accessible?(context)
124
- if @mutation
125
- @mutation.accessible?(context)
126
- else
127
- true
128
- end
120
+ true
129
121
  end
130
122
 
131
123
  def authorized?(object, context)
132
- if @mutation
133
- @mutation.authorized?(object, context)
134
- else
135
- true
136
- end
124
+ true
137
125
  end
138
126
  end
139
127
  end
@@ -48,12 +48,26 @@ module GraphQL
48
48
  # @return [GraphQL::Schema::Object, GraphQL::Execution::Lazy]
49
49
  # @raise [GraphQL::UnauthorizedError] if the user-provided hook returns `false`
50
50
  def authorized_new(object, context)
51
- auth_val = context.query.with_error_handling do
52
- begin
53
- authorized?(object, context)
54
- rescue GraphQL::UnauthorizedError => err
55
- context.schema.unauthorized_object(err)
51
+ trace_payload = { context: context, type: self, object: object, path: context[:current_path] }
52
+
53
+ maybe_lazy_auth_val = context.query.trace("authorized", trace_payload) do
54
+ context.query.with_error_handling do
55
+ begin
56
+ authorized?(object, context)
57
+ rescue GraphQL::UnauthorizedError => err
58
+ context.schema.unauthorized_object(err)
59
+ end
60
+ end
61
+ end
62
+
63
+ auth_val = if context.schema.lazy?(maybe_lazy_auth_val)
64
+ GraphQL::Execution::Lazy.new do
65
+ context.query.trace("authorized_lazy", trace_payload) do
66
+ context.schema.sync_lazy(maybe_lazy_auth_val)
67
+ end
56
68
  end
69
+ else
70
+ maybe_lazy_auth_val
57
71
  end
58
72
 
59
73
  context.schema.after_lazy(auth_val) do |is_authorized|