graphql 2.5.11 → 2.5.23

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  4. data/lib/graphql/dashboard/application_controller.rb +41 -0
  5. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  6. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  7. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  8. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  9. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  10. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  11. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  12. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  13. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  15. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  16. data/lib/graphql/dashboard.rb +11 -73
  17. data/lib/graphql/dataloader/async_dataloader.rb +22 -11
  18. data/lib/graphql/dataloader/null_dataloader.rb +48 -10
  19. data/lib/graphql/dataloader.rb +75 -23
  20. data/lib/graphql/date_encoding_error.rb +1 -1
  21. data/lib/graphql/execution/interpreter/resolve.rb +7 -13
  22. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  23. data/lib/graphql/execution/interpreter/runtime.rb +24 -18
  24. data/lib/graphql/execution/interpreter.rb +8 -22
  25. data/lib/graphql/execution/lazy.rb +1 -1
  26. data/lib/graphql/execution/multiplex.rb +1 -1
  27. data/lib/graphql/execution/next/field_resolve_step.rb +743 -0
  28. data/lib/graphql/execution/next/load_argument_step.rb +64 -0
  29. data/lib/graphql/execution/next/prepare_object_step.rb +129 -0
  30. data/lib/graphql/execution/next/runner.rb +411 -0
  31. data/lib/graphql/execution/next/selections_step.rb +37 -0
  32. data/lib/graphql/execution/next.rb +72 -0
  33. data/lib/graphql/execution.rb +8 -4
  34. data/lib/graphql/execution_error.rb +17 -10
  35. data/lib/graphql/introspection/directive_type.rb +7 -3
  36. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  37. data/lib/graphql/introspection/entry_points.rb +11 -3
  38. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  39. data/lib/graphql/introspection/field_type.rb +13 -5
  40. data/lib/graphql/introspection/input_value_type.rb +21 -13
  41. data/lib/graphql/introspection/type_type.rb +64 -28
  42. data/lib/graphql/invalid_null_error.rb +11 -5
  43. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  44. data/lib/graphql/language.rb +21 -12
  45. data/lib/graphql/pagination/connection.rb +2 -0
  46. data/lib/graphql/pagination/connections.rb +32 -0
  47. data/lib/graphql/query/context.rb +4 -3
  48. data/lib/graphql/query/null_context.rb +9 -3
  49. data/lib/graphql/schema/argument.rb +12 -0
  50. data/lib/graphql/schema/build_from_definition.rb +10 -1
  51. data/lib/graphql/schema/directive.rb +22 -4
  52. data/lib/graphql/schema/field/connection_extension.rb +15 -35
  53. data/lib/graphql/schema/field/scope_extension.rb +22 -13
  54. data/lib/graphql/schema/field.rb +79 -48
  55. data/lib/graphql/schema/field_extension.rb +33 -0
  56. data/lib/graphql/schema/list.rb +1 -1
  57. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -1
  58. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  59. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  60. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  61. data/lib/graphql/schema/member/has_fields.rb +86 -5
  62. data/lib/graphql/schema/member.rb +5 -0
  63. data/lib/graphql/schema/non_null.rb +1 -1
  64. data/lib/graphql/schema/object.rb +1 -0
  65. data/lib/graphql/schema/resolver.rb +60 -1
  66. data/lib/graphql/schema/subscription.rb +0 -2
  67. data/lib/graphql/schema/validator/required_validator.rb +33 -2
  68. data/lib/graphql/schema/visibility/profile.rb +68 -49
  69. data/lib/graphql/schema/visibility.rb +3 -3
  70. data/lib/graphql/schema/wrapper.rb +7 -1
  71. data/lib/graphql/schema.rb +53 -10
  72. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  73. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  74. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  75. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  76. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  77. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  78. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  79. data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
  80. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  81. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  82. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  83. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  84. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  85. data/lib/graphql/static_validation/validation_context.rb +1 -1
  86. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  87. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +25 -1
  88. data/lib/graphql/subscriptions/event.rb +1 -0
  89. data/lib/graphql/subscriptions.rb +21 -1
  90. data/lib/graphql/testing/helpers.rb +12 -9
  91. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  92. data/lib/graphql/testing.rb +1 -0
  93. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  94. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  95. data/lib/graphql/tracing/perfetto_trace.rb +209 -79
  96. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  97. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  98. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  99. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  100. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  101. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  102. data/lib/graphql/unauthorized_error.rb +9 -1
  103. data/lib/graphql/version.rb +1 -1
  104. data/lib/graphql.rb +8 -2
  105. metadata +17 -3
