graphql 1.12.8 → 1.12.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) 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/backtrace/tracer.rb +7 -4
  7. data/lib/graphql/cop/nullability.rb +28 -0
  8. data/lib/graphql/cop/resolve_methods.rb +28 -0
  9. data/lib/graphql/dataloader.rb +59 -15
  10. data/lib/graphql/dataloader/null_dataloader.rb +1 -0
  11. data/lib/graphql/execution/execute.rb +1 -1
  12. data/lib/graphql/execution/interpreter.rb +4 -8
  13. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -2
  14. data/lib/graphql/execution/interpreter/resolve.rb +6 -2
  15. data/lib/graphql/execution/interpreter/runtime.rb +496 -222
  16. data/lib/graphql/execution/lazy.rb +5 -1
  17. data/lib/graphql/introspection/schema_type.rb +1 -1
  18. data/lib/graphql/pagination/connections.rb +1 -1
  19. data/lib/graphql/query.rb +1 -1
  20. data/lib/graphql/query/null_context.rb +7 -1
  21. data/lib/graphql/rake_task.rb +3 -0
  22. data/lib/graphql/schema.rb +52 -218
  23. data/lib/graphql/schema/addition.rb +238 -0
  24. data/lib/graphql/schema/argument.rb +55 -36
  25. data/lib/graphql/schema/build_from_definition.rb +8 -2
  26. data/lib/graphql/schema/directive/transform.rb +13 -1
  27. data/lib/graphql/schema/enum.rb +10 -1
  28. data/lib/graphql/schema/input_object.rb +13 -17
  29. data/lib/graphql/schema/loader.rb +8 -0
  30. data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
  31. data/lib/graphql/schema/member/build_type.rb +1 -0
  32. data/lib/graphql/schema/object.rb +19 -5
  33. data/lib/graphql/schema/printer.rb +11 -16
  34. data/lib/graphql/schema/resolver.rb +52 -25
  35. data/lib/graphql/schema/scalar.rb +3 -1
  36. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
  37. data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
  38. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  39. data/lib/graphql/static_validation/validator.rb +5 -0
  40. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
  41. data/lib/graphql/subscriptions/serialize.rb +8 -1
  42. data/lib/graphql/types/relay/has_node_field.rb +1 -1
  43. data/lib/graphql/types/relay/has_nodes_field.rb +1 -1
  44. data/lib/graphql/types/relay/node_field.rb +2 -2
  45. data/lib/graphql/types/relay/nodes_field.rb +2 -2
  46. data/lib/graphql/version.rb +1 -1
  47. data/readme.md +0 -3
  48. metadata +9 -21
  49. 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
 
@@ -47,10 +47,16 @@ module GraphQL
47
47
  # _while_ building the schema.
48
48
  # It will dig for a type if it encounters a custom type. This could be a problem if there are cycles.
49
49
  directive_type_resolver = nil
50
- directive_type_resolver = build_resolve_type(GraphQL::Schema::BUILT_IN_TYPES, directives, ->(type_name) {
50
+ directive_type_resolver = build_resolve_type(types, directives, ->(type_name) {
51
51
  types[type_name] ||= begin
52
52
  defn = document.definitions.find { |d| d.respond_to?(:name) && d.name == type_name }
53
- build_definition_from_node(defn, directive_type_resolver, default_resolve)
53
+ if defn
54
+ build_definition_from_node(defn, directive_type_resolver, default_resolve)
55
+ elsif (built_in_defn = GraphQL::Schema::BUILT_IN_TYPES[type_name])
56
+ built_in_defn
57
+ else
58
+ raise "No definition for #{type_name.inspect} found in schema document or built-in types. Add a definition for it or remove it."
59
+ end
54
60
  end
55
61
  })
56
62
 
@@ -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].final_result
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
@@ -24,6 +24,15 @@ module GraphQL
24
24
  extend GraphQL::Schema::Member::ValidatesInput
25
25
 
26
26
  class UnresolvedValueError < GraphQL::EnumType::UnresolvedValueError
27
+ def initialize(value:, enum:, context:)
28
+ fix_message = ", but this isn't a valid value for `#{enum.graphql_name}`. Update the field or resolver to return one of `#{enum.graphql_name}`'s values instead."
29
+ message = if (cp = context[:current_path]) && (cf = context[:current_field])
30
+ "`#{cf.path}` returned `#{value.inspect}` at `#{cp.join(".")}`#{fix_message}"
31
+ else
32
+ "`#{value.inspect}` was returned for `#{enum.graphql_name}`#{fix_message}"
33
+ end
34
+ super(message)
35
+ end
27
36
  end
28
37
 
29
38
  class << self
@@ -100,7 +109,7 @@ module GraphQL
100
109
  if enum_value
101
110
  enum_value.graphql_name
102
111
  else
103
- raise(self::UnresolvedValueError, "Can't resolve enum #{graphql_name} for #{value.inspect}")
112
+ raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx)
104
113
  end
