graphql 2.3.7 → 2.4.0

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 (101) 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 +7 -6
  9. data/lib/graphql/analysis.rb +1 -1
  10. data/lib/graphql/current.rb +52 -0
  11. data/lib/graphql/dataloader/async_dataloader.rb +3 -2
  12. data/lib/graphql/dataloader/source.rb +6 -3
  13. data/lib/graphql/dataloader.rb +35 -11
  14. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  15. data/lib/graphql/execution/interpreter/resolve.rb +10 -6
  16. data/lib/graphql/execution/interpreter/runtime.rb +29 -25
  17. data/lib/graphql/execution/interpreter.rb +2 -0
  18. data/lib/graphql/execution/lookahead.rb +10 -10
  19. data/lib/graphql/introspection/directive_type.rb +1 -1
  20. data/lib/graphql/introspection/entry_points.rb +2 -2
  21. data/lib/graphql/introspection/field_type.rb +1 -1
  22. data/lib/graphql/introspection/schema_type.rb +6 -11
  23. data/lib/graphql/introspection/type_type.rb +5 -5
  24. data/lib/graphql/invalid_null_error.rb +1 -1
  25. data/lib/graphql/language/comment.rb +18 -0
  26. data/lib/graphql/language/document_from_schema_definition.rb +55 -28
  27. data/lib/graphql/language/lexer.rb +15 -15
  28. data/lib/graphql/language/nodes.rb +24 -16
  29. data/lib/graphql/language/parser.rb +14 -1
  30. data/lib/graphql/language/printer.rb +23 -7
  31. data/lib/graphql/language/sanitized_printer.rb +1 -1
  32. data/lib/graphql/language.rb +6 -6
  33. data/lib/graphql/query/context.rb +7 -1
  34. data/lib/graphql/query/null_context.rb +2 -2
  35. data/lib/graphql/query.rb +60 -8
  36. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
  37. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  38. data/lib/graphql/rubocop.rb +2 -0
  39. data/lib/graphql/schema/addition.rb +1 -0
  40. data/lib/graphql/schema/always_visible.rb +6 -2
  41. data/lib/graphql/schema/argument.rb +14 -1
  42. data/lib/graphql/schema/build_from_definition.rb +9 -1
  43. data/lib/graphql/schema/directive/flagged.rb +1 -1
  44. data/lib/graphql/schema/enum.rb +54 -20
  45. data/lib/graphql/schema/enum_value.rb +9 -1
  46. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  47. data/lib/graphql/schema/field.rb +92 -37
  48. data/lib/graphql/schema/has_single_input_argument.rb +2 -1
  49. data/lib/graphql/schema/input_object.rb +27 -13
  50. data/lib/graphql/schema/interface.rb +21 -4
  51. data/lib/graphql/schema/introspection_system.rb +5 -16
  52. data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
  53. data/lib/graphql/schema/member/has_arguments.rb +14 -9
  54. data/lib/graphql/schema/member/has_fields.rb +8 -6
  55. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  56. data/lib/graphql/schema/printer.rb +1 -0
  57. data/lib/graphql/schema/resolver.rb +7 -9
  58. data/lib/graphql/schema/type_expression.rb +2 -2
  59. data/lib/graphql/schema/validator/all_validator.rb +62 -0
  60. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  61. data/lib/graphql/schema/validator.rb +2 -0
  62. data/lib/graphql/schema/visibility/migration.rb +186 -0
  63. data/lib/graphql/schema/visibility/profile.rb +523 -0
  64. data/lib/graphql/schema/visibility.rb +75 -0
  65. data/lib/graphql/schema/warden.rb +156 -10
  66. data/lib/graphql/schema.rb +283 -85
  67. data/lib/graphql/static_validation/base_visitor.rb +6 -5
  68. data/lib/graphql/static_validation/literal_validator.rb +4 -4
  69. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  70. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +3 -2
  71. data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
  72. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  73. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +3 -2
  74. data/lib/graphql/static_validation/rules/fields_will_merge.rb +8 -7
  75. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  76. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +12 -2
  77. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  78. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
  79. data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
  80. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +3 -3
  81. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
  82. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +1 -1
  83. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  84. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
  85. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +11 -2
  86. data/lib/graphql/static_validation/validation_context.rb +17 -2
  87. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +2 -1
  88. data/lib/graphql/subscriptions/broadcast_analyzer.rb +10 -4
  89. data/lib/graphql/subscriptions/event.rb +1 -1
  90. data/lib/graphql/subscriptions.rb +5 -3
  91. data/lib/graphql/testing/helpers.rb +9 -5
  92. data/lib/graphql/tracing/notifications_trace.rb +2 -2
  93. data/lib/graphql/types/relay/connection_behaviors.rb +10 -0
  94. data/lib/graphql/types/relay/edge_behaviors.rb +10 -0
  95. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
  96. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  97. data/lib/graphql/version.rb +1 -1
  98. data/lib/graphql.rb +3 -0
  99. metadata +29 -8
  100. data/lib/graphql/language/token.rb +0 -34
  101. data/lib/graphql/schema/invalid_type_error.rb +0 -7
