graphql 2.3.7 → 2.4.8

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +46 -0
  3. data/lib/generators/graphql/orm_mutations_base.rb +1 -1
  4. data/lib/generators/graphql/templates/base_resolver.erb +2 -0
  5. data/lib/generators/graphql/type_generator.rb +1 -1
  6. data/lib/graphql/analysis/field_usage.rb +1 -1
  7. data/lib/graphql/analysis/query_complexity.rb +3 -3
  8. data/lib/graphql/analysis/visitor.rb +8 -7
  9. data/lib/graphql/analysis.rb +4 -4
  10. data/lib/graphql/autoload.rb +38 -0
  11. data/lib/graphql/current.rb +52 -0
  12. data/lib/graphql/dataloader/async_dataloader.rb +7 -6
  13. data/lib/graphql/dataloader/source.rb +7 -4
  14. data/lib/graphql/dataloader.rb +40 -19
  15. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  16. data/lib/graphql/execution/interpreter/resolve.rb +13 -9
  17. data/lib/graphql/execution/interpreter/runtime.rb +35 -31
  18. data/lib/graphql/execution/interpreter.rb +9 -5
  19. data/lib/graphql/execution/lookahead.rb +18 -11
  20. data/lib/graphql/introspection/directive_type.rb +1 -1
  21. data/lib/graphql/introspection/entry_points.rb +2 -2
  22. data/lib/graphql/introspection/field_type.rb +1 -1
  23. data/lib/graphql/introspection/schema_type.rb +6 -11
  24. data/lib/graphql/introspection/type_type.rb +5 -5
  25. data/lib/graphql/invalid_null_error.rb +1 -1
  26. data/lib/graphql/language/cache.rb +13 -0
  27. data/lib/graphql/language/comment.rb +18 -0
  28. data/lib/graphql/language/document_from_schema_definition.rb +62 -34
  29. data/lib/graphql/language/lexer.rb +18 -15
  30. data/lib/graphql/language/nodes.rb +24 -16
  31. data/lib/graphql/language/parser.rb +14 -1
  32. data/lib/graphql/language/printer.rb +31 -15
  33. data/lib/graphql/language/sanitized_printer.rb +1 -1
  34. data/lib/graphql/language.rb +6 -6
  35. data/lib/graphql/pagination/connection.rb +1 -1
  36. data/lib/graphql/query/context/scoped_context.rb +1 -1
  37. data/lib/graphql/query/context.rb +13 -6
  38. data/lib/graphql/query/null_context.rb +3 -5
  39. data/lib/graphql/query/variable_validation_error.rb +1 -1
  40. data/lib/graphql/query.rb +72 -18
  41. data/lib/graphql/railtie.rb +7 -0
  42. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
  43. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  44. data/lib/graphql/rubocop.rb +2 -0
  45. data/lib/graphql/schema/addition.rb +2 -1
  46. data/lib/graphql/schema/always_visible.rb +6 -2
  47. data/lib/graphql/schema/argument.rb +14 -1
  48. data/lib/graphql/schema/build_from_definition.rb +9 -1
  49. data/lib/graphql/schema/directive/flagged.rb +2 -2
  50. data/lib/graphql/schema/directive.rb +1 -1
  51. data/lib/graphql/schema/enum.rb +71 -23
  52. data/lib/graphql/schema/enum_value.rb +10 -2
  53. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  54. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  55. data/lib/graphql/schema/field.rb +102 -47
  56. data/lib/graphql/schema/field_extension.rb +1 -1
  57. data/lib/graphql/schema/has_single_input_argument.rb +5 -2
  58. data/lib/graphql/schema/input_object.rb +90 -39
  59. data/lib/graphql/schema/interface.rb +22 -5
  60. data/lib/graphql/schema/introspection_system.rb +5 -16
  61. data/lib/graphql/schema/loader.rb +1 -1
  62. data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
  63. data/lib/graphql/schema/member/has_arguments.rb +36 -23
  64. data/lib/graphql/schema/member/has_directives.rb +3 -3
  65. data/lib/graphql/schema/member/has_fields.rb +26 -6
  66. data/lib/graphql/schema/member/has_interfaces.rb +4 -4
  67. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  68. data/lib/graphql/schema/member/has_validators.rb +1 -1
  69. data/lib/graphql/schema/object.rb +8 -0
  70. data/lib/graphql/schema/printer.rb +1 -0
  71. data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
  72. data/lib/graphql/schema/resolver.rb +12 -14
  73. data/lib/graphql/schema/subscription.rb +52 -6
  74. data/lib/graphql/schema/type_expression.rb +2 -2
  75. data/lib/graphql/schema/union.rb +1 -1
  76. data/lib/graphql/schema/validator/all_validator.rb +62 -0
  77. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  78. data/lib/graphql/schema/validator.rb +3 -1
  79. data/lib/graphql/schema/visibility/migration.rb +188 -0
  80. data/lib/graphql/schema/visibility/profile.rb +359 -0
  81. data/lib/graphql/schema/visibility/visit.rb +190 -0
  82. data/lib/graphql/schema/visibility.rb +294 -0
  83. data/lib/graphql/schema/warden.rb +179 -16
  84. data/lib/graphql/schema.rb +348 -94
  85. data/lib/graphql/static_validation/base_visitor.rb +6 -5
  86. data/lib/graphql/static_validation/literal_validator.rb +4 -4
  87. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  88. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
  89. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +3 -2
  90. data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
  91. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  92. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +12 -2
  93. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +2 -2
  94. data/lib/graphql/static_validation/rules/fields_will_merge.rb +8 -7
  95. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  96. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +12 -2
  97. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  98. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
  99. data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
  100. data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
  101. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +4 -4
  102. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
  103. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +1 -1
  104. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +1 -1
  105. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  106. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
  107. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
  108. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +11 -2
  109. data/lib/graphql/static_validation/validation_context.rb +18 -2
  110. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +3 -2
  111. data/lib/graphql/subscriptions/broadcast_analyzer.rb +10 -4
  112. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
  113. data/lib/graphql/subscriptions/event.rb +13 -2
  114. data/lib/graphql/subscriptions/serialize.rb +2 -0
  115. data/lib/graphql/subscriptions.rb +6 -4
  116. data/lib/graphql/testing/helpers.rb +10 -6
  117. data/lib/graphql/tracing/active_support_notifications_trace.rb +1 -1
  118. data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
  119. data/lib/graphql/tracing/appoptics_trace.rb +2 -0
  120. data/lib/graphql/tracing/appoptics_tracing.rb +2 -0
  121. data/lib/graphql/tracing/appsignal_trace.rb +2 -0
  122. data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
  123. data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
  124. data/lib/graphql/tracing/data_dog_trace.rb +2 -0
  125. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  126. data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
  127. data/lib/graphql/tracing/legacy_trace.rb +4 -61
  128. data/lib/graphql/tracing/new_relic_trace.rb +2 -0
  129. data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
  130. data/lib/graphql/tracing/notifications_trace.rb +2 -2
  131. data/lib/graphql/tracing/notifications_tracing.rb +2 -0
  132. data/lib/graphql/tracing/null_trace.rb +9 -0
  133. data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
  134. data/lib/graphql/tracing/prometheus_trace.rb +5 -0
  135. data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
  136. data/lib/graphql/tracing/scout_trace.rb +2 -0
  137. data/lib/graphql/tracing/scout_tracing.rb +2 -0
  138. data/lib/graphql/tracing/sentry_trace.rb +2 -0
  139. data/lib/graphql/tracing/statsd_trace.rb +2 -0
  140. data/lib/graphql/tracing/statsd_tracing.rb +2 -0
  141. data/lib/graphql/tracing/trace.rb +3 -0
  142. data/lib/graphql/tracing.rb +28 -30
  143. data/lib/graphql/types/relay/connection_behaviors.rb +12 -2
  144. data/lib/graphql/types/relay/edge_behaviors.rb +11 -1
  145. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
  146. data/lib/graphql/types.rb +18 -11
  147. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  148. data/lib/graphql/version.rb +1 -1
  149. data/lib/graphql.rb +53 -45
  150. metadata +33 -8
  151. data/lib/graphql/language/token.rb +0 -34
  152. data/lib/graphql/schema/invalid_type_error.rb +0 -7