105
114
  end
106
115
 
@@ -11,6 +11,14 @@ module GraphQL
11
11
 
12
12
  include GraphQL::Dig
13
13
 
14
+ # @return [GraphQL::Query::Context] The context for this query
15
+ attr_reader :context
16
+ # @return [GraphQL::Query::Arguments, GraphQL::Execution::Interpereter::Arguments] The underlying arguments instance
17
+ attr_reader :arguments
18
+
19
+ # Ruby-like hash behaviors, read-only
20
+ def_delegators :@ruby_style_hash, :keys, :values, :each, :map, :any?, :empty?
21
+
14
22
  def initialize(arguments = nil, ruby_kwargs: nil, context:, defaults_used:)
15
23
  @context = context
16
24
  if ruby_kwargs
@@ -54,19 +62,8 @@ module GraphQL
54
62
  @maybe_lazies = maybe_lazies
55
63
  end
56
64
 
57
- # @return [GraphQL::Query::Context] The context for this query
58
- attr_reader :context
59
-
60
- # @return [GraphQL::Query::Arguments, GraphQL::Execution::Interpereter::Arguments] The underlying arguments instance
61
- attr_reader :arguments
62
-
63
- # Ruby-like hash behaviors, read-only
64
- def_delegators :@ruby_style_hash, :keys, :values, :each, :map, :any?, :empty?
65
-
66
65
  def to_h
67
- @ruby_style_hash.inject({}) do |h, (key, value)|
68
- h.merge(key => unwrap_value(value))
69
- end
66
+ unwrap_value(@ruby_style_hash)
70
67
  end
71
68
 
72
69
  def to_hash
@@ -91,8 +88,8 @@ module GraphQL
91
88
  when Array
92
89
  value.map { |item| unwrap_value(item) }
93
90
  when Hash
94
- value.inject({}) do |h, (key, value)|
95
- h.merge(key => unwrap_value(value))
91
+ value.reduce({}) do |h, (key, value)|
92
+ h.merge!(key => unwrap_value(value))
96
93
  end
97
94
  when InputObject
98
95
  value.to_h
@@ -162,7 +159,6 @@ module GraphQL
162
159
  # @api private
163
160
  INVALID_OBJECT_MESSAGE = "Expected %{object} to be a key-value object responding to `to_h` or `to_unsafe_h`."
164
161
 
165
-
166
162
  def validate_non_null_input(input, ctx)
167
163
  result = GraphQL::Query::InputValidationResult.new
168
164
 
@@ -226,8 +222,8 @@ module GraphQL
226
222
  # It's funny to think of a _result_ of an input object.
227
223
  # This is used for rendering the default value in introspection responses.
228
224
  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 }
225
+ # Allow the application to provide values as :snake_symbols, and convert them to the camelStrings
226
+ value = value.reduce({}) { |memo, (k, v)| memo[Member::BuildType.camelize(k.to_s)] = v; memo }
231
227
 
232
228
  result = {}
233
229
 
@@ -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
@@ -124,6 +124,7 @@ module GraphQL
124
124
  end
125
125
 
126
126
  def camelize(string)
127
+ return string if string == '_'
127
128
  return string unless string.include?("_")
128
129
  camelized = string.split('_').map(&:capitalize).join
129
130
  camelized[0] = camelized[0].downcase
@@ -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|
@@ -4,37 +4,32 @@ module GraphQL
4
4
  # Used to convert your {GraphQL::Schema} to a GraphQL schema string
5
5
  #
6
6
  # @example print your schema to standard output (via helper)
7
- # MySchema = GraphQL::Schema.define(query: QueryType)
8
7
  # puts GraphQL::Schema::Printer.print_schema(MySchema)
9
8
  #
10
9
  # @example print your schema to standard output
11
- # MySchema = GraphQL::Schema.define(query: QueryType)
12
10
  # puts GraphQL::Schema::Printer.new(MySchema).print_schema
13
11
  #
14
12
  # @example print a single type to standard output
15
- # query_root = GraphQL::ObjectType.define do
16
- # name "Query"
13
+ # class Types::Query < GraphQL::Schema::Object
17
14
  # description "The query root of this schema"
18
15
  #