@@ -20,6 +20,23 @@ module GraphQL
20
20
  dataloader.with(source_class, *source_args).load(load_key)
21
21
  end
22
22
 
23
+ # A shortcut method for loading many keys from a source.
24
+ # Identical to `dataloader.with(source_class, *source_args).load_all(load_keys)`
25
+ #
26
+ # @example
27
+ # field :score, Integer, resolve_batch: true
28
+ #
29
+ # def self.score(posts)
30
+ # dataload_all(PostScoreSource, posts.map(&:id))
31
+ # end
32
+ #
33
+ # @param source_class [Class<GraphQL::Dataloader::Source>]
34
+ # @param source_args [Array<Object>] Any extra parameters defined in `source_class`'s `initialize` method
35
+ # @param load_keys [Array<Object>] The keys to look up using `def fetch`
36
+ def dataload_all(source_class, *source_args, load_keys)
37
+ dataloader.with(source_class, *source_args).load_all(load_keys)
38
+ end
39
+
23
40
  # Find an object with ActiveRecord via {Dataloader::ActiveRecordSource}.
24
41
  # @param model [Class<ActiveRecord::Base>]
25
42
  # @param find_by_value [Object] Usually an `id`, might be another value if `find_by:` is also provided
@@ -39,6 +56,16 @@ module GraphQL
39
56
  source.load(find_by_value)
40
57
  end
41
58
 
59
+ # @see dataload_record Like `dataload_record`, but accepts an Array of `find_by_values`
60
+ def dataload_all_records(model, find_by_values, find_by: nil)
61
+ source = if find_by
62
+ dataloader.with(Dataloader::ActiveRecordSource, model, find_by: find_by)
63
+ else
64
+ dataloader.with(Dataloader::ActiveRecordSource, model)
65
+ end
66
+ source.load_all(find_by_values)
67
+ end
68
+
42
69
  # Look up an associated record using a Rails association (via {Dataloader::ActiveRecordAssociationSource})
43
70
  # @param association_name [Symbol] A `belongs_to` or `has_one` association. (If a `has_many` association is named here, it will be selected without pagination.)
44
71
  # @param record [ActiveRecord::Base] The object that the association belongs to.
@@ -56,6 +83,16 @@ module GraphQL
56
83
  end
57
84
  source.load(record)
58
85
  end
86
+
87
+ # @see dataload_association Like `dataload_assocation` but accepts an Array of records (required param)
88
+ def dataload_all_associations(records, association_name, scope: nil)
89
+ source = if scope
90
+ dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name, scope)
91
+ else
92
+ dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name)
93
+ end
94
+ source.load_all(records)
95
+ end
59
96
  end
60
97
  end
61
98
  end
@@ -5,11 +5,88 @@ module GraphQL
5
5
  class Member
6
6
  # Shared code for Objects, Interfaces, Mutations, Subscriptions
7
7
  module HasFields
8
+ include EmptyObjects
8
9
  # Add a field to this object or interface with the given definition
