graphql 1.12.7 → 1.12.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) 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 +59 -15
  7. data/lib/graphql/dataloader/null_dataloader.rb +1 -0
  8. data/lib/graphql/execution/errors.rb +4 -4
  9. data/lib/graphql/execution/execute.rb +1 -1
  10. data/lib/graphql/execution/interpreter.rb +4 -8
  11. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -2
  12. data/lib/graphql/execution/interpreter/runtime.rb +382 -223
  13. data/lib/graphql/introspection/schema_type.rb +1 -1
  14. data/lib/graphql/pagination/connections.rb +1 -1
  15. data/lib/graphql/query/null_context.rb +7 -1
  16. data/lib/graphql/rake_task.rb +3 -0
  17. data/lib/graphql/schema.rb +44 -218
  18. data/lib/graphql/schema/addition.rb +238 -0
  19. data/lib/graphql/schema/argument.rb +55 -36
  20. data/lib/graphql/schema/directive/transform.rb +13 -1
  21. data/lib/graphql/schema/input_object.rb +2 -2
  22. data/lib/graphql/schema/loader.rb +8 -0
  23. data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
  24. data/lib/graphql/schema/object.rb +19 -5
  25. data/lib/graphql/schema/resolver.rb +46 -24
  26. data/lib/graphql/schema/scalar.rb +3 -1
  27. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
  28. data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
  29. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  30. data/lib/graphql/static_validation/validator.rb +5 -0
  31. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
  32. data/lib/graphql/subscriptions/serialize.rb +11 -1
  33. data/lib/graphql/version.rb +1 -1
  34. metadata +17 -3
  35. data/lib/graphql/execution/interpreter/hash_response.rb +0 -46
@@ -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|
@@ -333,30 +333,32 @@ module GraphQL
333
333
  arg_defn = super(*args, from_resolver: true, **kwargs)
334
334
  own_arguments_loads_as_type[arg_defn.keyword] = loads if loads
335
335
 