@@ -8,6 +8,7 @@ module GraphQL
8
8
  # - Arguments, via `.argument(...)` helper, which will be applied to the field.
9
9
  # - Return type, via `.type(..., null: ...)`, which will be applied to the field.
10
10
  # - Description, via `.description(...)`, which will be applied to the field
11
+ # - Comment, via `.comment(...)`, which will be applied to the field
11
12
  # - Resolution, via `#resolve(**args)` method, which will be called to resolve the field.
12
13
  # - `#object` and `#context` accessors for use during `#resolve`.
13
14
  #
@@ -19,7 +20,7 @@ module GraphQL
19
20
  # @see {GraphQL::Function} `Resolver` is a replacement for `GraphQL::Function`
20
21
  class Resolver
21
22
  include Schema::Member::GraphQLTypeNames
22
- # Really we only need description from here, but:
23
+ # Really we only need description & comment from here, but:
23
24
  extend Schema::Member::BaseDSLMethods
24
25
  extend GraphQL::Schema::Member::HasArguments
25
26
  extend GraphQL::Schema::Member::HasValidators
@@ -36,7 +37,7 @@ module GraphQL
36
37
  @field = field
37
38
  # Since this hash is constantly rebuilt, cache it for this call
38
39
  @arguments_by_keyword = {}
