graphql 2.3.18 → 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/dataloader/async_dataloader.rb +3 -2
  3. data/lib/graphql/dataloader/source.rb +1 -1
  4. data/lib/graphql/dataloader.rb +31 -10
  5. data/lib/graphql/query/null_context.rb +3 -5
  6. data/lib/graphql/query.rb +49 -16
  7. data/lib/graphql/schema/always_visible.rb +6 -3
  8. data/lib/graphql/schema/argument.rb +1 -0
  9. data/lib/graphql/schema/build_from_definition.rb +1 -0
  10. data/lib/graphql/schema/enum.rb +19 -3
  11. data/lib/graphql/schema/enum_value.rb +1 -1
  12. data/lib/graphql/schema/input_object.rb +20 -7
  13. data/lib/graphql/schema/member/has_arguments.rb +2 -2
  14. data/lib/graphql/schema/member/has_fields.rb +2 -2
  15. data/lib/graphql/schema/printer.rb +1 -0
  16. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  17. data/lib/graphql/schema/visibility/migration.rb +34 -35
  18. data/lib/graphql/schema/visibility/{subset.rb → profile.rb} +37 -19
  19. data/lib/graphql/schema/visibility.rb +57 -12
  20. data/lib/graphql/schema/warden.rb +87 -21
  21. data/lib/graphql/schema.rb +177 -41
  22. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  23. data/lib/graphql/static_validation/rules/directives_are_defined.rb +2 -1
  24. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +2 -1
  25. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -0
  26. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +11 -1
  27. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +10 -1
  28. data/lib/graphql/static_validation/validation_context.rb +15 -0
  29. data/lib/graphql/testing/helpers.rb +1 -1
  30. data/lib/graphql/version.rb +1 -1
  31. metadata +3 -3
@@ -11,26 +11,28 @@ module GraphQL
11
11
  # - It doesn't use {Schema}'s top-level caches (eg {Schema.references_to}, {Schema.possible_types}, {Schema.types})
12
12
  # - It doesn't hide Interface or Union types when all their possible types are hidden. (Instead, those types should implement `.visible?` to hide in that case.)
13
13
  # - It checks `.visible?` on root introspection types
14
- #
15
- # In the future, {Subset} will support lazy-loading types as needed during execution and multi-request caching of subsets.
16
- class Subset
17
- # @return [Schema::Visibility::Subset]
14
+ # - It can be used to cache profiles by name for re-use across queries
15
+ class Profile
16
+ # @return [Schema::Visibility::Profile]
18
17
  def self.from_context(ctx, schema)
19
18
  if ctx.respond_to?(:types) && (types = ctx.types).is_a?(self)
20
19
  types
21
20
  else
22
- # TODO use a cached instance from the schema
23
- self.new(context: ctx, schema: schema)
21
+ schema.visibility.profile_for(ctx, nil)
24
22
  end
25
23
  end
26
24
 
27
- def self.pass_thru(context:, schema:)
28
- subset = self.new(context: context, schema: schema)
29
- subset.instance_variable_set(:@cached_visible, Hash.new { |h,k| h[k] = true })
30
- subset
25
+ def self.null_profile(context:, schema:)
26
+ profile = self.new(name: "NullProfile", context: context, schema: schema)
27
+ profile.instance_variable_set(:@cached_visible, Hash.new { |k, v| k[v] = true }.compare_by_identity)
28
+ profile
31
29
  end
32
30
 
33
- def initialize(context:, schema:)
31
+ # @return [Symbol, nil]
32
+ attr_reader :name
33
+
34
+ def initialize(name: nil, context:, schema:)
35
+ @name = name
34
36
  @context = context
35
37
  @schema = schema
36
38
  @all_types = {}
@@ -67,6 +69,7 @@ module GraphQL
67
69
  @cached_visible_arguments = Hash.new do |h, arg|
68
70
  h[arg] = if @cached_visible[arg] && (arg_type = arg.type.unwrap) && @cached_visible[arg_type]
69
71
  add_type(arg_type, arg)
