graphql 1.9.3 → 1.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/lib/graphql.rb +1 -0
  3. data/lib/graphql/analysis/ast/field_usage.rb +1 -1
  4. data/lib/graphql/analysis/ast/max_query_complexity.rb +1 -1
  5. data/lib/graphql/analysis/ast/query_complexity.rb +3 -2
  6. data/lib/graphql/execution/execute.rb +10 -6
  7. data/lib/graphql/execution/interpreter/runtime.rb +23 -12
  8. data/lib/graphql/execution/lookahead.rb +0 -4
  9. data/lib/graphql/integer_encoding_error.rb +9 -1
  10. data/lib/graphql/language/visitor.rb +6 -0
  11. data/lib/graphql/load_application_object_failed_error.rb +22 -0
  12. data/lib/graphql/query.rb +6 -1
  13. data/lib/graphql/query/arguments.rb +1 -1
  14. data/lib/graphql/query/variables.rb +14 -8
  15. data/lib/graphql/rake_task/validate.rb +1 -1
  16. data/lib/graphql/schema/field_extension.rb +2 -2
  17. data/lib/graphql/schema/input_object.rb +15 -6
  18. data/lib/graphql/schema/member/has_arguments.rb +80 -0
  19. data/lib/graphql/schema/member/instrumentation.rb +6 -0
  20. data/lib/graphql/schema/resolver.rb +8 -92
  21. data/lib/graphql/static_validation/base_visitor.rb +8 -0
  22. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -1
  23. data/lib/graphql/tracing.rb +2 -2
  24. data/lib/graphql/tracing/data_dog_tracing.rb +19 -0
  25. data/lib/graphql/tracing/platform_tracing.rb +2 -1
  26. data/lib/graphql/types.rb +1 -0
  27. data/lib/graphql/types/big_int.rb +19 -0
  28. data/lib/graphql/types/int.rb +1 -0
  29. data/lib/graphql/version.rb +1 -1
  30. data/spec/graphql/authorization_spec.rb +26 -0
  31. data/spec/graphql/language/visitor_spec.rb +19 -0
  32. data/spec/graphql/schema/argument_spec.rb +26 -1
  33. data/spec/graphql/schema/field_extension_spec.rb +24 -2
  34. data/spec/graphql/schema/input_object_spec.rb +119 -10
  35. data/spec/graphql/schema/introspection_system_spec.rb +9 -0
  36. data/spec/graphql/schema/resolver_spec.rb +2 -2
  37. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +2 -2
  38. data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +1 -1
  39. data/spec/graphql/static_validation/rules/required_input_object_attributes_are_present_spec.rb +16 -2
  40. data/spec/graphql/subscriptions_spec.rb +38 -0
  41. data/spec/graphql/tracing/new_relic_tracing_spec.rb +21 -0
  42. data/spec/graphql/types/big_int_spec.rb +24 -0
  43. data/spec/support/jazz.rb +11 -0
  44. metadata +13 -51
  45. data/spec/dummy/Gemfile.lock +0 -157
  46. data/spec/dummy/log/test.log +0 -199
  47. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/4w/4wzXRZrAkwKdgYaSE0pid5eB-fer8vSfSku_NPg4rMA.cache +0 -0
  48. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/7I/7IHVBiJT06QSpgLpLoJIxboQ0B-D_tMTxsvoezBTV3Q.cache +0 -1
  49. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/8w/8wY_SKagj8wHuwGNAAf6JnQ8joMbC6cEYpHrTAI8Urc.cache +0 -1
  50. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/AK/AKzz1u6bGb4auXcrObA_g5LL-oV0ejNGa448AgAi_WQ.cache +0 -1
  51. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/ET/ETW4uxvaYpruL8y6_ZptUH82ZowMaHIqvg5WexBFdEM.cache +0 -3
  52. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/F1/F1TWpjjyA56k9Z90n5B3xRn7DUdGjX73QCkYC6k07JQ.cache +0 -0
  53. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/F8/F8MUNRzORGFgr329fNM0xLaoWCXdv3BIalT7dsvLfjs.cache +0 -2
  54. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/KB/KB07ZaKNC5uXJ7TjLi-WqnY6g7dq8wWp_8N3HNjBNxg.cache +0 -2
  55. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Ms/MsKSimH_UCB-H1tLvDABDHuvGciuoW6kVqQWDrXU5FQ.cache +0 -0
  56. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Mt/Mtci-Kim50aPOmeClD4AIicKn1d1WJ0n454IjSd94sk.cache +0 -0
  57. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/QH/QHt3Tc1Y6M66Oo_pDuMyWrQNs4Pp3SMeZR5K1wJj2Ts.cache +0 -1
  58. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/XU/XU4k1OXnfMils5SrirorPvDSyDSqiOWLZNtmAH1HH8k.cache +0 -0
  59. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/ZI/ZIof7mZxWWCnraIFOCuV6a8QRWzKJXJnx2Xd7C0ZyX0.cache +0 -1
  60. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/cG/cGc_puuPS5pZKgUcy1Y_i1L6jl5UtsiIrMH59rTzR6c.cache +0 -3
  61. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/df/dfro_B6bx3KP1Go-7jEOqqZ2j4hVRseXIc3es9PKQno.cache +0 -1
  62. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/jO/jO1DfbqnG0mTULsjJJANc3fefrG2zt7DIMmcptMT628.cache +0 -1
  63. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/pE/pE7gO6pQ-z187Swb4hT554wmqsq-cNzgPWLrCz-LQQQ.cache +0 -0
  64. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/r9/r9iU1l58a6rxkZSW5RSC52_tD-_UQuHxoMVnkfJ7Mhs.cache +0 -1
  65. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/xi/xitPPFfPIyDMpaznV0sBBcw8eSCV8PJcLLWin78sCgE.cache +0 -0
  66. data/spec/dummy/tmp/screenshots/failures_test_it_handles_subscriptions.png +0 -0
  67. data/spec/integration/tmp/app/graphql/types/family_type.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: fe0eb7f5a621028df479067eb72143c397f34ecd
