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.
- checksums.yaml +4 -4
- data/lib/graphql/dataloader/async_dataloader.rb +3 -2
- data/lib/graphql/dataloader/source.rb +1 -1
- data/lib/graphql/dataloader.rb +31 -10
- data/lib/graphql/query/null_context.rb +1 -1
- data/lib/graphql/query.rb +49 -16
- data/lib/graphql/schema/always_visible.rb +6 -3
- data/lib/graphql/schema/argument.rb +1 -0
- data/lib/graphql/schema/build_from_definition.rb +1 -0
- data/lib/graphql/schema/enum.rb +2 -0
- data/lib/graphql/schema/input_object.rb +20 -7
- data/lib/graphql/schema/member/has_arguments.rb +2 -2
- data/lib/graphql/schema/member/has_fields.rb +2 -2
- data/lib/graphql/schema/printer.rb +1 -0
- data/lib/graphql/schema/validator/required_validator.rb +28 -4
- data/lib/graphql/schema/visibility/migration.rb +32 -34
- data/lib/graphql/schema/visibility/{subset.rb → profile.rb} +31 -17
- data/lib/graphql/schema/visibility.rb +57 -12
- data/lib/graphql/schema/warden.rb +77 -15
- data/lib/graphql/schema.rb +177 -41
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +2 -1
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +2 -1
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -0
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +11 -1
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +10 -1
- data/lib/graphql/static_validation/validation_context.rb +15 -0
- data/lib/graphql/testing/helpers.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- 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
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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?(
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
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/
|
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
|
-
|
9
|
-
|
10
|
-
|
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.
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
50
|
+
def migration_errors?
|
51
|
+
@migration_errors
|
52
|
+
end
|
23
53
|
|
24
|
-
|
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
|
65
|
-
@
|
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
|
-
@
|
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
|
84
|
+
module NullVisibilityProfile
|
78
85
|
def self.new(context:, schema:)
|
79
|
-
NullWarden.new(context: context, schema: schema).
|
86
|
+
NullWarden.new(context: context, schema: schema).visibility_profile
|
80
87
|
end
|
81
88
|
end
|
82
89
|
|
83
|
-
attr_reader :
|
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
|
108
|
-
@
|
114
|
+
def visibility_profile
|
115
|
+
@visibility_profile ||= VisibilityProfile.new(self)
|
109
116
|
end
|
110
117
|
|
111
|
-
class
|
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
|
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 = @
|
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
|
|