39
- self.class.arguments(context).each do |name, arg|
40
+ context.types.arguments(self.class).each do |arg|
40
41
  @arguments_by_keyword[arg.keyword] = arg
41
42
  end
42
43
  @prepared_arguments = nil
@@ -66,7 +67,7 @@ module GraphQL
66
67
  # @api private
67
68
  def resolve_with_support(**args)
68
69
  # First call the ready? hook which may raise
69
- raw_ready_val = if args.any?
70
+ raw_ready_val = if !args.empty?
70
71
  ready?(**args)
71
72
  else
72
73
  ready?
@@ -87,7 +88,7 @@ module GraphQL
87
88
  @prepared_arguments = loaded_args
88
89
  Schema::Validator.validate!(self.class.validators, object, context, loaded_args, as: @field)
89
90
  # Then call `authorized?`, which may raise or may return a lazy object
90
- raw_authorized_val = if loaded_args.any?
91
+ raw_authorized_val = if !loaded_args.empty?
91
92
  authorized?(**loaded_args)
92
93
  else
93
94
  authorized?
@@ -116,7 +117,7 @@ module GraphQL
116
117
 
117
118
  # @api private {GraphQL::Schema::Mutation} uses this to clear the dataloader cache
118
119
  def call_resolve(args_hash)
119
- if args_hash.any?
120
+ if !args_hash.empty?
120
121
  public_send(self.class.resolve_method, **args_hash)
121
122
  else
122
123
  public_send(self.class.resolve_method)
@@ -152,7 +153,7 @@ module GraphQL
152
153
  # @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.)
153
154
  def authorized?(**inputs)
154
155
  arg_owner = @field # || self.class
155
- args = arg_owner.arguments(context)
156
+ args = context.types.arguments(arg_owner)
156
157
  authorize_arguments(args, inputs)
157
158
  end
158
159
 
@@ -169,7 +170,7 @@ module GraphQL
169
170
  private
170
171
 
171
172
  def authorize_arguments(args, inputs)
172
- args.each_value do |argument|
173
+ args.each do |argument|
173
174
  arg_keyword = argument.keyword
174
175
  if inputs.key?(arg_keyword) && !(arg_value = inputs[arg_keyword]).nil? && (arg_value != argument.default_value)
175
176
  auth_result = argument.authorized?(self, arg_value, context)
@@ -182,10 +183,9 @@ module GraphQL
182
183
  elsif auth_result == false
183
184
  return auth_result
184
185
  end
185
- else
186
- true
187
186
  end
188
187
  end