4
- data.tar.gz: db4afa75f08ba2a8a409837c962442b824779e05
2
+ SHA256:
3
+ metadata.gz: c4a10bf3dce4841a53a78308eab58db972aeeb6daade67cf019a45ad48d2c1f6
4
+ data.tar.gz: ca20d0a586c0076ee47b65c09e12a39d8c52b2db4a4c232cf9d1fee95202280c
5
5
  SHA512:
6
- metadata.gz: 9bccb51d411ce789817fa821216eae65c0e88faee075f3859c130ffb10a6c6c8fca7fd1a2455c8aff2d9ba7f7a378804528e4d3f5ea9a579167de10f77234e0a
7
- data.tar.gz: dba82fb8a24a3b309701278db3f6d95818090f22f128d3a7ed57703fcef9826949a1eec7449afddc5892b24e08cc1172d3b98c88f80f092dbaf0a06a9674323e
6
+ metadata.gz: c3ebc480c7e4c209b319f382aae6eed1f0d1c61c6886ad620950b51dc457863f9da1af8d451f382a22a467230daa95c5d8a28fd86d1bde22f499aa9a797d6f75
7
+ data.tar.gz: 86655364098750c3430395b637def8952a1bdb5082643ebe74fb0110d04114411f9077539621cd50c38ff151c64eb54c275ae0b23f248ac27a99f75e356f918d
@@ -106,3 +106,4 @@ require "graphql/deprecated_dsl"
106
106
  require "graphql/authorization"
107
107
  require "graphql/unauthorized_error"
108
108
  require "graphql/unauthorized_field_error"
109
+ require "graphql/load_application_object_failed_error"
@@ -9,7 +9,7 @@ module GraphQL
9
9
  @used_deprecated_fields = Set.new
10
10
  end
11
11
 
12
- def on_leave_field(parent, node, visitor)
12
+ def on_leave_field(node, parent, visitor)
13
13
  field_defn = visitor.field_definition
14
14
  field = "#{visitor.parent_type_definition.name}.#{field_defn.name}"
15
15
  @used_fields << field