72
+ arg.validate_default_value
70
73
  true
71
74
  else
72
75
  false
@@ -120,7 +123,7 @@ module GraphQL
120
123
  end.compare_by_identity
121
124
 
122
125
  @cached_enum_values = Hash.new do |h, enum_t|
123
- values = non_duplicate_items(enum_t.all_enum_value_definitions, @cached_visible)
126
+ values = non_duplicate_items(enum_t.enum_values(@context), @cached_visible)
124
127
  if values.size == 0
125
128
  raise GraphQL::Schema::Enum::MissingValuesError.new(enum_t)
126
129
  end
@@ -324,6 +327,10 @@ module GraphQL
324
327
  !!@all_types[name]
325
328
  end
326
329
 
330
+ def visible_enum_value?(enum_value, _ctx = nil)
331
+ @cached_visible[enum_value]
332
+ end
333
+
327
334
  private
328
335
 
329
336
  def add_if_visible(t)
@@ -403,8 +410,9 @@ module GraphQL
403
410
 
404
411
  @unfiltered_interface_type_memberships = Hash.new { |h, k| h[k] = [] }.compare_by_identity
405
412
  @add_possible_types = Set.new
413
+ @late_types = []
406
414
 
407
- while @unvisited_types.any?
415
+ while @unvisited_types.any? || @late_types.any?
408
416
  while t = @unvisited_types.pop
409
417
  # These have already been checked for `.visible?`
410
418
  visit_type(t)
@@ -418,6 +426,12 @@ module GraphQL
418
426
  end
419
427
  end
420
428
  @add_possible_types.clear
429
+
430
+ while (union_tm = @late_types.shift)
431
+ late_obj_t = union_tm.object_type
432
+ obj_t = @all_types[late_obj_t.graphql_name] || raise("Failed to resolve #{late_obj_t.graphql_name.inspect} from #{union_tm.inspect}")
433
+ union_tm.abstract_type.assign_type_membership_object_type(obj_t)
434
+ end
421
435
  end
422
436
 
423
437
  @all_types.delete_if { |type_name, type_defn| !referenced?(type_defn) }
@@ -470,12 +484,16 @@ module GraphQL
470
484
  type.type_memberships.each do |tm|
471
485
  if @cached_visible[tm]
472
486
  obj_t = tm.object_type
473
- if obj_t.is_a?(String)
474
- obj_t = Member::BuildType.constantize(obj_t)
475
- tm.object_type = obj_t
476
- end
477
- if @cached_visible[obj_t]
478
- add_type(obj_t, tm)
487
+ if obj_t.is_a?(GraphQL::Schema::LateBoundType)
488
+ @late_types << tm
489
+ else
490
+ if obj_t.is_a?(String)
491
+ obj_t = Member::BuildType.constantize(obj_t)
492
+ tm.object_type = obj_t
493
+ end
494
+ if @cached_visible[obj_t]
495
+ add_type(obj_t, tm)
496
+ end
479
497
  end
480
498
  end
481
499
  end
@@ -1,28 +1,73 @@
1
1
  # frozen_string_literal: true
2
- require "graphql/schema/visibility/subset"
2
+ require "graphql/schema/visibility/profile"
3
3
  require "graphql/schema/visibility/migration"
4
4
 
5
5
  module GraphQL
6
6
  class Schema
7
+ # Use this plugin to make some parts of your schema hidden from some viewers.
8
+ #
7
9
  class Visibility
8
- def self.use(schema, preload: nil, migration_errors: false)
9
- schema.visibility = self.new(schema, preload: preload)
10
- schema.use_schema_visibility = true
10
+ # @param schema [Class<GraphQL::Schema>]
11
+ # @param profiles [Hash<Symbol => Hash>] A hash of `name => context` pairs for preloading visibility profiles
12
+ # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?`)
13
+ # @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
14
+ def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails) ? Rails.env.production? : nil), migration_errors: false)
15
+ schema.visibility = self.new(schema, dynamic: dynamic, preload: preload, profiles: profiles, migration_errors: migration_errors)
16
+ end
17
+
18
+ def initialize(schema, dynamic:, preload:, profiles:, migration_errors:)
19
+ @schema = schema
20
+ schema.use_visibility_profile = true
11
21
  if migration_errors