188
+ true
189
189
  end
190
190
 
191
191
  def load_arguments(args)
@@ -208,7 +208,7 @@ module GraphQL
208
208
  end
209
209
 
210
210
  # Avoid returning a lazy if none are needed
211
- if prepare_lazies.any?
211
+ if !prepare_lazies.empty?
212
212
  GraphQL::Execution::Lazy.all(prepare_lazies).then { prepared_args }
213
213
  else
214
214
  prepared_args
@@ -394,7 +394,7 @@ module GraphQL
394
394
  if superclass.respond_to?(:extensions)
395
395
  s_exts = superclass.extensions
396
396
  if own_exts
397
- if s_exts.any?
397
+ if !s_exts.empty?
398
398
  own_exts + s_exts
399
399
  else
400
400
  own_exts
@@ -409,9 +409,7 @@ module GraphQL
409
409
 
410
410
  private
411
411
 
412
- def own_extensions
413
- @own_extensions
414
- end
412
+ attr_reader :own_extensions
415
413
  end
416
414
  end
417
415
  end
@@ -19,13 +19,22 @@ module GraphQL
19
19
  # propagate null.
20
20
  null false
21
21
 
22
+ # @api private
22
23
  def initialize(object:, context:, field:)
23
24
  super
24
25
  # Figure out whether this is an update or an initial subscription
25
26
  @mode = context.query.subscription_update? ? :update : :subscribe
27
+ @subscription_written = false
28
+ @original_arguments = nil
29
+ if (subs_ns = context.namespace(:subscriptions)) &&
30
+ (sub_insts = subs_ns[:subscriptions])
31
+ sub_insts[context.current_path] = self
32
+ end
26
33
  end
27
34
 
35
+ # @api private
28
36
  def resolve_with_support(**args)
37
+ @original_arguments = args # before `loads:` have been run
29
38
  result = nil
30
39
  unsubscribed = true
31
40
  unsubscribed_result = catch :graphql_subscription_unsubscribed do
@@ -46,7 +55,9 @@ module GraphQL
46
55
  end
47
56
  end
48
57
 
49
- # Implement the {Resolve} API
58
+ # Implement the {Resolve} API.
59
+ # You can implement this if you want code to run for _both_ the initial subscription
60
+ # and for later updates. Or, implement {#subscribe} and {#update}
50
61
  def resolve(**args)
51
62
  # Dispatch based on `@mode`, which will raise a `NoMethodError` if we ever
52
63
  # have an unexpected `@mode`
@@ -54,8 +65,9 @@ module GraphQL
54
65
  end
55
66
 
56
67
  # Wrap the user-defined `#subscribe` hook
68
+ # @api private
57
69
  def resolve_subscribe(**args)
58
- ret_val = args.any? ? subscribe(**args) : subscribe
70
+ ret_val = !args.empty? ? subscribe(**args) : subscribe
59
71
  if ret_val == :no_response
60
72
  context.skip
61
73
  else
@@ -71,8 +83,9 @@ module GraphQL
71
83
  end
72
84
 
73
85
  # Wrap the user-provided `#update` hook
86
+ # @api private
74
87
  def resolve_update(**args)
75
- ret_val = args.any? ? update(**args) : update
88
+ ret_val = !args.empty? ? update(**args) : update
76
89
  if ret_val == NO_UPDATE
77
90
  context.namespace(:subscriptions)[:no_update] = true
78
91
  context.skip
@@ -106,14 +119,13 @@ module GraphQL
106
119
  throw :graphql_subscription_unsubscribed, update_value
107
120
  end
108
121
 
109
- READING_SCOPE = ::Object.new
110
122
  # Call this method to provide a new subscription_scope; OR
111
123
  # call it without an argument to get the subscription_scope
112
124
  # @param new_scope [Symbol]
113
125
  # @param optional [Boolean] If true, then don't require `scope:` to be provided to updates to this subscription.
114
126
  # @return [Symbol]