9
- # @see {GraphQL::Schema::Field#initialize} for method signature
10
+ # @param name_positional [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); `name:` keyword is also accepted
11
+ # @param type_positional [Class, GraphQL::BaseType, Array] The return type of this field; `type:` keyword is also accepted
12
+ # @param desc_positional [String] Field description; `description:` keyword is also accepted
13
+ # @option kwargs [Symbol] :name The underscore-cased version of this field name (will be camelized for the GraphQL API); positional argument also accepted
14
+ # @option kwargs [Class, GraphQL::BaseType, Array] :type The return type of this field; positional argument is also accepted
15
+ # @option kwargs [Boolean] :null (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
16
+ # @option kwargs [String] :description Field description; positional argument also accepted
17
+ # @option kwargs [String] :comment Field comment
18
+ # @option kwargs [String] :deprecation_reason If present, the field is marked "deprecated" with this message
19
+ # @option kwargs [Symbol] :method The method to call on the underlying object to resolve this field (defaults to `name`)
20
+ # @option kwargs [String, Symbol] :hash_key The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
21
+ # @option kwargs [Array<String, Symbol>] :dig The nested hash keys to lookup on the underlying hash to resolve this field using dig
22
+ # @option kwargs [Symbol, true] :resolver_method The method on the type to call to resolve this field (defaults to `name`)
23
+ # @option kwargs [Symbol, true] :resolve_static Used by {Schema.execute_next} to produce a single value, shared by all objects which resolve this field. Called on the owner type class with `context, **arguments`
24
+ # @option kwargs [Symbol, true] :resolve_batch Used by {Schema.execute_next} map `objects` to a same-sized Array of results. Called on the owner type class with `objects, context, **arguments`.
25
+ # @option kwargs [Symbol, true] :resolve_each Used by {Schema.execute_next} to get a value value for each item. Called on the owner type class with `object, context, **arguments`.
26
+ # @option kwargs [Symbol, true] :resolve_legacy_instance_method Used by {Schema.execute_next} to get a value value for each item. Calls an instance method on the object type class.
27
+ # @option kwargs [Boolean] :connection `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
28
+ # @option kwargs [Class] :connection_extension The extension to add, to implement connections. If `nil`, no extension is added.
29
+ # @option kwargs [Integer, nil] :max_page_size For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
30
+ # @option kwargs [Integer, nil] :default_page_size For connections, the default number of items to return from this field, or `nil` to return unlimited results.
31
+ # @option kwargs [Boolean] :introspection If true, this field will be marked as `#introspection?` and the name may begin with `__`
32
+ # @option kwargs [{String=>GraphQL::Schema::Argument, Hash}] :arguments Arguments for this field (may be added in the block, also)
33
+ # @option kwargs [Boolean] :camelize If true, the field name will be camelized when building the schema
34
+ # @option kwargs [Numeric] :complexity When provided, set the complexity for this field
35
+ # @option kwargs [Boolean] :scope If true, the return type's `.scope_items` method will be called on the return value
36
+ # @option kwargs [Symbol, String] :subscription_scope A key in `context` which will be used to scope subscription payloads
37
+ # @option kwargs [Array<Class, Hash<Class => Object>>] :extensions Named extensions to apply to this field (see also {#extension})
38
+ # @option kwargs [Hash{Class => Hash}] :directives Directives to apply to this field
39
+ # @option kwargs [Boolean] :trace If true, a {GraphQL::Tracing} tracer will measure this scalar field
40
+ # @option kwargs [Boolean] :broadcastable Whether or not this field can be distributed in subscription broadcasts
41
+ # @option kwargs [Language::Nodes::FieldDefinition, nil] :ast_node If this schema was parsed from definition, this AST node defined the field
42
+ # @option kwargs [Boolean] :method_conflict_warning If false, skip the warning if this field's method conflicts with a built-in method
43
+ # @option kwargs [Array<Hash>] :validates Configurations for validating this field
44
+ # @option kwargs [Object] :fallback_value A fallback value if the method is not defined
45
+ # @option kwargs [Class<GraphQL::Schema::Mutation>] :mutation
46
+ # @option kwargs [Class<GraphQL::Schema::Resolver>] :resolver
47
+ # @option kwargs [Class<GraphQL::Schema::Subscription>] :subscription
48
+ # @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby)
49
+ # @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby)
50
+ # @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby)
51
+ # @option kwargs [Class, Hash] :dataload Shorthand for dataloader lookups
52
+ # @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field
53
+ # @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled.
54
+ # @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
55
+ # @yieldparam field [GraphQL::Schema::Field] The newly-created field instance
56
+ # @yieldreturn [void]
10
57
  # @return [GraphQL::Schema::Field]
11
- def field(*args, **kwargs, &block)
12
- field_defn = field_class.from_options(*args, owner: self, **kwargs, &block)
58
+ def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &definition_block)
59
+ resolver = kwargs.delete(:resolver)
60
+ mutation = kwargs.delete(:mutation)
61
+ subscription = kwargs.delete(:subscription)
62
+ if (resolver_class = resolver || mutation || subscription)
63
+ # Add a reference to that parent class
64
+ kwargs[:resolver_class] = resolver_class
65
+ end
66
+
67
+ kwargs[:name] ||= name_positional
68
+ if !type_positional.nil?
69
+ if desc_positional
70
+ if kwargs[:description]
71
+ raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{kwargs[:description].inspect})"
72
+ end
73
+
74
+ kwargs[:description] = desc_positional
75
+ kwargs[:type] = type_positional
76
+ elsif (resolver || mutation) && type_positional.is_a?(String)
77
+ # The return type should be copied from the resolver, and the second positional argument is the description
78
+ kwargs[:description] = type_positional
79
+ else
80
+ kwargs[:type] = type_positional
81
+ end
82
+
83
+ if type_positional.is_a?(Class) && type_positional < GraphQL::Schema::Mutation
84
+ raise ArgumentError, "Use `field #{name_positional.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
85
+ end
86
+ end
87
+
88
+ kwargs[:owner] = self
89
+ field_defn = field_class.new(**kwargs, &definition_block)
13
90
  add_field(field_defn)
