graphql 1.9.3 → 1.9.4

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.
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