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.
- 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 +3 -5
- 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 +19 -3
- data/lib/graphql/schema/enum_value.rb +1 -1
- 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 +34 -35
- data/lib/graphql/schema/visibility/{subset.rb → profile.rb} +37 -19
- data/lib/graphql/schema/visibility.rb +57 -12
- data/lib/graphql/schema/warden.rb +87 -21
- 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
|
-
def self.
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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.
|
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?(
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
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/
|
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,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
|
65
|
-
@
|
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
|
-
@
|
84
|
+
@visibility_profile = Warden::VisibilityProfile.new(self)
|
74
85
|
end
|
75
86
|
|
76
|
-
#
|
77
|
-
|
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 :
|
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);
|
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
|
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
|
@@ -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
|
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 = @
|
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
|
|