graphql 2.3.8 → 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 (73) 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.rb +1 -1
  7. data/lib/graphql/current.rb +52 -0
  8. data/lib/graphql/dataloader/async_dataloader.rb +3 -2
  9. data/lib/graphql/dataloader/source.rb +6 -3
  10. data/lib/graphql/dataloader.rb +35 -11
  11. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  12. data/lib/graphql/execution/interpreter/resolve.rb +10 -6
  13. data/lib/graphql/execution/interpreter/runtime.rb +23 -19
  14. data/lib/graphql/execution/interpreter.rb +2 -0
  15. data/lib/graphql/introspection/entry_points.rb +2 -2
  16. data/lib/graphql/introspection/schema_type.rb +4 -19
  17. data/lib/graphql/invalid_null_error.rb +1 -1
  18. data/lib/graphql/language/comment.rb +18 -0
  19. data/lib/graphql/language/document_from_schema_definition.rb +38 -4
  20. data/lib/graphql/language/lexer.rb +15 -12
  21. data/lib/graphql/language/nodes.rb +24 -16
  22. data/lib/graphql/language/parser.rb +14 -1
  23. data/lib/graphql/language/printer.rb +23 -7
  24. data/lib/graphql/language.rb +6 -5
  25. data/lib/graphql/query/context.rb +4 -2
  26. data/lib/graphql/query/null_context.rb +1 -5
  27. data/lib/graphql/query.rb +49 -20
  28. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
  29. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  30. data/lib/graphql/rubocop.rb +2 -0
  31. data/lib/graphql/schema/addition.rb +1 -0
  32. data/lib/graphql/schema/always_visible.rb +6 -3
  33. data/lib/graphql/schema/argument.rb +14 -1
  34. data/lib/graphql/schema/build_from_definition.rb +9 -1
  35. data/lib/graphql/schema/directive/flagged.rb +1 -1
  36. data/lib/graphql/schema/enum.rb +49 -15
  37. data/lib/graphql/schema/enum_value.rb +9 -1
  38. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  39. data/lib/graphql/schema/field.rb +91 -36
  40. data/lib/graphql/schema/input_object.rb +20 -7
  41. data/lib/graphql/schema/interface.rb +21 -4
  42. data/lib/graphql/schema/introspection_system.rb +3 -2
  43. data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
  44. data/lib/graphql/schema/member/has_arguments.rb +9 -5
  45. data/lib/graphql/schema/member/has_fields.rb +5 -5
  46. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  47. data/lib/graphql/schema/printer.rb +1 -0
  48. data/lib/graphql/schema/resolver.rb +3 -4
  49. data/lib/graphql/schema/validator/all_validator.rb +2 -0
  50. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  51. data/lib/graphql/schema/visibility/migration.rb +186 -0
  52. data/lib/graphql/schema/visibility/profile.rb +523 -0
  53. data/lib/graphql/schema/visibility.rb +75 -0
  54. data/lib/graphql/schema/warden.rb +80 -21
  55. data/lib/graphql/schema.rb +254 -85
  56. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  57. data/lib/graphql/static_validation/rules/directives_are_defined.rb +2 -1
  58. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  59. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +2 -1
  60. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -0
  61. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +11 -1
  62. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  63. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +10 -1
  64. data/lib/graphql/static_validation/validation_context.rb +15 -0
  65. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +2 -1
  66. data/lib/graphql/subscriptions.rb +3 -1
  67. data/lib/graphql/testing/helpers.rb +7 -3
  68. data/lib/graphql/tracing/notifications_trace.rb +2 -2
  69. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  70. data/lib/graphql/version.rb +1 -1
  71. data/lib/graphql.rb +3 -0
  72. metadata +28 -7
  73. data/lib/graphql/schema/subset.rb +0 -397
@@ -50,12 +50,27 @@ module GraphQL
50
50
  end
51
51
  end
52
52
 