@@ -9,7 +9,7 @@ module GraphQL
9
9
  def result
10
10
  return if query.max_complexity.nil?
11
11
 
12
- total_complexity = super
12
+ total_complexity = max_possible_complexity
13
13
 
14
14
  if total_complexity > query.max_complexity
15
15
  GraphQL::AnalysisError.new("Query has complexity of #{total_complexity}, which exceeds max complexity of #{query.max_complexity}")
@@ -13,7 +13,7 @@ module GraphQL
13
13
 
14
14
  # Overide this method to use the complexity result
15
15
  def result
16
- raise NotImplementedError
16
+ max_possible_complexity
17
17
  end
18
18
 
19
19
  def on_enter_field(node, parent, visitor)
@@ -57,7 +57,8 @@ module GraphQL
57
57
  visitor.leave_fragment_spread_inline(node)
58
58
  end
59
59
 
60
- def result
60
+ # @return [Integer]
61
+ def max_possible_complexity
61
62
  @complexities_on_type.last.max_possible_complexity
62
63
  end
63
64
 
@@ -50,12 +50,16 @@ module GraphQL
50
50
  operation = query.selected_operation
51
51
  op_type = operation.operation_type
52
52
  root_type = query.root_type_for_operation(op_type)
53
- resolve_selection(
54
- query.root_value,
55
- root_type,
56
- query.context,
57
- mutation: query.mutation?
58
- )
53
+ if query.context[:__root_unauthorized]
54
+ # This was set by member/instrumentation.rb so that we wouldn't continue.
55
+ else
56
+ resolve_selection(
57
+ query.root_value,
58
+ root_type,
59
+ query.context,
60
+ mutation: query.mutation?
61
+ )
62
+ end
59
63
  end
60
64
  end
61
65
 
@@ -49,9 +49,15 @@ module GraphQL
49
49
  root_type = legacy_root_type.metadata[:type_class] || raise("Invariant: type must be class-based: #{legacy_root_type}")
50
50
  object_proxy = root_type.authorized_new(query.root_value, context)
51
51
  object_proxy = schema.sync_lazy(object_proxy)
52
- path = []
53
- evaluate_selections(path, object_proxy, root_type, root_operation.selections, root_operation_type: root_op_type)
54
- nil
52
+ if object_proxy.nil?
53
+ # Root .authorized? returned false.
54
+ write_in_response([], nil)
55
+ nil
56
+ else
57
+ path = []
58
+ evaluate_selections(path, object_proxy, root_type, root_operation.selections, root_operation_type: root_op_type)
59
+ nil
60
+ end
55
61
  end
56
62
 
57
63
  def gather_selections(owner_object, owner_type, selections, selections_by_name)
@@ -158,8 +164,13 @@ module GraphQL
158
164
  object = field_defn.owner.authorized_new(object, context)
159
165
  end
160
166
 
167
+ begin
168
+ kwarg_arguments = arguments(object, field_defn, ast_node)
169
+ rescue GraphQL::ExecutionError => e
170
+ continue_value(next_path, e, field_defn, return_type.non_null?, ast_node)
171
+ next
172
+ end
161
173
 
162
- kwarg_arguments = arguments(object, field_defn, ast_node)
163
174
  # It might turn out that making arguments for every field is slow.
164
175
  # If we have to cache them, we'll need a more subtle approach here.
165
176
  field_defn.extras.each do |extra|
@@ -194,10 +205,10 @@ module GraphQL
194
205
 
195
206
  field_result = resolve_with_directives(object, ast_node) do
196
207
  # Actually call the field resolver and capture the result
197
- app_result = query.trace("execute_field", {field: field_defn, path: next_path}) do
208
+ app_result = query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path}) do
198
209
  field_defn.resolve(object, kwarg_arguments, context)
199
210
  end
200
- after_lazy(app_result, field: field_defn, path: next_path) do |inner_result|
211
+ after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path) do |inner_result|
201
212
  continue_value = continue_value(next_path, inner_result, field_defn, return_type.non_null?, ast_node)
202
213
  if HALT != continue_value
203
214
  continue_field(next_path, continue_value, field_defn, return_type, ast_node, next_selections, false)