115
- def self.subscription_scope(new_scope = READING_SCOPE, optional: false)
116
- if new_scope != READING_SCOPE
127
+ def self.subscription_scope(new_scope = NOT_CONFIGURED, optional: false)
128
+ if new_scope != NOT_CONFIGURED
117
129
  @subscription_scope = new_scope
118
130
  @subscription_scope_optional = optional
119
131
  elsif defined?(@subscription_scope)
@@ -150,6 +162,40 @@ module GraphQL
150
162
  def self.topic_for(arguments:, field:, scope:)
151
163
  Subscriptions::Serialize.dump_recursive([scope, field.graphql_name, arguments])
152
164
  end
165
+
166
+ # Calls through to `schema.subscriptions` to register this subscription with the backend.
167
+ # This is automatically called by GraphQL-Ruby after a query finishes successfully,
168
+ # but if you need to commit the subscription during `#subscribe`, you can call it there.
169
+ # (This method also sets a flag showing that this subscription was already written.)
170
+ #
171
+ # If you call this method yourself, you may also need to {#unsubscribe}
172
+ # or call `subscriptions.delete_subscription` to clean up the database if the query crashes with an error
173
+ # later in execution.
174
+ # @return [void]
175
+ def write_subscription
176
+ if subscription_written?
177
+ raise GraphQL::Error, "`write_subscription` was called but `#{self.class}#subscription_written?` is already true. Remove a call to `write subscription`."
178
+ else
179
+ @subscription_written = true
180
+ context.schema.subscriptions.write_subscription(context.query, [event])
181
+ end
182
+ nil
183
+ end
184
+
185
+ # @return [Boolean] `true` if {#write_subscription} was called already
186
+ def subscription_written?
187
+ @subscription_written
188
+ end
189
+
190
+ # @return [Subscriptions::Event] This object is used as a representation of this subscription for the backend
191
+ def event
192
+ @event ||= Subscriptions::Event.new(
193
+ name: field.name,
194
+ arguments: @original_arguments,
195
+ context: context,
196
+ field: field,
197
+ )
198
+ end
153
199
  end
154
200
  end
155
201
  end
@@ -5,13 +5,13 @@ module GraphQL
5
5
  module TypeExpression
6
6
  # Fetch a type from a type map by its AST specification.
7
7
  # Return `nil` if not found.
8
- # @param type_owner [#get_type] A thing for looking up types by name
8
+ # @param type_owner [#type] A thing for looking up types by name
9
9
  # @param ast_node [GraphQL::Language::Nodes::AbstractNode]
10
10
  # @return [Class, GraphQL::Schema::NonNull, GraphQL::Schema:List]
11
11
  def self.build_type(type_owner, ast_node)
12
12
  case ast_node
13
13
  when GraphQL::Language::Nodes::TypeName
14
- type_owner.get_type(ast_node.name) # rubocop:disable Development/ContextIsPassedCop -- this is a `context` or `warden`, it's already query-aware
14
+ type_owner.type(ast_node.name) # rubocop:disable Development/ContextIsPassedCop -- this is a `context` or `warden`, it's already query-aware
15
15
  when GraphQL::Language::Nodes::NonNullType
16
16
  ast_inner_type = ast_node.of_type
17
17
  inner_type = build_type(type_owner, ast_inner_type)
@@ -11,7 +11,7 @@ module GraphQL
11
11
  end
12
12
 
13
13
  def possible_types(*types, context: GraphQL::Query::NullContext.instance, **options)
14
- if types.any?
14
+ if !types.empty?
15
15
  types.each do |t|
16
16
  assert_valid_union_member(t)
17
17
  type_memberships << type_membership_class.new(self, t, **options)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ class Validator