@@ -135,10 +135,11 @@ module GraphQL
135
135
 
136
136
  def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
137
137
  warden = Warden.from_context(context)
138
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
138
139
  for ancestor in ancestors
139
140
  if ancestor.respond_to?(:own_arguments) &&
140
141
  (a = ancestor.own_arguments[argument_name]) &&
141
- (a = Warden.visible_entry?(:visible_argument?, a, context, warden))
142
+ (skip_visible || (a = Warden.visible_entry?(:visible_argument?, a, context, warden)))
142
143
  return a
143
144
  end
144
145
  end
@@ -197,16 +198,20 @@ module GraphQL
197
198
  end
198
199
 
199
200
  def all_argument_definitions
200
- all_defns = own_arguments.values
201
- all_defns.flatten!
202
- all_defns
201
+ if own_arguments.any?
202
+ all_defns = own_arguments.values
203
+ all_defns.flatten!
204
+ all_defns
205
+ else
206
+ EmptyObjects::EMPTY_ARRAY
207
+ end
203
208
  end
204
209
 
205
210
  # @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name.
206
211
  def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
207
212
  warden = Warden.from_context(context)
208
- if (arg_config = own_arguments[argument_name]) && (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden))
209
- visible_arg
213
+ if (arg_config = own_arguments[argument_name]) && ((context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)) || (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden)))
214
+ visible_arg || arg_config
210
215
  elsif defined?(@resolver_class) && @resolver_class
211
216
  @resolver_class.get_field_argument(argument_name, context)
212
217
  else
@@ -230,7 +235,7 @@ module GraphQL
230
235
  # @return [Interpreter::Arguments, Execution::Lazy<Interpreter::Arguments>]
231
236
  def coerce_arguments(parent_object, values, context, &block)
232
237
  # Cache this hash to avoid re-merging it
233
- arg_defns = context.warden.arguments(self)
238
+ arg_defns = context.types.arguments(self)
234
239
  total_args_count = arg_defns.size
235
240
 
236
241
  finished_args = nil
@@ -364,8 +369,8 @@ module GraphQL
364
369
  end
365
370
 
366
371
  if !(
367
- context.warden.possible_types(argument.loads).include?(application_object_type) ||
368
- context.warden.loadable?(argument.loads, context)
372
+ context.types.possible_types(argument.loads).include?(application_object_type) ||
373
+ context.types.loadable?(argument.loads, context)
369
374
  )
370
375
  err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object)
371
376
  application_object = load_application_object_failed(err)
@@ -99,11 +99,12 @@ module GraphQL
99
99
  module InterfaceMethods
100
100
  def get_field(field_name, context = GraphQL::Query::NullContext.instance)
101
101
  warden = Warden.from_context(context)
102
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
102
103
  for ancestor in ancestors
103
104
  if ancestor.respond_to?(:own_fields) &&
104
105
  (f_entry = ancestor.own_fields[field_name]) &&
105
- (f = Warden.visible_entry?(:visible_field?, f_entry, context, warden))
106
- return f
106
+ (skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden)))
107
+ return f_entry
107
108
  end
108
109
  end
109
110
  nil
@@ -120,7 +121,7 @@ module GraphQL
120
121
  # Choose the most local definition that passes `.visible?` --
121
122
  # stop checking for fields by name once one has been found.
122
123
  if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
123
- visible_fields[field_name] = f
124
+ visible_fields[field_name] = f.ensure_loaded
124
125
  end
125
126
  end
126
127
  end
@@ -134,13 +135,14 @@ module GraphQL
134
135
  # Objects need to check that the interface implementation is visible, too