@@ -271,7 +282,7 @@ module GraphQL
271
282
  r
272
283
  when "UNION", "INTERFACE"
273
284
  resolved_type_or_lazy = query.resolve_type(type, value)
274
- after_lazy(resolved_type_or_lazy, path: path, field: field) do |resolved_type|
285
+ after_lazy(resolved_type_or_lazy, owner: type, path: path, field: field) do |resolved_type|
275
286
  possible_types = query.possible_types(type)
276
287
 
277
288
  if !possible_types.include?(resolved_type)
@@ -291,7 +302,7 @@ module GraphQL
291
302
  rescue GraphQL::ExecutionError => err
292
303
  err
293
304
  end
294
- after_lazy(object_proxy, path: path, field: field) do |inner_object|
305
+ after_lazy(object_proxy, owner: type, path: path, field: field) do |inner_object|
295
306
  continue_value = continue_value(path, inner_object, field, is_non_null, ast_node)
296
307
  if HALT != continue_value
297
308
  response_hash = {}
@@ -312,7 +323,7 @@ module GraphQL
312
323
  idx += 1
313
324
  set_type_at_path(next_path, inner_type)
314
325
  # This will update `response_list` with the lazy
315
- after_lazy(inner_value, path: next_path, field: field) do |inner_inner_value|
326
+ after_lazy(inner_value, owner: inner_type, path: next_path, field: field) do |inner_inner_value|
316
327
  # reset `is_non_null` here and below, because the inner type will have its own nullability constraint
317
328
  continue_value = continue_value(next_path, inner_inner_value, field, false, ast_node)
318
329
  if HALT != continue_value
@@ -378,7 +389,7 @@ module GraphQL
378
389
  # @param field [GraphQL::Schema::Field]
379
390
  # @param eager [Boolean] Set to `true` for mutation root fields only
380
391
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
381
- def after_lazy(obj, field:, path:, eager: false)
392
+ def after_lazy(obj, owner:, field:, path:, eager: false)
382
393
  @interpreter_context[:current_path] = path
383
394
  @interpreter_context[:current_field] = field
384
395
  if schema.lazy?(obj)
@@ -387,14 +398,14 @@ module GraphQL
387
398
  @interpreter_context[:current_field] = field
388
399
  # Wrap the execution of _this_ method with tracing,
389
400
  # but don't wrap the continuation below
390
- inner_obj = query.trace("execute_field_lazy", {field: field, path: path}) do
401
+ inner_obj = query.trace("execute_field_lazy", {owner: owner, field: field, path: path}) do
391
402
  begin
392
403
  schema.sync_lazy(obj)
393
404
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
394
405
  yield(err)
395
406
  end
396
407
  end
397
- after_lazy(inner_obj, field: field, path: path, eager: eager) do |really_inner_obj|
408
+ after_lazy(inner_obj, owner: owner, field: field, path: path, eager: eager) do |really_inner_obj|
398
409
  yield(really_inner_obj)
399
410
  end
400
411
  end
@@ -9,10 +9,6 @@ module GraphQL
9
9
  # A field may get access to its lookahead by adding `extras: [:lookahead]`
10
10
  # to its configuration.
11
11
  #
12
- # __NOTE__: Lookahead for typed fragments (eg `node { ... on Thing { ... } }`)
13
- # hasn't been implemented yet. It's possible, I just didn't need it yet.
14
- # Feel free to open a PR or an issue if you want to add it.
15
- #
16
12
  # @example looking ahead in a field
17
13
  # field :articles, [Types::Article], null: false,
18
14
  # extras: [:lookahead]
@@ -1,12 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
+ # This error is raised when `Types::Int` is asked to return a value outside of 32-bit integer range.
4
+ #
5
+ # For values outside that range, consider:
6
+ #
7
+ # - `ID` for database primary keys or other identifiers
8
+ # - `GraphQL::Types::BigInt` for really big integer values
9
+ #
10
+ # @see GraphQL::Types::Int which raises this error
3
11
  class IntegerEncodingError < GraphQL::RuntimeTypeError
