graphql 2.3.18 → 2.4.1

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 (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