6
+ # Use this to validate each member of an array value.
7
+ #
8
+ # @example validate format of all strings in an array
9
+ #
10
+ # argument :handles, [String],
11
+ # validates: { all: { format: { with: /\A[a-z0-9_]+\Z/ } } }
12
+ #
13
+ # @example multiple validators can be combined
14
+ #
15
+ # argument :handles, [String],
16
+ # validates: { all: { format: { with: /\A[a-z0-9_]+\Z/ }, length: { maximum: 32 } } }
17
+ #
18
+ # @example any type can be used
19
+ #
20
+ # argument :choices, [Integer],
21
+ # validates: { all: { inclusion: { in: 1..12 } } }
22
+ #
23
+ class AllValidator < Validator
24
+ def initialize(validated:, allow_blank: false, allow_null: false, **validators)
25
+ super(validated: validated, allow_blank: allow_blank, allow_null: allow_null)
26
+
27
+ @validators = Validator.from_config(validated, validators)
28
+ end
29
+
30
+ def validate(object, context, value)
31
+ return EMPTY_ARRAY if permitted_empty_value?(value)
32
+
33
+ all_errors = EMPTY_ARRAY
34
+
35
+ value.each do |subvalue|
36
+ @validators.each do |validator|
37
+ errors = validator.validate(object, context, subvalue)
38
+ if errors &&
39
+ (errors.is_a?(Array) && errors != EMPTY_ARRAY) ||
40
+ (errors.is_a?(String))
41
+ if all_errors.frozen? # It's empty
42
+ all_errors = []
43
+ end
44
+ if errors.is_a?(String)
45
+ all_errors << errors
46
+ else
47
+ all_errors.concat(errors)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ unless all_errors.frozen?
54
+ all_errors.uniq!
55
+ end
56
+
57
+ all_errors
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -35,9 +35,10 @@ module GraphQL
35
35
  # end
36
36
  #
37
37
  class RequiredValidator < Validator
38
- # @param one_of [Symbol, Array<Symbol>] An argument, or a list of arguments, that represents a valid set of inputs for this field
38
+ # @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
39
+ # @param argument [Symbol] An argument that is required for this field
39
40
  # @param message [String]
40
- def initialize(one_of: nil, argument: nil, message: "%{validated} has the wrong arguments", **default_options)
41
+ def initialize(one_of: nil, argument: nil, message: nil, **default_options)
41
42
  @one_of = if one_of
42
43
  one_of
43
44
  elsif argument
@@ -49,7 +50,7 @@ module GraphQL
49
50
  super(**default_options)
50
51
  end
51
52
 
52
- def validate(_object, _context, value)
53
+ def validate(_object, context, value)
53
54
  matched_conditions = 0
54
55
 
55
56
  if !value.nil?
@@ -73,9 +74,32 @@ module GraphQL
73
74
  if matched_conditions == 1
74
75
  nil # OK
75
76
  else
76
- @message
77
+ @message || build_message(context)
77
78
  end
78
79
  end
80
+
81
+ def build_message(context)
82
+ argument_definitions = @validated.arguments(context).values
83
+ required_names = @one_of.map do |arg_keyword|
84
+ if arg_keyword.is_a?(Array)
85
+ names = arg_keyword.map { |arg| arg_keyword_to_grapqhl_name(argument_definitions, arg) }
86
+ "(" + names.join(" and ") + ")"
87
+ else
88
+ arg_keyword_to_grapqhl_name(argument_definitions, arg_keyword)
89
+ end
90
+ end
91
+
92
+ if required_names.size == 1
93
+ "%{validated} must include the following argument: #{required_names.first}."
94
+ else
95
+ "%{validated} must include exactly one of the following arguments: #{required_names.join(", ")}."
96
+ end
97
+ end
98
+
99
+ def arg_keyword_to_grapqhl_name(argument_definitions, arg_keyword)
100
+ argument_definition = argument_definitions.find { |defn| defn.keyword == arg_keyword }
101
+ argument_definition.graphql_name
102
+ end
79
103
  end
80
104
  end
81
105
  end
@@ -143,7 +143,7 @@ module GraphQL
143
143
  end
144
144
  end
145
145
 
146
- if all_errors.any?
146
+ if !all_errors.empty?
147
147
  raise ValidationFailedError.new(errors: all_errors)
148
148
  end
149
149
  nil
@@ -169,3 +169,5 @@ require "graphql/schema/validator/allow_null_validator"
169
169
  GraphQL::Schema::Validator.install(:allow_null, GraphQL::Schema::Validator::AllowNullValidator)