4
12
  # The value which couldn't be encoded
5
13
  attr_reader :integer_value
6
14
 
7
15
  def initialize(value)
8
16
  @integer_value = value
9
- super('Integer out of bounds.')
17
+ super("Integer out of bounds: #{value}. \nConsider using ID or GraphQL::Types::BigInt instead.")
10
18
  end
11
19
  end
12
20
  end
@@ -168,6 +168,8 @@ module GraphQL
168
168
  # Run the hooks for `node`, and if the hooks return a copy of `node`,
169
169
  # copy `parent` so that it contains the copy of that node as a child,
170
170
  # then return the copies
171
+ # If a non-array value is returned, consuming functions should ignore
172
+ # said value
171
173
  def on_node_with_modifications(node, parent)
172
174
  new_node_and_new_parent = visit_node(node, parent)
173
175
  if new_node_and_new_parent.is_a?(Array)
@@ -181,6 +183,10 @@ module GraphQL
181
183
  # The user-provided hook requested to remove this node
182
184
  new_parent = new_parent && new_parent.delete_child(node)
183
185
  return nil, new_parent
186
+ elsif new_node_and_new_parent.none? { |n| n == nil || n.class < Nodes::AbstractNode }
187
+ # The user-provided hook returned an array of who-knows-what
188
+ # return nil here to signify that no changes should be made
189
+ nil
184
190
  else
185
191
  new_node_and_new_parent
186
192
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ # Raised when a argument is configured with `loads:` and the client provides an `ID`,
5
+ # but no object is loaded for that ID.
6
+ #
7
+ # @see GraphQL::Schema::Member::HasArguments::ArgumentObjectLoader#load_application_object_failed, A hook which you can override in resolvers, mutations and input objects.
8
+ class LoadApplicationObjectFailedError < GraphQL::ExecutionError
9
+ # @return [GraphQL::Schema::Argument] the argument definition for the argument that was looked up
10
+ attr_reader :argument
11
+ # @return [String] The ID provided by the client
12
+ attr_reader :id
13
+ # @return [Object] The value found with this ID
14
+ attr_reader :object
15
+ def initialize(argument:, id:, object:)
16
+ @id = id
17
+ @argument = argument
18
+ @object = object
19
+ super("No object found for `#{argument.graphql_name}: #{id.inspect}`")
20
+ end
21
+ end
22
+ end
@@ -40,7 +40,7 @@ module GraphQL
40
40
  # @return [Boolean] if false, static validation is skipped (execution behavior for invalid queries is undefined)
41
41
  attr_accessor :validate
42
42
 
43
- attr_accessor :query_string
43
+ attr_writer :query_string
44
44
 
45
45
  # @return [GraphQL::Language::Nodes::Document]
46
46
  def document
@@ -135,6 +135,11 @@ module GraphQL
135
135
  end
136
136
  end
137
137
 
138
+ # If a document was provided to `GraphQL::Schema#execute` instead of the raw query string, we will need to get it from the document
139
+ def query_string
140
+ @query_string ||= (document ? document.to_query_string : nil)
141
+ end
142
+
138
143
  def_delegators :@schema, :interpreter?
139
144
 
140
145
  def subscription_update?
@@ -19,7 +19,7 @@ module GraphQL
19
19
  method_names = [expose_as, expose_as_underscored].uniq
20
20
  method_names.each do |method_name|
21
21
  # Don't define a helper method if it would override something.