12
- schema.subset_class = Migration
22
+ schema.visibility_profile_class = Migration
23
+ end
24
+ @profiles = profiles
25
+ @cached_profiles = {}
26
+ @dynamic = dynamic
27
+ @migration_errors = migration_errors
28
+ if preload
29
+ profiles.each do |profile_name, example_ctx|
30
+ example_ctx[:visibility_profile] = profile_name
31
+ prof = profile_for(example_ctx, profile_name)
32
+ prof.all_types # force loading
33
+ end
13
34
  end
14
35
  end
15
36
 
16
- def initialize(schema, preload:)
17
- @schema = schema
18
- @cached_subsets = {}
37
+ # Make another Visibility for `schema` based on this one
38
+ # @return [Visibility]
39
+ # @api private
40
+ def dup_for(other_schema)
41
+ self.class.new(
42
+ other_schema,
43
+ dynamic: @dynamic,
44
+ preload: @preload,
45
+ profiles: @profiles,
46
+ migration_errors: @migration_errors
47
+ )
48
+ end
19
49
 
20
- if preload.nil? && defined?(Rails) && Rails.env.production?
21
- preload = true
22
- end
50
+ def migration_errors?
51
+ @migration_errors
52
+ end
23
53
 
24
- if preload
54
+ attr_reader :cached_profiles
25
55
 
56
+ def profile_for(context, visibility_profile)
57
+ if @profiles.any?
58
+ if visibility_profile.nil?
59
+ if @dynamic
60
+ @schema.visibility_profile_class.new(context: context, schema: @schema)
61
+ elsif @profiles.any?
62
+ raise ArgumentError, "#{@schema} expects a visibility profile, but `visibility_profile:` wasn't passed. Provide a `visibility_profile:` value or add `dynamic: true` to your visibility configuration."
63
+ end
64
+ elsif !@profiles.include?(visibility_profile)
65
+ raise ArgumentError, "`#{visibility_profile.inspect}` isn't allowed for `visibility_profile:` (must be one of #{@profiles.keys.map(&:inspect).join(", ")}). Or, add `#{visibility_profile.inspect}` to the list of profiles in the schema definition."
66
+ else
67
+ @cached_profiles[visibility_profile] ||= @schema.visibility_profile_class.new(name: visibility_profile, context: context, schema: @schema)
68
+ end
69
+ else
70
+ @schema.visibility_profile_class.new(context: context, schema: @schema)
26
71
  end
27
72
  end
28
73
  end
@@ -19,6 +19,17 @@ module GraphQL
19
19
  PassThruWarden
20
20
  end
21
21
 
22
+ def self.types_from_context(context)
23
+ context.types || PassThruWarden
24
+ rescue NoMethodError
25
+ # this might be a hash which won't respond to #warden
26
+ PassThruWarden
27
+ end
28
+
29
+ def self.use(schema)
30
+ # no-op
31
+ end
32
+
22
33
  # @param visibility_method [Symbol] a Warden method to call for this entry
23
34
  # @param entry [Object, Array<Object>] One or more definitions for a given name in a GraphQL Schema
24
35
  # @param context [GraphQL::Query::Context]
@@ -61,8 +72,8 @@ module GraphQL
61
72
  def interface_type_memberships(obj_t, ctx); obj_t.interface_type_memberships; end
62
73
  def arguments(owner, ctx); owner.arguments(ctx); end
63
74
  def loadable?(type, ctx); type.visible?(ctx); end
64
- def schema_subset
65
- @schema_subset ||= Warden::SchemaSubset.new(self)
75
+ def visibility_profile
76
+ @visibility_profile ||= Warden::VisibilityProfile.new(self)
66
77
  end
67
78
  end
68
79
  end
@@ -70,27 +81,23 @@ module GraphQL
70
81
  class NullWarden
71
82
  def initialize(_filter = nil, context:, schema:)