170
170
  require "graphql/schema/validator/allow_blank_validator"
171
171
  GraphQL::Schema::Validator.install(:allow_blank, GraphQL::Schema::Validator::AllowBlankValidator)
172
+ require "graphql/schema/validator/all_validator"
173
+ GraphQL::Schema::Validator.install(:all, GraphQL::Schema::Validator::AllValidator)
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Schema
4
+ class Visibility
5
+ # You can use this to see how {GraphQL::Schema::Warden} and {GraphQL::Schema::Visibility::Profile}
6
+ # handle `.visible?` differently in your schema.
7
+ #
8
+ # It runs the same method on both implementations and raises an error when the results diverge.
9
+ #
10
+ # To fix the error, modify your schema so that both implementations return the same thing.
11
+ # Or, open an issue on GitHub to discuss the difference.
12
+ #
13
+ # This plugin adds overhead to runtime and may cause unexpected crashes -- **don't** use it in production!
14
+ #
15
+ # This plugin adds two keys to `context` when running:
16
+ #
17
+ # - `visibility_migration_running: true`
18
+ # - For the {Schema::Warden} which it instantiates, it adds `visibility_migration_warden_running: true`.
19
+ #
20
+ # Use those keys to modify your `visible?` behavior as needed.
21
+ #
22
+ # Also, in a pinch, you can set `skip_visibility_migration_error: true` in context to turn off this behavior per-query.
23
+ # (In that case, it uses {Profile} directly.)
24
+ #
25
+ # @example Adding this plugin
26
+ #
27
+ # use GraphQL::Schema::Visibility, migration_errors: true
28
+ #
29
+ class Migration < GraphQL::Schema::Visibility::Profile
30
+ class RuntimeTypesMismatchError < GraphQL::Error
31
+ def initialize(method_called, warden_result, profile_result, method_args)
32
+ super(<<~ERR)
33
+ Mismatch in types for `##{method_called}(#{method_args.map(&:inspect).join(", ")})`:
34
+
35
+ #{compare_results(warden_result, profile_result)}
36
+
37
+ Update your `.visible?` implementation to make these implementations return the same value.
38
+
39
+ See: https://graphql-ruby.org/authorization/visibility_migration.html
40
+ ERR
41
+ end
42
+
43
+ private
44
+ def compare_results(warden_result, profile_result)
45
+ if warden_result.is_a?(Array) && profile_result.is_a?(Array)
46
+ all_results = warden_result | profile_result
47
+ all_results.sort_by!(&:graphql_name)
48
+
49
+ entries_text = all_results.map { |entry| "#{entry.graphql_name} (#{entry})"}
50
+ width = entries_text.map(&:size).max
51
+ yes = " ✔ "
52
+ no = " "
53
+ res = "".dup
54
+ res << "#{"Result".center(width)} Warden Profile \n"
55
+ all_results.each_with_index do |entry, idx|
56
+ res << "#{entries_text[idx].ljust(width)}#{warden_result.include?(entry) ? yes : no}#{profile_result.include?(entry) ? yes : no}\n"
57
+ end
58
+ res << "\n"
59
+ else
60
+ "- Warden returned: #{humanize(warden_result)}\n\n- Visibility::Profile returned: #{humanize(profile_result)}"
61
+ end
62
+ end
63
+ def humanize(val)
64
+ case val
65
+ when Array
66
+ "#{val.size}: #{val.map { |v| humanize(v) }.sort.inspect}"
67
+ when Module
68
+ if val.respond_to?(:graphql_name)
69
+ "#{val.graphql_name} (#{val.inspect})"
70
+ else
71
+ val.inspect
72
+ end
73
+ else
74
+ val.inspect
75
+ end
76
+ end
77
+ end
78
+
79
+ def initialize(context:, schema:, name: nil)
80
+ @name = name
81
+ @skip_error = context[:skip_visibility_migration_error] || context.is_a?(Query::NullContext) || context.is_a?(Hash)
82
+ @profile_types = GraphQL::Schema::Visibility::Profile.new(context: context, schema: schema)
83
+ if !@skip_error
84
+ context[:visibility_migration_running] = true
85
+ warden_ctx_vals = context.to_h.dup
86
+ warden_ctx_vals[:visibility_migration_warden_running] = true
87
+ if schema.const_defined?(:WardenCompatSchema, false) # don't use a defn from a superclass
88
+ warden_schema = schema.const_get(:WardenCompatSchema, false)
89
+ else
90
+ warden_schema = Class.new(schema)
91
+ warden_schema.use_visibility_profile = false
92
+ # TODO public API
93
+ warden_schema.send(:add_type_and_traverse, [warden_schema.query, warden_schema.mutation, warden_schema.subscription].compact, root: true)
94
+ warden_schema.send(:add_type_and_traverse, warden_schema.directives.values + warden_schema.orphan_types, root: false)
95
+ schema.const_set(:WardenCompatSchema, warden_schema)
96
+ end
97
+ warden_ctx = GraphQL::Query::Context.new(query: context.query, values: warden_ctx_vals)
98
+ warden_ctx.warden = GraphQL::Schema::Warden.new(schema: warden_schema, context: warden_ctx)
99
+ warden_ctx.warden.skip_warning = true
100
+ warden_ctx.types = @warden_types = warden_ctx.warden.visibility_profile
101
+ end
102
+ end
103
+
104
+ def loaded_types
105
+ @profile_types.loaded_types
106
+ end
107
+
108
+ PUBLIC_PROFILE_METHODS = [
109
+ :enum_values,
110
+ :interfaces,
111
+ :all_types,
112
+ :all_types_h,
113
+ :fields,
114
+ :loadable?,
115
+ :loadable_possible_types,
116
+ :type,
117
+ :arguments,
118
+ :argument,
119
+ :directive_exists?,
120
+ :directives,
121
+ :field,
122
+ :query_root,
123
+ :mutation_root,
124
+ :possible_types,
125
+ :subscription_root,
126
+ :reachable_type?,
127
+ :visible_enum_value?,
128
+ ]
129
+
130
+ PUBLIC_PROFILE_METHODS.each do |profile_method|
131
+ define_method(profile_method) do |*args|
132
+ call_method_and_compare(profile_method, args)
133
+ end
134
+ end
135
+
136
+ def call_method_and_compare(method, args)
137
+ res_1 = @profile_types.public_send(method, *args)
138
+ if @skip_error
139
+ return res_1
140
+ end
141
+
142
+ res_2 = @warden_types.public_send(method, *args)
143
+ normalized_res_1 = res_1.is_a?(Array) ? Set.new(res_1) : res_1
144
+ normalized_res_2 = res_2.is_a?(Array) ? Set.new(res_2) : res_2
145
+ if !equivalent_schema_members?(normalized_res_1, normalized_res_2)
146
+ # Raise the errors with the orignally returned values:
147
+ err = RuntimeTypesMismatchError.new(method, res_2, res_1, args)
148
+ raise err
149
+ else
150
+ res_1
151
+ end
152
+ end
153
+
154
+ def equivalent_schema_members?(member1, member2)
155
+ if member1.class != member2.class
156
+ return false
157
+ end
158
+
159
+ case member1
160
+ when Set
161
+ member1_array = member1.to_a.sort_by(&:graphql_name)
162
+ member2_array = member2.to_a.sort_by(&:graphql_name)
163
+ member1_array.each_with_index do |inner_member1, idx|
164
+ inner_member2 = member2_array[idx]
165
+ equivalent_schema_members?(inner_member1, inner_member2)
166
+ end
167
+ when GraphQL::Schema::Field
168
+ member1.ensure_loaded
169
+ member2.ensure_loaded
170
+ if member1.introspection? && member2.introspection?
171
+ member1.inspect == member2.inspect
172
+ else
173
+ member1 == member2
174
+ end
175
+ when Module
176
+ if member1.introspection? && member2.introspection?
177
+ member1.graphql_name == member2.graphql_name
178
+ else
179
+ member1 == member2
180
+ end
181
+ else
182
+ member1 == member2
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end