22
- if instance_methods.include?(method_name)
22
+ if instance_methods.include?(method_name.to_sym)
23
23
  warn(
24
24
  "Unable to define a helper for argument with name '#{method_name}' "\
25
25
  "as this is a reserved name. If you're using an argument such as "\
@@ -32,20 +32,26 @@ module GraphQL
32
32
 
33
33
  begin
34
34
  validation_result = variable_type.validate_input(provided_value, ctx)
35
- rescue GraphQL::CoercionError => ex
35
+ if validation_result.valid?
36
+ if value_was_provided
37
+ # Add the variable if a value was provided
38
+ memo[variable_name] = variable_type.coerce_input(provided_value, ctx)
39
+ elsif default_value != nil
40
+ # Add the variable if it wasn't provided but it has a default value (including `null`)
41
+ memo[variable_name] = GraphQL::Query::LiteralInput.coerce(variable_type, default_value, self)
42
+ end
43
+ end
44
+ rescue GraphQL::CoercionError, GraphQL::ExecutionError => ex
45
+ # TODO: This should really include the path to the problematic node in the variable value
46
+ # like InputValidationResults generated by validate_non_null_input but unfortunately we don't
47
+ # have this information available in the coerce_input call chain. Note this path is the path
48
+ # that appears under errors.extensions.problems.path and NOT the result path under errors.path.
36
49
  validation_result = GraphQL::Query::InputValidationResult.new
37
50
  validation_result.add_problem(ex.message)
38
51
  end
39
52
 
40
53
  if !validation_result.valid?
41
- # This finds variables that were required but not provided
42
54
  @errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result)
43
- elsif value_was_provided
44
- # Add the variable if a value was provided
45
- memo[variable_name] = variable_type.coerce_input(provided_value, ctx)
46
- elsif default_value != nil
47
- # Add the variable if it wasn't provided but it has a default value (including `null`)
48
- memo[variable_name] = GraphQL::Query::LiteralInput.coerce(variable_type, default_value, self)
49
55
  end
50
56
  end
51
57
  end
@@ -37,7 +37,7 @@ module GraphQL
37
37
  # Remove final newline from .txt file
38
38
  github_digest = Net::HTTP.get(github_uri).chomp
39
39
 
40
- docs_uri = URI("https://raw.githubusercontent.com/rmosolgo/graphql-ruby/master/guides/pro/checksums/graphql-pro-#{version}.txt")
40
+ docs_uri = URI("https://graphql-ruby.org/pro/checksums/graphql-pro-#{version}.txt")
41
41
  docs_digest = Net::HTTP.get(docs_uri).chomp
42
42
 
43
43
  if docs_digest == gem_digest && github_digest == gem_digest
@@ -18,10 +18,10 @@ module GraphQL
18
18
  # Called when the extension is mounted with `extension(name, options)`.
19
19
  # The instance is frozen to avoid improper use of state during execution.
20
20
  # @param field [GraphQL::Schema::Field] The field where this extension was mounted
21
- # @param options [Object] The second argument to `extension`, or `nil` if nothing was passed.
21
+ # @param options [Object] The second argument to `extension`, or `{}` if nothing was passed.
22
22
  def initialize(field:, options:)
23
23
  @field = field
24
- @options = options
24
+ @options = options || {}
25
25
  apply
26
26
  freeze
27
27
  end
@@ -17,7 +17,9 @@ module GraphQL
17
17
  @ruby_style_hash = @arguments.to_kwargs
18
18
  end
19
19
  # Apply prepares, not great to have it duplicated here.
20
+ @arguments_by_keyword = {}
20
21
  self.class.arguments.each do |name, arg_defn|
22
+ @arguments_by_keyword[arg_defn.keyword] = arg_defn
21
23
  ruby_kwargs_key = arg_defn.keyword
22
24
  if @ruby_style_hash.key?(ruby_kwargs_key) && arg_defn.prepare
23
25
  @ruby_style_hash[ruby_kwargs_key] = arg_defn.prepare_value(self, @ruby_style_hash[ruby_kwargs_key])
@@ -81,13 +83,20 @@ module GraphQL
81
83
  # @return [Class<GraphQL::Arguments>]
82
84
  attr_accessor :arguments_class
83
85
 
84
- def argument(*args)
85
- argument_defn = super
86
+ def argument(name, type, *rest, loads: nil, **kwargs, &block)
87
+ argument_defn = super(*argument_with_loads(name, type, *rest, loads: loads, **kwargs, &block))
86
88
  # Add a method access
87
- arg_name = argument_defn.graphql_definition.name
88
- method_name = Member::BuildType.underscore(arg_name).to_sym
89
+ method_name = argument_defn.keyword
89
90
  define_method(method_name) do