72
83
  @schema = schema
73
- @schema_subset = Warden::SchemaSubset.new(self)
84
+ @visibility_profile = Warden::VisibilityProfile.new(self)
74
85
  end
75
86
 
76
- # @api private
77
- module NullSubset
78
- def self.new(context:, schema:)
79
- NullWarden.new(context: context, schema: schema).schema_subset
80
- end
81
- end
87
+ # No-op, but for compatibility:
88
+ attr_writer :skip_warning
82
89
 
83
- attr_reader :schema_subset
90
+ attr_reader :visibility_profile
84
91
 
85
92
  def visible_field?(field_defn, _ctx = nil, owner = nil); true; end
86
93
  def visible_argument?(arg_defn, _ctx = nil); true; end
87
94
  def visible_type?(type_defn, _ctx = nil); true; end
88
- def visible_enum_value?(enum_value, _ctx = nil); true; end
95
+ def visible_enum_value?(enum_value, _ctx = nil); enum_value.visible?(Query::NullContext.instance); end
89
96
  def visible_type_membership?(type_membership, _ctx = nil); true; end
90
97
  def interface_type_memberships(obj_type, _ctx = nil); obj_type.interface_type_memberships; end
91
- def get_type(type_name); @schema.get_type(type_name); end # rubocop:disable Development/ContextIsPassedCop
98
+ def get_type(type_name); @schema.get_type(type_name, Query::NullContext.instance, false); end # rubocop:disable Development/ContextIsPassedCop
92
99
  def arguments(argument_owner, ctx = nil); argument_owner.all_argument_definitions; end
93
- def enum_values(enum_defn); enum_defn.enum_values; end # rubocop:disable Development/ContextIsPassedCop
100
+ def enum_values(enum_defn); enum_defn.enum_values(Query::NullContext.instance); end # rubocop:disable Development/ContextIsPassedCop
94
101
  def get_argument(parent_type, argument_name); parent_type.get_argument(argument_name); end # rubocop:disable Development/ContextIsPassedCop
95
102
  def types; @schema.types; end # rubocop:disable Development/ContextIsPassedCop
96
103
  def root_type_for_operation(op_name); @schema.root_type_for_operation(op_name); end
@@ -100,15 +107,15 @@ module GraphQL
100
107
  def reachable_type?(type_name); true; end
101
108
  def loadable?(type, _ctx); true; end
102
109
  def reachable_types; @schema.types.values; end # rubocop:disable Development/ContextIsPassedCop
103
- def possible_types(type_defn); @schema.possible_types(type_defn); end
110
+ def possible_types(type_defn); @schema.possible_types(type_defn, Query::NullContext.instance, false); end
104
111
  def interfaces(obj_type); obj_type.interfaces; end
105
112
  end
106
113
 
107
- def schema_subset
108
- @schema_subset ||= SchemaSubset.new(self)
114
+ def visibility_profile
115
+ @visibility_profile ||= VisibilityProfile.new(self)
109
116
  end
110
117
 
111
- class SchemaSubset
118
+ class VisibilityProfile
112
119
  def initialize(warden)
113
120
  @warden = warden
114
121
  end
@@ -176,6 +183,10 @@ module GraphQL
176
183
  def reachable_type?(type_name)
177
184
  !!@warden.reachable_type?(type_name)
178
185
  end
186
+
187
+ def visible_enum_value?(enum_value, ctx = nil)
188
+ @warden.visible_enum_value?(enum_value, ctx)
189
+ end
179
190
  end
180
191
 
181
192
  # @param context [GraphQL::Query::Context]
@@ -187,16 +198,19 @@ module GraphQL
187
198
  @mutation = @schema.mutation
188
199
  @subscription = @schema.subscription
189
200
  @context = context
190
- @visibility_cache = read_through { |m| schema.visible?(m, context) }
201
+ @visibility_cache = read_through { |m| check_visible(schema, m) }
191
202
  # Initialize all ivars to improve object shape consistency:
192
203
  @types = @visible_types = @reachable_types = @visible_parent_fields =