14
91
  field_defn
15
92
  end
@@ -73,10 +150,14 @@ module GraphQL
73
150
 
74
151
  def global_id_field(field_name, **kwargs)
75
152
  type = self
76
- field field_name, "ID", **kwargs, null: false
153
+ field field_name, "ID", **kwargs, null: false, resolve_each: true
77
154
  define_method(field_name) do
78
155
  context.schema.id_from_object(object, type, context)
79
156
  end
157
+
158
+ define_singleton_method(field_name) do |object, context|
159
+ context.schema.id_from_object(object, type, context)
160
+ end
80
161
  end
81
162
 
82
163
  # @param new_has_no_fields [Boolean] Call with `true` to make this Object type ignore the requirement to have any defined fields.
@@ -232,7 +313,7 @@ module GraphQL
232
313
  end
233
314
  end
234
315
 
235
- # @param [GraphQL::Schema::Field]
316
+ # @param field_defn [GraphQL::Schema::Field]
236
317
  # @return [String] A warning to give when this field definition might conflict with a built-in method
237
318
  def conflict_field_name_warning(field_defn)
238
319
  "#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
@@ -2,6 +2,7 @@
2
2
  require 'graphql/schema/member/base_dsl_methods'
3
3
  require 'graphql/schema/member/graphql_type_names'
4
4
  require 'graphql/schema/member/has_ast_node'
5
+ require 'graphql/schema/member/has_authorization'
5
6
  require 'graphql/schema/member/has_dataloader'
6
7
  require 'graphql/schema/member/has_directives'
7
8
  require 'graphql/schema/member/has_deprecation_reason'
@@ -31,6 +32,10 @@ module GraphQL
31
32
  extend HasPath
32
33
  extend HasAstNode
33
34
  extend HasDirectives
35
+
36
+ def self.authorizes?(_ctx)
37
+ false
38
+ end
34
39
  end
35
40
  end
36
41
  end
@@ -24,7 +24,7 @@ module GraphQL
24
24
  end
25
25
 
26
26
  def to_type_signature
27
- "#{@of_type.to_type_signature}!"
27
+ @type_signature ||= -"#{@of_type.to_type_signature}!"
28
28
  end
29
29
 
30
30
  def inspect
@@ -5,6 +5,7 @@ require "graphql/query/null_context"
5
5
  module GraphQL
6
6
  class Schema
7
7
  class Object < GraphQL::Schema::Member
8
+ extend GraphQL::Schema::Member::HasAuthorization
8
9
  extend GraphQL::Schema::Member::HasFields
9
10
  extend GraphQL::Schema::Member::HasInterfaces
10
11
  include Member::HasDataloader
@@ -23,6 +23,7 @@ module GraphQL
23
23
  # Really we only need description & comment from here, but:
24
24
  extend Schema::Member::BaseDSLMethods
25
25
  extend GraphQL::Schema::Member::HasArguments
26
+ extend GraphQL::Schema::Member::HasAuthorization
26
27
  extend GraphQL::Schema::Member::HasValidators
27
28
  include Schema::Member::HasPath
28
29
  extend Schema::Member::HasPath
@@ -45,8 +46,10 @@ module GraphQL
45
46
  @prepared_arguments = nil
46
47
  end
47
48
 
49
+ attr_accessor :exec_result, :exec_index, :field_resolve_step, :raw_arguments
50
+
48
51
  # @return [Object] The application object this field is being resolved on
49
- attr_reader :object
52
+ attr_accessor :object
50
53
 
51
54
  # @return [GraphQL::Query::Context]
52
55
  attr_reader :context
@@ -54,6 +57,58 @@ module GraphQL
54
57
  # @return [GraphQL::Schema::Field]
55
58
  attr_reader :field
56
59
 