90
- @ruby_style_hash[method_name]
91
+ value = @ruby_style_hash[method_name]
92
+ argument = @arguments_by_keyword[method_name]
93
+ if loads && argument_defn.type.list?
94
+ GraphQL::Execution::Lazy.all(value.map { |val| load_application_object(argument, loads, val) })
95
+ elsif loads
96
+ load_application_object(argument, loads, value)
97
+ else
98
+ value
99
+ end
91
100
  end
92
101
  end
93
102
 
@@ -113,4 +122,4 @@ module GraphQL
113
122
  end
114
123
  end
115
124
  end
116
- end
125
+ end
@@ -5,10 +5,33 @@ module GraphQL
5
5
  module HasArguments
6
6
  def self.included(cls)
7
7
  cls.extend(ArgumentClassAccessor)
8
+ cls.include(ArgumentObjectLoader)
8
9
  end
9
10
 
10
11
  def self.extended(cls)
11
12
  cls.extend(ArgumentClassAccessor)
13
+ cls.include(ArgumentObjectLoader)
14
+ end
15
+
16
+ def argument_with_loads(name, type, *rest, loads: nil, **kwargs)
17
+ if loads
18
+ name_as_string = name.to_s
19
+
20
+ inferred_arg_name = case name_as_string
21
+ when /_id$/
22
+ name_as_string.sub(/_id$/, "").to_sym
23
+ when /_ids$/
24
+ name_as_string.sub(/_ids$/, "")
25
+ .sub(/([^s])$/, "\\1s")
26
+ .to_sym
27
+ else
28
+ name
29
+ end
30
+
31
+ kwargs[:as] ||= inferred_arg_name
32
+ end
33
+
34
+ return [name, type, *rest, **kwargs]
12
35
  end
13
36
 
14
37
  # @see {GraphQL::Schema::Argument#initialize} for parameters
@@ -53,6 +76,63 @@ module GraphQL
53
76
  end
54
77
  end
55
78
 
79
+ module ArgumentObjectLoader
80
+ # Look up the corresponding object for a provided ID.
81
+ # By default, it uses Relay-style {Schema.object_from_id},
82
+ # override this to find objects another way.
83
+ #
84
+ # @param type [Class, Module] A GraphQL type definition
85
+ # @param id [String] A client-provided to look up
86
+ # @param context [GraphQL::Query::Context] the current context
87
+ def object_from_id(type, id, context)
88
+ context.schema.object_from_id(id, context)
89
+ end
90
+
91
+ def load_application_object(argument, lookup_as_type, id)
92
+ # See if any object can be found for this ID
93
+ loaded_application_object = object_from_id(lookup_as_type, id, context)
94
+ context.schema.after_lazy(loaded_application_object) do |application_object|
95
+ if application_object.nil?
96
+ err = GraphQL::LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
97
+ load_application_object_failed(err)
98
+ end
99
+ # Double-check that the located object is actually of this type
100
+ # (Don't want to allow arbitrary access to objects this way)
101
+ resolved_application_object_type = context.schema.resolve_type(lookup_as_type, application_object, context)
102
+ context.schema.after_lazy(resolved_application_object_type) do |application_object_type|
103
+ possible_object_types = context.schema.possible_types(lookup_as_type)
104
+ if !possible_object_types.include?(application_object_type)
105
+ err = GraphQL::LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
106
+ load_application_object_failed(err)
107
+ else
108
+ # This object was loaded successfully
109
+ # and resolved to the right type,
110
+ # now apply the `.authorized?` class method if there is one
111
+ if (class_based_type = application_object_type.metadata[:type_class])
112
+ context.schema.after_lazy(class_based_type.authorized?(application_object, context)) do |authed|
113
+ if authed
114
+ application_object
115
+ else
116
+ raise GraphQL::UnauthorizedError.new(
117
+ object: application_object,
118
+ type: class_based_type,
119
+ context: context,
120
+ )
121
+ end
122
+ end
123
+ else
124
+ application_object
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def load_application_object_failed(err)
132
+ raise err
133
+ end
134
+ end
135
+
56
136
  def own_arguments
57
137
  @own_arguments ||= {}
58
138
  end