193
204
  @visible_possible_types = @visible_fields = @visible_arguments = @visible_enum_arrays =
194
205
  @visible_enum_values = @visible_interfaces = @type_visibility = @type_memberships =
195
206
  @visible_and_reachable_type = @unions = @unfiltered_interfaces =
196
- @reachable_type_set = @schema_subset =
207
+ @reachable_type_set = @visibility_profile =
197
208
  nil
209
+ @skip_warning = schema.plugins.any? { |(plugin, _opts)| plugin == GraphQL::Schema::Warden }
198
210
  end
199
211
 
212
+ attr_writer :skip_warning
213
+
200
214
  # @return [Hash<String, GraphQL::BaseType>] Visible types in the schema
201
215
  def types
202
216
  @types ||= begin
@@ -218,7 +232,7 @@ module GraphQL
218
232
  # @return [GraphQL::BaseType, nil] The type named `type_name`, if it exists (else `nil`)
219
233
  def get_type(type_name)
220
234
  @visible_types ||= read_through do |name|
221
- type_defn = @schema.get_type(name, @context)
235
+ type_defn = @schema.get_type(name, @context, false)
222
236
  if type_defn && visible_and_reachable_type?(type_defn)
223
237
  type_defn
224
238
  else
@@ -265,7 +279,7 @@ module GraphQL
265
279
  # @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn`
266
280
  def possible_types(type_defn)
267
281
  @visible_possible_types ||= read_through { |type_defn|
268
- pt = @schema.possible_types(type_defn, @context)
282
+ pt = @schema.possible_types(type_defn, @context, false)
269
283
  pt.select { |t| visible_and_reachable_type?(t) }
270
284
  }
271
285
  @visible_possible_types[type_defn]
@@ -465,6 +479,58 @@ module GraphQL
465
479
  Hash.new { |h, k| h[k] = yield(k) }.compare_by_identity
466
480
  end
467
481
 
482
+ def check_visible(schema, member)
483
+ if schema.visible?(member, @context)
484
+ true
485
+ elsif @skip_warning
486
+ false
487
+ else
488
+ member_s = member.respond_to?(:path) ? member.path : member.inspect
489
+ member_type = case member
490
+ when Module
491
+ if member.respond_to?(:kind)
492
+ member.kind.name.downcase
493
+ else
494
+ ""
495
+ end
496
+ when GraphQL::Schema::Field
497
+ "field"
498
+ when GraphQL::Schema::EnumValue
499
+ "enum value"
500
+ when GraphQL::Schema::Argument
501
+ "argument"
502
+ else
503
+ ""
504
+ end
505
+
506
+ schema_s = schema.name ? "#{schema.name}'s" : ""
507
+ schema_name = schema.name ? "#{schema.name}" : "your schema"
508
+ warn(ADD_WARDEN_WARNING % { schema_s: schema_s, schema_name: schema_name, member: member_s, member_type: member_type })
509
+ @skip_warning = true # only warn once per query
510
+ # If there's no schema name, add the backtrace for additional context:
511
+ if schema_s == ""
512
+ puts caller.map { |l| " #{l}"}
513
+ end
514
+ false
515
+ end
516
+ end
517
+
518
+ ADD_WARDEN_WARNING = <<~WARNING
519
+ DEPRECATION: %{schema_s} "%{member}" %{member_type} returned `false` for `.visible?` but `GraphQL::Schema::Visibility` isn't configured yet.
520
+
521
+ Address this warning by adding:
522
+
523
+ use GraphQL::Schema::Visibility
524
+
525
+ to the definition for %{schema_name}. (Future GraphQL-Ruby versions won't check `.visible?` methods by default.)
526
+
527
+ Alternatively, for legacy behavior, add:
528
+
529
+ use GraphQL::Schema::Warden # legacy visibility behavior
530
+
531
+ For more information see: https://graphql-ruby.org/authorization/visibility.html
532
+ WARNING
533
+
468
534
  def reachable_type_set
469
535
  return @reachable_type_set if @reachable_type_set
470
536