336
- if loads && arg_defn.type.list?
337
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
338
- def load_#{arg_defn.keyword}(values)
339
- argument = @arguments_by_keyword[:#{arg_defn.keyword}]
340
- lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
341
- context.schema.after_lazy(values) do |values2|
342
- GraphQL::Execution::Lazy.all(values2.map { |value| load_application_object(argument, lookup_as_type, value, context) })
336
+ if !method_defined?(:"load_#{arg_defn.keyword}")
337
+ if loads && arg_defn.type.list?
338
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
339
+ def load_#{arg_defn.keyword}(values)
340
+ argument = @arguments_by_keyword[:#{arg_defn.keyword}]
341
+ lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
342
+ context.schema.after_lazy(values) do |values2|
343
+ GraphQL::Execution::Lazy.all(values2.map { |value| load_application_object(argument, lookup_as_type, value, context) })
344
+ end
343
345
  end
346
+ RUBY
347
+ elsif loads
348
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
349
+ def load_#{arg_defn.keyword}(value)
350
+ argument = @arguments_by_keyword[:#{arg_defn.keyword}]
351
+ lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
352
+ load_application_object(argument, lookup_as_type, value, context)
353
+ end
354
+ RUBY
355
+ else
356
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
357
+ def load_#{arg_defn.keyword}(value)
358
+ value
359
+ end
360
+ RUBY
344
361
  end
345
- RUBY
346
- elsif loads
347
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
348
- def load_#{arg_defn.keyword}(value)
349
- argument = @arguments_by_keyword[:#{arg_defn.keyword}]
350
- lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
351
- load_application_object(argument, lookup_as_type, value, context)
352
- end
353
- RUBY
354
- else
355
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
356
- def load_#{arg_defn.keyword}(value)
357
- value
358
- end
359
- RUBY
360
362
  end
361
363
 
362
364
  arg_defn
@@ -372,16 +374,36 @@ module GraphQL
372
374
  # @param extension [Class] Extension class
373
375
  # @param options [Hash] Optional extension options
374
376
  def extension(extension, **options)
375
- extensions << {extension => options}
377
+ @own_extensions ||= []
378
+ @own_extensions << {extension => options}
376
379
  end
377
380
 
378
381
  # @api private
379
382
  def extensions
380
- @extensions ||= []
383
+ own_exts = @own_extensions
384
+ # Jump through some hoops to avoid creating arrays when we don't actually need them
385
+ if superclass.respond_to?(:extensions)
386
+ s_exts = superclass.extensions
387
+ if own_exts
388
+ if s_exts.any?
389
+ own_exts + s_exts
390
+ else
391
+ own_exts
392
+ end
393
+ else
394
+ s_exts
395
+ end
396
+ else
397
+ own_exts || EMPTY_ARRAY
398
+ end
381
399
  end
382
400
 
383
401
  private
384
402
 
403
+ def own_extensions
404
+ @own_extensions
405
+ end
406
+
385
407
  def own_arguments_loads_as_type
386
408
  @own_arguments_loads_as_type ||= {}
387
409
  end
@@ -44,7 +44,9 @@ module GraphQL
44
44
  def validate_non_null_input(value, ctx)
45
45
  result = Query::InputValidationResult.new
46
46
  coerced_result = begin
47
- coerce_input(value, ctx)
47
+ ctx.query.with_error_handling do
48
+ coerce_input(value, ctx)
49
+ end
48
50
  rescue GraphQL::CoercionError => err
49
51
  err
50
52
  end
@@ -4,7 +4,7 @@ module GraphQL
4
4
  module DirectivesAreDefined
5
5
  def initialize(*)
6
6
  super
7
- @directive_names = context.schema.directives.keys
7
+ @directive_names = context.warden.directives.map(&:graphql_name)
8
8
  end
9
9
 
10
10
  def on_directive(node, parent)
@@ -373,17 +373,26 @@ module GraphQL
373
373
  # In this context, `parents` represends the "self scope" of the field,
374
374
  # what types may be found at this point in the query.
375
375
  def mutually_exclusive?(parents1, parents2)
376
- parents1.each do |type1|
377
- parents2.each do |type2|
378
- # If the types we're comparing are both different object types,
379
- # they have to be mutually exclusive.
380
- if type1 != type2 && type1.kind.object? && type2.kind.object?
381
- return true
376
+ if parents1.empty? || parents2.empty?
377
+ false
378
+ elsif parents1.length == parents2.length
379
+ parents1.length.times.any? do |i|
380
+ type1 = parents1[i - 1]
381
+ type2 = parents2[i - 1]
382
+ if type1 == type2
383
+ # If the types we're comparing are the same type,
384
+ # then they aren't mutually exclusive
385
+ false
386
+ else
387
+ # Check if these two scopes have _any_ types in common.
388
+ possible_right_types = context.query.possible_types(type1)
389
+ possible_left_types = context.query.possible_types(type2)
390
+ (possible_right_types & possible_left_types).empty?
382
391
  end
383
392
  end
393
+ else
394
+ true
384
395
  end
385
-
386
- false
387
396
  end
388
397
  end
389
398
  end
@@ -23,7 +23,7 @@ module GraphQL
23
23
  defn = if arg_defn && arg_defn.type.unwrap.kind.input_object?
24
24
  arg_defn.type.unwrap
25
25
  else
26
- context.field_definition
26
+ context.directive_definition || context.field_definition
27
27
  end
28
28
 
29
29
  parent_type = context.warden.get_argument(defn, parent_name(parent, defn))
@@ -73,6 +73,11 @@ module GraphQL
73
73
  irep: irep,
74
74
  }
75
75
  end
76
+ rescue GraphQL::ExecutionError => e
77
+ {
78
+ errors: [e],
79
+ irep: nil,
80
+ }
76
81
  end
77
82
 
78
83
  # Invoked when static validation times out.
@@ -34,12 +34,12 @@ module GraphQL
34
34
  # channel: self,
35
35
  # }
36
36
  #
37
- # result = MySchema.execute({
37
+ # result = MySchema.execute(
38
38
  # query: query,
39
39
  # context: context,
40
40
  # variables: variables,
41
41
  # operation_name: operation_name
42
- # })
42
+ # )
43
43
  #
44
44
  # payload = {
45
45
  # result: result.to_h,
@@ -146,14 +146,15 @@ module GraphQL
146
146
  def setup_stream(channel, initial_event)
147
147
  topic = initial_event.topic
148
148
  channel.stream_from(stream_event_name(initial_event), coder: @action_cable_coder) do |message|
149
- object = @serializer.load(message)
150
149
  events_by_fingerprint = @events[topic]
150
+ object = nil
151
151
  events_by_fingerprint.each do |_fingerprint, events|
152
152
  if events.any? && events.first == initial_event
153
153
  # The fingerprint has told us that this response should be shared by all subscribers,
154
154
  # so just run it once, then deliver the result to every subscriber
155
155
  first_event = events.first
156
156
  first_subscription_id = first_event.context.fetch(:subscription_id)
157
+ object ||= @serializer.load(message)
157
158
  result = execute_update(first_subscription_id, first_event, object)
158
159
  # Having calculated the result _once_, send the same payload to all subscribers
159
160
  events.each do |event|
@@ -55,7 +55,14 @@ module GraphQL
55
55
  # @return [Object] An object that load Global::Identification recursive
56
56
  def load_value(value)
57
57
  if value.is_a?(Array)
58
- value.map{|item| load_value(item)}
58
+ is_gids = (v1 = value[0]).is_a?(Hash) && v1.size == 1 && v1[GLOBALID_KEY]
59
+ if is_gids
60
+ # Assume it's an array of global IDs
61
+ ids = value.map { |v| v[GLOBALID_KEY] }
62
+ GlobalID::Locator.locate_many(ids)
63
+ else
64
+ value.map { |item| load_value(item) }
65
+ end
59
66
  elsif value.is_a?(Hash)
60
67
  if value.size == 1
61
68
  case value.keys.first # there's only 1 key
@@ -70,6 +77,9 @@ module GraphQL
70
77
  when OPEN_STRUCT_KEY
71
78
  ostruct_values = load_value(value[OPEN_STRUCT_KEY])
72
79
  OpenStruct.new(ostruct_values)
80
+ else
81
+ key = value.keys.first
82
+ { key => load_value(value[key]) }
73
83
  end
74
84
  else
75
85
  loaded_h = {}