60
+ attr_writer :prepared_arguments
61
+
62
+ def call
63
+ if self.class < Schema::HasSingleInputArgument
64
+ @prepared_arguments = @prepared_arguments[:input]
65
+ end
66
+ q = context.query
67
+ trace_objs = [object]
68
+ q.current_trace.begin_execute_field(field, @prepared_arguments, trace_objs, q)
69
+ begin
70
+ is_authed, new_return_value = authorized?(**@prepared_arguments)
71
+ rescue GraphQL::UnauthorizedError => err
72
+ new_return_value = q.schema.unauthorized_object(err)
73
+ is_authed = true # the error was handled
74
+ end
75
+
76
+ if (runner = @field_resolve_step.runner).resolves_lazies && runner.schema.lazy?(is_authed)
77
+ is_authed, new_return_value = runner.schema.sync_lazy(is_authed)
78
+ end
79
+
80
+ result = if is_authed
81
+ Schema::Validator.validate!(self.class.validators, object, context, @prepared_arguments, as: @field)
82
+ if q.subscription? && @field.owner == context.schema.subscription
83
+ # This needs to use arguments without `loads:`
84
+ @original_arguments = @field_resolve_step.coerce_arguments(@field, @field_resolve_step.ast_node.arguments, false)
85
+ end
86
+ call_resolve(@prepared_arguments)
87
+ elsif new_return_value.nil?
88
+ err = UnauthorizedFieldError.new(object: object, type: @field_resolve_step.parent_type, context: context, field: @field)
89
+ context.schema.unauthorized_field(err)
90
+ else
91
+ new_return_value
92
+ end
93
+ q = context.query
94
+ q.current_trace.end_execute_field(field, @prepared_arguments, trace_objs, q, [result])
95
+ exec_result[exec_index] = result
96
+ rescue RuntimeError => err
97
+ exec_result[exec_index] = err
98
+ rescue StandardError => stderr
99
+ exec_result[exec_index] = begin
100
+ context.query.handle_or_reraise(stderr)
101
+ rescue GraphQL::ExecutionError => ex_err
102
+ ex_err
103
+ end
104
+ ensure
105
+ field_pending_steps = field_resolve_step.pending_steps
106
+ field_pending_steps.delete(self)
107
+ if field_pending_steps.size == 0 && field_resolve_step.field_results
108
+ field_resolve_step.runner.add_step(field_resolve_step)
109
+ end
110
+ end
111
+
57
112
  def arguments
58
113
  @prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
59
114
  end
@@ -154,6 +209,10 @@ module GraphQL
154
209
  authorize_arguments(args, inputs)
155
210
  end
156
211
 
212
+ def self.authorizes?(context)
213
+ self.instance_method(:authorized?).owner != GraphQL::Schema::Resolver
214
+ end
215
+
157
216
  # Called when an object loaded by `loads:` fails the `.authorized?` check for its resolved GraphQL object type.
158
217
  #
159
218
  # By default, the error is re-raised and passed along to {{Schema.unauthorized_object}}.
@@ -15,8 +15,6 @@ module GraphQL
15
15
  extend GraphQL::Schema::Resolver::HasPayloadType
16
16
  extend GraphQL::Schema::Member::HasFields
17
17
  NO_UPDATE = :no_update
18
- # The generated payload type is required; If there's no payload,
19
- # propagate null.
20
18
  null false
21
19
 
22
20
  # @api private
@@ -8,6 +8,13 @@ module GraphQL
8
8
  #
9
9
  # (This is for specifying mutually exclusive sets of arguments.)
10
10
  #
11
+ # If you use {GraphQL::Schema::Visibility} to hide all the arguments in a `one_of: [..]` set,
12
+ # then a developer-facing {GraphQL::Error} will be raised during execution. Pass `allow_all_hidden: true` to
13
+ # skip validation in this case instead.
14
+ #
15
+ # This validator also implements `argument ... required: :nullable`. If an argument has `required: :nullable`
16
+ # but it's hidden with {GraphQL::Schema::Visibility}, then this validator doesn't run.
17
+ #
11
18
  # @example Require exactly one of these arguments
12
19
  #
13
20
  # field :update_amount, IngredientAmount, null: false do
@@ -37,15 +44,17 @@ module GraphQL
37
44
  class RequiredValidator < Validator
38
45
  # @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
39
46
  # @param argument [Symbol] An argument that is required for this field
47
+ # @param allow_all_hidden [Boolean] If `true`, then this validator won't run if all the `one_of: ...` arguments have been hidden
40
48
  # @param message [String]