19
- # field :post do
20
- # type post_type
21
- # resolve ->(obj, args, ctx) { Post.find(args["id"]) }
22
- # end
16
+ # field :post, Types::Post, null: true
23
17
  # end
24
18
  #
25
- # post_type = GraphQL::ObjectType.define do
26
- # name "Post"
19
+ # class Types::Post < GraphQL::Schema::Object
27
20
  # description "A blog post"
28
21
  #
29
- # field :id, !types.ID
30
- # field :title, !types.String
31
- # field :body, !types.String
22
+ # field :id, ID, null: false
23
+ # field :title, String, null: false
24
+ # field :body, String, null: false
32
25
  # end
33
26
  #
34
- # MySchema = GraphQL::Schema.define(query: query_root)
27
+ # class MySchema < GraphQL::Schema
28
+ # query(Types::Query)
29
+ # end
35
30
  #
36
31
  # printer = GraphQL::Schema::Printer.new(MySchema)
37
- # puts printer.print_type(post_type)
32
+ # puts printer.print_type(Types::Post)
38
33
  #
39
34
  class Printer < GraphQL::Language::Printer
40
35
  attr_reader :schema, :warden
@@ -87,7 +82,7 @@ module GraphQL
87
82
 
88
83
  # Return a GraphQL schema string for the defined types in the schema
89
84
  def print_schema
90
- print(@document)
85
+ print(@document) + "\n"
91
86
  end
92
87
 
93
88
  def print_type(type)
@@ -307,10 +307,15 @@ module GraphQL
307
307
  arguments: arguments,
308
308
  null: null,
309
309
  complexity: complexity,
310
- extensions: extensions,
311
310
  broadcastable: broadcastable?,
312
311
  }
313
312
 
313
+ # If there aren't any, then the returned array is `[].freeze`,
314
+ # but passing that along breaks some user code.
315
+ if (exts = extensions).any?
316
+ field_opts[:extensions] = exts
317
+ end
318
+
314
319
  if has_max_page_size?
315
320
  field_opts[:max_page_size] = max_page_size
316
321
  end
@@ -333,30 +338,32 @@ module GraphQL
333
338
  arg_defn = super(*args, from_resolver: true, **kwargs)
334
339
  own_arguments_loads_as_type[arg_defn.keyword] = loads if loads
335
340
 
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) })
341
+ if !method_defined?(:"load_#{arg_defn.keyword}")
342
+ if loads && arg_defn.type.list?
343
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
344
+ def load_#{arg_defn.keyword}(values)
345
+ argument = @arguments_by_keyword[:#{arg_defn.keyword}]
346
+ lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
347
+ context.schema.after_lazy(values) do |values2|
348
+ GraphQL::Execution::Lazy.all(values2.map { |value| load_application_object(argument, lookup_as_type, value, context) })
349
+ end
343
350
  end
351
+ RUBY
352
+ elsif loads
353
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
354
+ def load_#{arg_defn.keyword}(value)
355
+ argument = @arguments_by_keyword[:#{arg_defn.keyword}]
356
+ lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
357
+ load_application_object(argument, lookup_as_type, value, context)
358
+ end
359
+ RUBY
360
+ else
361
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
362
+ def load_#{arg_defn.keyword}(value)
363
+ value
364
+ end
365
+ RUBY
344
366
  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
367
  end
361
368
 
362
369
  arg_defn
@@ -372,16 +379,36 @@ module GraphQL
372
379
  # @param extension [Class] Extension class
373
380
  # @param options [Hash] Optional extension options
374
381
  def extension(extension, **options)
375
- extensions << {extension => options}
382
+ @own_extensions ||= []
383
+ @own_extensions << {extension => options}
376
384
  end
377
385
 
378
386
  # @api private
379
387
  def extensions
380
- @extensions ||= []
388
+ own_exts = @own_extensions
389
+ # Jump through some hoops to avoid creating arrays when we don't actually need them
390
+ if superclass.respond_to?(:extensions)
391
+ s_exts = superclass.extensions
392
+ if own_exts
393
+ if s_exts.any?
394
+ own_exts + s_exts
395
+ else
396
+ own_exts
397
+ end
398
+ else
399
+ s_exts
400
+ end
401
+ else
402
+ own_exts || EMPTY_ARRAY
403
+ end
381
404
  end
382
405
 
383
406
  private
384
407
 
408
+ def own_extensions
409
+ @own_extensions
410
+ end
411
+
385
412
  def own_arguments_loads_as_type
386
413
  @own_arguments_loads_as_type ||= {}
387
414
  end