graphql 2.3.18 → 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.

Potentially problematic release.


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

Files changed (30) 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 +1 -1
  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 +2 -0
  11. data/lib/graphql/schema/input_object.rb +20 -7
  12. data/lib/graphql/schema/member/has_arguments.rb +2 -2
  13. data/lib/graphql/schema/member/has_fields.rb +2 -2
  14. data/lib/graphql/schema/printer.rb +1 -0
  15. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  16. data/lib/graphql/schema/visibility/migration.rb +32 -34
  17. data/lib/graphql/schema/visibility/{subset.rb → profile.rb} +31 -17
  18. data/lib/graphql/schema/visibility.rb +57 -12
  19. data/lib/graphql/schema/warden.rb +77 -15
  20. data/lib/graphql/schema.rb +177 -41
  21. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  22. data/lib/graphql/static_validation/rules/directives_are_defined.rb +2 -1
  23. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +2 -1
  24. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -0
  25. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +11 -1
  26. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +10 -1
  27. data/lib/graphql/static_validation/validation_context.rb +15 -0
  28. data/lib/graphql/testing/helpers.rb +1 -1
  29. data/lib/graphql/version.rb +1 -1
  30. 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
25
  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
26
+ profile = self.new(context: context, schema: schema)
27
+ profile.instance_variable_set(:@cached_visible, Hash.new { |h,k| h[k] = true })
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
@@ -403,8 +406,9 @@ module GraphQL
403
406
 
404
407
  @unfiltered_interface_type_memberships = Hash.new { |h, k| h[k] = [] }.compare_by_identity
405
408
  @add_possible_types = Set.new
409
+ @late_types = []
406
410
 
407
- while @unvisited_types.any?
411
+ while @unvisited_types.any? || @late_types.any?
408
412
  while t = @unvisited_types.pop
409
413
  # These have already been checked for `.visible?`
410
414
  visit_type(t)
@@ -418,6 +422,12 @@ module GraphQL
418
422
  end
419
423
  end
420
424
  @add_possible_types.clear
425
+
426
+ while (union_tm = @late_types.shift)
427
+ late_obj_t = union_tm.object_type
428
+ obj_t = @all_types[late_obj_t.graphql_name] || raise("Failed to resolve #{late_obj_t.graphql_name.inspect} from #{union_tm.inspect}")
429
+ union_tm.abstract_type.assign_type_membership_object_type(obj_t)
430
+ end
421
431
  end
422
432
 
423
433
  @all_types.delete_if { |type_name, type_defn| !referenced?(type_defn) }
@@ -470,12 +480,16 @@ module GraphQL
470
480
  type.type_memberships.each do |tm|
471
481
  if @cached_visible[tm]
472
482
  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)
483
+ if obj_t.is_a?(GraphQL::Schema::LateBoundType)
484
+ @late_types << tm
485
+ else
486
+ if obj_t.is_a?(String)
487
+ obj_t = Member::BuildType.constantize(obj_t)
488
+ tm.object_type = obj_t
489
+ end
490
+ if @cached_visible[obj_t]
491
+ add_type(obj_t, tm)
492
+ end
479
493
  end
480
494
  end
481
495
  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,10 @@ module GraphQL
19
19
  PassThruWarden
20
20
  end
21
21
 
22
+ def self.use(schema)
23
+ # no-op
24
+ end
25
+
22
26
  # @param visibility_method [Symbol] a Warden method to call for this entry
23
27
  # @param entry [Object, Array<Object>] One or more definitions for a given name in a GraphQL Schema
24
28
  # @param context [GraphQL::Query::Context]
@@ -61,8 +65,8 @@ module GraphQL
61
65
  def interface_type_memberships(obj_t, ctx); obj_t.interface_type_memberships; end
62
66
  def arguments(owner, ctx); owner.arguments(ctx); end
63
67
  def loadable?(type, ctx); type.visible?(ctx); end
64
- def schema_subset
65
- @schema_subset ||= Warden::SchemaSubset.new(self)
68
+ def visibility_profile
69
+ @visibility_profile ||= Warden::VisibilityProfile.new(self)
66
70
  end
67
71
  end
68
72
  end
@@ -70,17 +74,20 @@ module GraphQL
70
74
  class NullWarden
71
75
  def initialize(_filter = nil, context:, schema:)
72
76
  @schema = schema
73
- @schema_subset = Warden::SchemaSubset.new(self)
77
+ @visibility_profile = Warden::VisibilityProfile.new(self)
74
78
  end
75
79
 
80
+ # No-op, but for compatibility:
81
+ attr_writer :skip_warning
82
+
76
83
  # @api private
77
- module NullSubset
84
+ module NullVisibilityProfile
78
85
  def self.new(context:, schema:)
79
- NullWarden.new(context: context, schema: schema).schema_subset
86
+ NullWarden.new(context: context, schema: schema).visibility_profile
80
87
  end