41
- def initialize(one_of: nil, argument: nil, message: nil, **default_options)
49
+ def initialize(one_of: nil, argument: nil, allow_all_hidden: nil, message: nil, **default_options)
42
50
  @one_of = if one_of
43
51
  one_of
44
52
  elsif argument
45
- [argument]
53
+ [ argument ]
46
54
  else
47
55
  raise ArgumentError, "`one_of:` or `argument:` must be given in `validates required: {...}`"
48
56
  end
57
+ @allow_all_hidden = allow_all_hidden.nil? ? !!argument : allow_all_hidden
49
58
  @message = message
50
59
  super(**default_options)
51
60
  end
@@ -54,10 +63,17 @@ module GraphQL
54
63
  fully_matched_conditions = 0
55
64
  partially_matched_conditions = 0
56
65
 
66
+ visible_keywords = context.types.arguments(@validated).map(&:keyword)
67
+ no_visible_conditions = true
68
+
57
69
  if !value.nil?
58
70
  @one_of.each do |one_of_condition|
59
71
  case one_of_condition
60
72
  when Symbol
73
+ if no_visible_conditions && visible_keywords.include?(one_of_condition)
74
+ no_visible_conditions = false
75
+ end
76
+
61
77
  if value.key?(one_of_condition)
62
78
  fully_matched_conditions += 1
63
79
  end
@@ -66,6 +82,9 @@ module GraphQL
66
82
  full_match = true
67
83
 
68
84
  one_of_condition.each do |k|
85
+ if no_visible_conditions && visible_keywords.include?(k)
86
+ no_visible_conditions = false
87
+ end
69
88
  if value.key?(k)
70
89
  any_match = true
71
90
  else
@@ -88,6 +107,18 @@ module GraphQL
88
107
  end
89
108
  end
90
109
 
110
+ if no_visible_conditions
111
+ if @allow_all_hidden
112
+ return nil
113
+ else
114
+ raise GraphQL::Error, <<~ERR
115
+ #{@validated.path} validates `required: ...` but all required arguments were hidden.
116
+
117
+ Update your schema definition to allow the client to see some fields or skip validation by adding `required: { ..., allow_all_hidden: true }`
118
+ ERR
119
+ end
120
+ end
121
+
91
122
  if fully_matched_conditions == 1 && partially_matched_conditions == 0
92
123
  nil # OK
93
124
  else
@@ -54,6 +54,9 @@ module GraphQL
54
54
  @cached_fields.default_proc = nil
55
55
  @cached_arguments.default_proc = nil
56
56
  @loadable_possible_types.default_proc = nil
57
+ @cached_field_result.default_proc = nil
58
+ @cached_field_result.each { |_, h| h.default_proc = nil }
59
+ @cached_type_result.default_proc = nil
57
60
  super
58
61
  end
59
62
 
@@ -122,6 +125,14 @@ module GraphQL
122
125
  end.compare_by_identity
123
126
 
124
127
  @loadable_possible_types = Hash.new { |h, union_type| h[union_type] = union_type.possible_types }.compare_by_identity
128
+
129
+ # Combined cache for field(owner, field_name) — avoids repeated kind check + parent lookup + visibility check
130
+ @cached_field_result = Hash.new { |h, owner|
131
+ h[owner] = Hash.new { |h2, field_name| h2[field_name] = compute_field(owner, field_name) }
132
+ }.compare_by_identity
133
+
134
+ # Cache for type(type_name) — avoids repeated get_type + visibility + referenced? checks
135
+ @cached_type_result = Hash.new { |h, type_name| h[type_name] = compute_type(type_name) }
125
136
  end
126
137
 
127
138
  def field_on_visible_interface?(field, owner)
@@ -149,58 +160,11 @@ module GraphQL
149
160
  end
150
161
 
151
162
  def type(type_name)
152
- t = @visibility.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
153
- if t
154
- if t.is_a?(Array)
155
- vis_t = nil
156
- t.each do |t_defn|
157
- if @cached_visible[t_defn] && referenced?(t_defn)
158
- if vis_t.nil?
159
- vis_t = t_defn
160
- else
161
- raise_duplicate_definition(vis_t, t_defn)
162
- end
163
- end
164
- end
165
- vis_t
166
- else
167
- if t && @cached_visible[t] && referenced?(t)
168
- t
169
- else
170
- nil
171
- end
172
- end
173
- end
163
+ @cached_type_result[type_name]
174
164
  end