53
+ # Call this method to provide a new comment; OR
54
+ # call it without an argument to get the comment
55
+ # @param new_comment [String]
56
+ # @return [String, nil]
57
+ def comment(new_comment = NOT_CONFIGURED)
58
+ if !NOT_CONFIGURED.equal?(new_comment)
59
+ @comment = new_comment
60
+ elsif defined?(@comment)
61
+ @comment
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
53
67
  # This pushes some configurations _down_ the inheritance tree,
54
68
  # in order to prevent repetitive lookups at runtime.
55
69
  module ConfigurationExtension
56
70
  def inherited(child_class)
57
71
  child_class.introspection(introspection)
58
72
  child_class.description(description)
73
+ child_class.comment(nil)
59
74
  child_class.default_graphql_name = nil
60
75
 
61
76
  if defined?(@graphql_name) && @graphql_name && (self.name.nil? || graphql_name != default_graphql_name)
@@ -135,7 +135,7 @@ 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::Subset)
138
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
139
139
  for ancestor in ancestors
140
140
  if ancestor.respond_to?(:own_arguments) &&
141
141
  (a = ancestor.own_arguments[argument_name]) &&
@@ -198,15 +198,19 @@ module GraphQL
198
198
  end
199
199
 
200
200
  def all_argument_definitions
201
- all_defns = own_arguments.values
202
- all_defns.flatten!
203
- 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
204
208
  end
205
209
 
206
210
  # @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name.
207
211
  def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
208
212
  warden = Warden.from_context(context)
209
- if (arg_config = own_arguments[argument_name]) && ((context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Subset)) || (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden)))
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)))
210
214
  visible_arg || arg_config
211
215
  elsif defined?(@resolver_class) && @resolver_class
212
216
  @resolver_class.get_field_argument(argument_name, context)
@@ -99,7 +99,7 @@ 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::Subset)
102
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
103
103
  for ancestor in ancestors
104
104
  if ancestor.respond_to?(:own_fields) &&
105
105
  (f_entry = ancestor.own_fields[field_name]) &&
@@ -121,7 +121,7 @@ module GraphQL
121
121
  # Choose the most local definition that passes `.visible?` --
122
122
  # stop checking for fields by name once one has been found.
123
123
  if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
124
- visible_fields[field_name] = f
124
+ visible_fields[field_name] = f.ensure_loaded
125
125
  end
126
126
  end
127
127
  end
@@ -135,14 +135,14 @@ module GraphQL
135
135
  # Objects need to check that the interface implementation is visible, too
136
136
  warden = Warden.from_context(context)
137
137
  ancs = ancestors
138
- skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Subset)
138
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
139
139
  i = 0
140
140
  while (ancestor = ancs[i])
141
141
  if ancestor.respond_to?(:own_fields) &&
142
142
  visible_interface_implementation?(ancestor, context, warden) &&
143
143
  (f_entry = ancestor.own_fields[field_name]) &&
144
144
  (skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden)))
145
- return f_entry
145
+ return (skip_visible ? f_entry : f_entry.ensure_loaded)
146
146
  end
147
147
  i += 1
148
148
  end
@@ -161,7 +161,7 @@ module GraphQL
161
161
  # Choose the most local definition that passes `.visible?` --
162
162
  # stop checking for fields by name once one has been found.
163
163
  if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
164
- visible_fields[field_name] = f
164
+ visible_fields[field_name] = f.ensure_loaded
165
165
  end
166
166
  end
167
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
@@ -408,9 +409,7 @@ module GraphQL
408
409
 
409
410
  private
410
411
 
411
- def own_extensions
412
- @own_extensions
413
- end
412
+ attr_reader :own_extensions
414
413
  end
415
414
  end
416
415
  end
@@ -28,6 +28,8 @@ module GraphQL
28
28
  end
29
29
 
30
30
  def validate(object, context, value)
31
+ return EMPTY_ARRAY if permitted_empty_value?(value)
32
+
31
33
  all_errors = EMPTY_ARRAY
32
34
 
33
35
  value.each do |subvalue|
@@ -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
@@ -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