135
136
  warden = Warden.from_context(context)
136
137
  ancs = ancestors
138
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
137
139
  i = 0
138
140
  while (ancestor = ancs[i])
139
141
  if ancestor.respond_to?(:own_fields) &&
140
142
  visible_interface_implementation?(ancestor, context, warden) &&
141
143
  (f_entry = ancestor.own_fields[field_name]) &&
142
- (f = Warden.visible_entry?(:visible_field?, f_entry, context, warden))
143
- return f
144
+ (skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden)))
145
+ return (skip_visible ? f_entry : f_entry.ensure_loaded)
144
146
  end
145
147
  i += 1
146
148
  end
@@ -159,7 +161,7 @@ module GraphQL
159
161
  # Choose the most local definition that passes `.visible?` --
160
162
  # stop checking for fields by name once one has been found.
161
163
  if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
162
- visible_fields[field_name] = f
164
+ visible_fields[field_name] = f.ensure_loaded
163
165
  end
164
166
  end
165
167
  end
@@ -7,7 +7,11 @@ module GraphQL
7
7
  module HasUnresolvedTypeError
8
8
  private
9
9
  def add_unresolved_type_error(child_class)
10
- child_class.const_set(:UnresolvedTypeError, Class.new(GraphQL::UnresolvedTypeError))
10
+ if child_class.name # Don't set this for anonymous classes
11
+ child_class.const_set(:UnresolvedTypeError, Class.new(GraphQL::UnresolvedTypeError))
12
+ else
13
+ child_class.const_set(:UnresolvedTypeError, UnresolvedTypeError)
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -58,6 +58,7 @@ module GraphQL
58
58
  end
59
59
  end
60
60
  schema = Class.new(GraphQL::Schema) {
61
+ use GraphQL::Schema::Visibility
61
62
  query(query_root)
62
63
  def self.visible?(member, _ctx)
63
64
  member.graphql_name != "Root"
@@ -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
@@ -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)
@@ -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
@@ -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)
@@ -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
@@ -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,186 @@
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
+ :type,
116
+ :arguments,
117
+ :argument,
118
+ :directive_exists?,
119
+ :directives,
120
+ :field,
121
+ :query_root,
122
+ :mutation_root,
123
+ :possible_types,
124
+ :subscription_root,
125
+ :reachable_type?
126
+ ]
127
+
128
+ PUBLIC_PROFILE_METHODS.each do |profile_method|
129
+ define_method(profile_method) do |*args|
130
+ call_method_and_compare(profile_method, args)
131
+ end
132
+ end
133
+
134
+ def call_method_and_compare(method, args)
135
+ res_1 = @profile_types.public_send(method, *args)
136
+ if @skip_error
137
+ return res_1
138
+ end
139
+
140
+ res_2 = @warden_types.public_send(method, *args)
141
+ normalized_res_1 = res_1.is_a?(Array) ? Set.new(res_1) : res_1
142
+ normalized_res_2 = res_2.is_a?(Array) ? Set.new(res_2) : res_2
143
+ if !equivalent_schema_members?(normalized_res_1, normalized_res_2)
144
+ # Raise the errors with the orignally returned values:
145
+ err = RuntimeTypesMismatchError.new(method, res_2, res_1, args)
146
+ raise err
147
+ else
148
+ res_1
149
+ end
150
+ end
151
+
152
+ def equivalent_schema_members?(member1, member2)
153
+ if member1.class != member2.class
154
+ return false
155
+ end
156
+
157
+ case member1
158
+ when Set
159
+ member1_array = member1.to_a.sort_by(&:graphql_name)
160
+ member2_array = member2.to_a.sort_by(&:graphql_name)
161
+ member1_array.each_with_index do |inner_member1, idx|
162
+ inner_member2 = member2_array[idx]
163
+ equivalent_schema_members?(inner_member1, inner_member2)
164
+ end
165
+ when GraphQL::Schema::Field
166
+ member1.ensure_loaded
167
+ member2.ensure_loaded
168
+ if member1.introspection? && member2.introspection?
169
+ member1.inspect == member2.inspect
170
+ else
171
+ member1 == member2
172
+ end
173
+ when Module
174
+ if member1.introspection? && member2.introspection?
175
+ member1.graphql_name == member2.graphql_name
176
+ else
177
+ member1 == member2
178
+ end
179
+ else
180
+ member1 == member2
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end