175
165
 
176
166
  def field(owner, field_name)
177
- f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name])
178
- field
179
- elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name))
180
- entry_point_field
181
- elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name))
182
- dynamic_field
183
- else
184
- nil
185
- end
186
- if f.is_a?(Array)
187
- visible_f = nil
188
- f.each do |f_defn|
189
- if @cached_visible_fields[owner][f_defn]
190
-
191
- if visible_f.nil?
192
- visible_f = f_defn
193
- else
194
- raise_duplicate_definition(visible_f, f_defn)
195
- end
196
- end
197
- end
198
- visible_f&.ensure_loaded
199
- elsif f && @cached_visible_fields[owner][f.ensure_loaded]
200
- f
201
- else
202
- nil
203
- end
167
+ @cached_field_result[owner][field_name]
204
168
  end
205
169
 
206
170
  def fields(owner)
@@ -306,6 +270,7 @@ module GraphQL
306
270
  def preload
307
271
  load_all_types
308
272
  @all_types.each do |type_name, type_defn|
273
+ type(type_name)
309
274
  if type_defn.kind.fields?
310
275
  fields(type_defn).each do |f|
311
276
  field(type_defn, f.graphql_name)
@@ -341,6 +306,60 @@ module GraphQL
341
306
 
342
307
  private
343
308
 
309
+ def compute_type(type_name)
310
+ t = @visibility.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
311
+ if t
312
+ if t.is_a?(Array)
313
+ vis_t = nil
314
+ t.each do |t_defn|
315
+ if @cached_visible[t_defn] && referenced?(t_defn)
316
+ if vis_t.nil?
317
+ vis_t = t_defn
318
+ else
319
+ raise_duplicate_definition(vis_t, t_defn)
320
+ end
321
+ end
322
+ end
323
+ vis_t
324
+ else
325
+ if t && @cached_visible[t] && referenced?(t)
326
+ t
327
+ else
328
+ nil
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ def compute_field(owner, field_name)
335
+ f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name])
336
+ field
337
+ elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name))
338
+ entry_point_field
339
+ elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name))
340
+ dynamic_field
341
+ else
342
+ nil
343
+ end
344
+ if f.is_a?(Array)
345
+ visible_f = nil
346
+ f.each do |f_defn|
347
+ if @cached_visible_fields[owner][f_defn]
348
+ if visible_f.nil?
349
+ visible_f = f_defn
350
+ else
351
+ raise_duplicate_definition(visible_f, f_defn)
352
+ end
353
+ end
354
+ end
355
+ visible_f&.ensure_loaded
356
+ elsif f && @cached_visible_fields[owner][f.ensure_loaded]
357
+ f
358
+ else
359
+ nil
360
+ end
361
+ end
362
+
344
363
  def non_duplicate_items(definitions, visibility_cache)
345
364
  non_dups = []
346
365
  names = Set.new
@@ -10,9 +10,9 @@ module GraphQL
10
10
  class Visibility
11
11
  # @param schema [Class<GraphQL::Schema>]
12
12
  # @param profiles [Hash<Symbol => Hash>] A hash of `name => context` pairs for preloading visibility profiles
13
- # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?`)
13
+ # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?` and `Rails.env.staging?`)
14
14
  # @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
15
- def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? Rails.env.production? : nil), migration_errors: false)
15
+ def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? (Rails.env.production? || Rails.env.staging?) : nil), migration_errors: false)
16
16
  profiles&.each { |name, ctx|
17
17
  ctx[:visibility_profile] = name
18
18
  ctx.freeze
@@ -191,7 +191,7 @@ module GraphQL
191
191
  if refresh
192
192
  @top_level_profile = nil
193
193
  end
194
- @top_level_profile ||= @schema.visibility_profile_class.new(context: Query::NullContext.instance, schema: @schema, visibility: self)
194
+ @top_level_profile ||= @schema.visibility_profile_class.new(context: @schema.null_context, schema: @schema, visibility: self)
195
195
  end
196
196
 
197
197
  private
@@ -13,7 +13,13 @@ module GraphQL
13
13
  end
14
14
 
15
15
  def unwrap
16
- @of_type.unwrap
16
+ @unwrapped ||= @of_type.unwrap
17
+ end
18
+
19
+ def freeze
20
+ unwrap
21
+ to_type_signature
22
+ super
17
23
  end
18
24
 
19
25
  def ==(other)