81
88
  end
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
@@ -88,7 +95,7 @@ module GraphQL
88
95
  def visible_enum_value?(enum_value, _ctx = nil); true; 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
100
  def enum_values(enum_defn); enum_defn.enum_values; 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
@@ -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
@@ -187,16 +194,19 @@ module GraphQL
187
194
  @mutation = @schema.mutation
188
195
  @subscription = @schema.subscription
189
196
  @context = context
190
- @visibility_cache = read_through { |m| schema.visible?(m, context) }
197
+ @visibility_cache = read_through { |m| check_visible(schema, m) }
191
198
  # Initialize all ivars to improve object shape consistency:
192
199
  @types = @visible_types = @reachable_types = @visible_parent_fields =
193
200
  @visible_possible_types = @visible_fields = @visible_arguments = @visible_enum_arrays =
194
201
  @visible_enum_values = @visible_interfaces = @type_visibility = @type_memberships =
195
202
  @visible_and_reachable_type = @unions = @unfiltered_interfaces =
196
- @reachable_type_set = @schema_subset =
203
+ @reachable_type_set = @visibility_profile =
197
204
  nil
205
+ @skip_warning = schema.plugins.any? { |(plugin, _opts)| plugin == GraphQL::Schema::Warden }
198
206
  end
199
207
 
208
+ attr_writer :skip_warning
209
+
200
210
  # @return [Hash<String, GraphQL::BaseType>] Visible types in the schema
201
211
  def types
202
212
  @types ||= begin
@@ -218,7 +228,7 @@ module GraphQL
218
228
  # @return [GraphQL::BaseType, nil] The type named `type_name`, if it exists (else `nil`)
219
229
  def get_type(type_name)
220
230
  @visible_types ||= read_through do |name|
221
- type_defn = @schema.get_type(name, @context)
231
+ type_defn = @schema.get_type(name, @context, false)
222
232
  if type_defn && visible_and_reachable_type?(type_defn)
223
233
  type_defn
224
234
  else
@@ -265,7 +275,7 @@ module GraphQL
265
275
  # @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn`
266
276
  def possible_types(type_defn)
267
277
  @visible_possible_types ||= read_through { |type_defn|
268
- pt = @schema.possible_types(type_defn, @context)
278
+ pt = @schema.possible_types(type_defn, @context, false)
269
279
  pt.select { |t| visible_and_reachable_type?(t) }
270
280
  }
271
281
  @visible_possible_types[type_defn]
@@ -465,6 +475,58 @@ module GraphQL
465
475
  Hash.new { |h, k| h[k] = yield(k) }.compare_by_identity
466
476
  end
467
477
 
478
+ def check_visible(schema, member)
479
+ if schema.visible?(member, @context)
480
+ true
481
+ elsif @skip_warning
482
+ false
483
+ else
484
+ member_s = member.respond_to?(:path) ? member.path : member.inspect
485
+ member_type = case member
486
+ when Module
487
+ if member.respond_to?(:kind)
488
+ member.kind.name.downcase
489
+ else
490
+ ""
491
+ end
492
+ when GraphQL::Schema::Field
493
+ "field"
494
+ when GraphQL::Schema::EnumValue
495
+ "enum value"
496
+ when GraphQL::Schema::Argument
497
+ "argument"
498
+ else
499
+ ""
500
+ end
501
+
502
+ schema_s = schema.name ? "#{schema.name}'s" : ""
503
+ schema_name = schema.name ? "#{schema.name}" : "your schema"
504
+ warn(ADD_WARDEN_WARNING % { schema_s: schema_s, schema_name: schema_name, member: member_s, member_type: member_type })
505
+ @skip_warning = true # only warn once per query
506
+ # If there's no schema name, add the backtrace for additional context:
507
+ if schema_s == ""
508
+ puts caller.map { |l| " #{l}"}
509
+ end
510
+ false
511
+ end
512
+ end
513
+
514
+ ADD_WARDEN_WARNING = <<~WARNING
515
+ DEPRECATION: %{schema_s} "%{member}" %{member_type} returned `false` for `.visible?` but `GraphQL::Schema::Visibility` isn't configured yet.
516
+
517
+ Address this warning by adding:
518
+
519
+ use GraphQL::Schema::Visibility
520
+
521
+ to the definition for %{schema_name}. (Future GraphQL-Ruby versions won't check `.visible?` methods by default.)
522
+
523
+ Alternatively, for legacy behavior, add:
524
+
525
+ use GraphQL::Schema::Warden # legacy visibility behavior
526
+
527
+ For more information see: https://graphql-ruby.org/authorization/visibility.html
528
+ WARNING
529
+
468
530
  def reachable_type_set
469
531
  return @reachable_type_set if @reachable_type_set
470
532