graphql 2.3.7 → 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.
- checksums.yaml +4 -4
- data/lib/generators/graphql/install_generator.rb +46 -0
- data/lib/generators/graphql/orm_mutations_base.rb +1 -1
- data/lib/generators/graphql/templates/base_resolver.erb +2 -0
- data/lib/generators/graphql/type_generator.rb +1 -1
- data/lib/graphql/analysis/field_usage.rb +1 -1
- data/lib/graphql/analysis/query_complexity.rb +3 -3
- data/lib/graphql/analysis/visitor.rb +7 -6
- data/lib/graphql/analysis.rb +1 -1
- data/lib/graphql/current.rb +52 -0
- data/lib/graphql/dataloader/async_dataloader.rb +3 -2
- data/lib/graphql/dataloader/source.rb +6 -3
- data/lib/graphql/dataloader.rb +35 -11
- data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
- data/lib/graphql/execution/interpreter/resolve.rb +10 -6
- data/lib/graphql/execution/interpreter/runtime.rb +29 -25
- data/lib/graphql/execution/interpreter.rb +2 -0
- data/lib/graphql/execution/lookahead.rb +10 -10
- data/lib/graphql/introspection/directive_type.rb +1 -1
- data/lib/graphql/introspection/entry_points.rb +2 -2
- data/lib/graphql/introspection/field_type.rb +1 -1
- data/lib/graphql/introspection/schema_type.rb +6 -11
- data/lib/graphql/introspection/type_type.rb +5 -5
- data/lib/graphql/invalid_null_error.rb +1 -1
- data/lib/graphql/language/comment.rb +18 -0
- data/lib/graphql/language/document_from_schema_definition.rb +55 -28
- data/lib/graphql/language/lexer.rb +15 -15
- data/lib/graphql/language/nodes.rb +24 -16
- data/lib/graphql/language/parser.rb +14 -1
- data/lib/graphql/language/printer.rb +23 -7
- data/lib/graphql/language/sanitized_printer.rb +1 -1
- data/lib/graphql/language.rb +6 -6
- data/lib/graphql/query/context.rb +7 -1
- data/lib/graphql/query/null_context.rb +2 -2
- data/lib/graphql/query.rb +60 -8
- data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
- data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
- data/lib/graphql/rubocop.rb +2 -0
- data/lib/graphql/schema/addition.rb +1 -0
- data/lib/graphql/schema/always_visible.rb +6 -2
- data/lib/graphql/schema/argument.rb +14 -1
- data/lib/graphql/schema/build_from_definition.rb +9 -1
- data/lib/graphql/schema/directive/flagged.rb +1 -1
- data/lib/graphql/schema/enum.rb +54 -20
- data/lib/graphql/schema/enum_value.rb +9 -1
- data/lib/graphql/schema/field/connection_extension.rb +1 -1
- data/lib/graphql/schema/field.rb +92 -37
- data/lib/graphql/schema/has_single_input_argument.rb +2 -1
- data/lib/graphql/schema/input_object.rb +27 -13
- data/lib/graphql/schema/interface.rb +21 -4
- data/lib/graphql/schema/introspection_system.rb +5 -16
- data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
- data/lib/graphql/schema/member/has_arguments.rb +14 -9
- data/lib/graphql/schema/member/has_fields.rb +8 -6
- data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
- data/lib/graphql/schema/printer.rb +1 -0
- data/lib/graphql/schema/resolver.rb +7 -9
- data/lib/graphql/schema/type_expression.rb +2 -2
- data/lib/graphql/schema/validator/all_validator.rb +62 -0
- data/lib/graphql/schema/validator/required_validator.rb +28 -4
- data/lib/graphql/schema/validator.rb +2 -0
- data/lib/graphql/schema/visibility/migration.rb +186 -0
- data/lib/graphql/schema/visibility/profile.rb +523 -0
- data/lib/graphql/schema/visibility.rb +75 -0
- data/lib/graphql/schema/warden.rb +156 -10
- data/lib/graphql/schema.rb +283 -85
- data/lib/graphql/static_validation/base_visitor.rb +6 -5
- data/lib/graphql/static_validation/literal_validator.rb +4 -4
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +3 -2
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
- data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +3 -2
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +8 -7
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +12 -2
- data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
- data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
- data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +3 -3
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
- data/lib/graphql/static_validation/rules/subscription_root_exists.rb +1 -1
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +11 -2
- data/lib/graphql/static_validation/validation_context.rb +17 -2
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +2 -1
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +10 -4
- data/lib/graphql/subscriptions/event.rb +1 -1
- data/lib/graphql/subscriptions.rb +5 -3
- data/lib/graphql/testing/helpers.rb +9 -5
- data/lib/graphql/tracing/notifications_trace.rb +2 -2
- data/lib/graphql/types/relay/connection_behaviors.rb +10 -0
- data/lib/graphql/types/relay/edge_behaviors.rb +10 -0
- data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
- data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +3 -0
- metadata +29 -8
- data/lib/graphql/language/token.rb +0 -34
- data/lib/graphql/schema/invalid_type_error.rb +0 -7
|
@@ -135,10 +135,11 @@ module GraphQL
|
|
|
135
135
|
|
|
136
136
|
def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
|
|
137
137
|
warden = Warden.from_context(context)
|
|
138
|
+
skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
|
|
138
139
|
for ancestor in ancestors
|
|
139
140
|
if ancestor.respond_to?(:own_arguments) &&
|
|
140
141
|
(a = ancestor.own_arguments[argument_name]) &&
|
|
141
|
-
(a = Warden.visible_entry?(:visible_argument?, a, context, warden))
|
|
142
|
+
(skip_visible || (a = Warden.visible_entry?(:visible_argument?, a, context, warden)))
|
|
142
143
|
return a
|
|
143
144
|
end
|
|
144
145
|
end
|
|
@@ -197,16 +198,20 @@ module GraphQL
|
|
|
197
198
|
end
|
|
198
199
|
|
|
199
200
|
def all_argument_definitions
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
if own_arguments.any?
|
|
202
|
+
all_defns = own_arguments.values
|
|
203
|
+
all_defns.flatten!
|
|
204
|
+
all_defns
|
|
205
|
+
else
|
|
206
|
+
EmptyObjects::EMPTY_ARRAY
|
|
207
|
+
end
|
|
203
208
|
end
|
|
204
209
|
|
|
205
210
|
# @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name.
|
|
206
211
|
def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
|
|
207
212
|
warden = Warden.from_context(context)
|
|
208
|
-
if (arg_config = own_arguments[argument_name]) && (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden))
|
|
209
|
-
visible_arg
|
|
213
|
+
if (arg_config = own_arguments[argument_name]) && ((context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)) || (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden)))
|
|
214
|
+
visible_arg || arg_config
|
|
210
215
|
elsif defined?(@resolver_class) && @resolver_class
|
|
211
216
|
@resolver_class.get_field_argument(argument_name, context)
|
|
212
217
|
else
|
|
@@ -230,7 +235,7 @@ module GraphQL
|
|
|
230
235
|
# @return [Interpreter::Arguments, Execution::Lazy<Interpreter::Arguments>]
|
|
231
236
|
def coerce_arguments(parent_object, values, context, &block)
|
|
232
237
|
# Cache this hash to avoid re-merging it
|
|
233
|
-
arg_defns = context.
|
|
238
|
+
arg_defns = context.types.arguments(self)
|
|
234
239
|
total_args_count = arg_defns.size
|
|
235
240
|
|
|
236
241
|
finished_args = nil
|
|
@@ -364,8 +369,8 @@ module GraphQL
|
|
|
364
369
|
end
|
|
365
370
|
|
|
366
371
|
if !(
|
|
367
|
-
context.
|
|
368
|
-
context.
|
|
372
|
+
context.types.possible_types(argument.loads).include?(application_object_type) ||
|
|
373
|
+
context.types.loadable?(argument.loads, context)
|
|
369
374
|
)
|
|
370
375
|
err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object)
|
|
371
376
|
application_object = load_application_object_failed(err)
|
|
@@ -99,11 +99,12 @@ module GraphQL
|
|
|
99
99
|
module InterfaceMethods
|
|
100
100
|
def get_field(field_name, context = GraphQL::Query::NullContext.instance)
|
|
101
101
|
warden = Warden.from_context(context)
|
|
102
|
+
skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
|
|
102
103
|
for ancestor in ancestors
|
|
103
104
|
if ancestor.respond_to?(:own_fields) &&
|
|
104
105
|
(f_entry = ancestor.own_fields[field_name]) &&
|
|
105
|
-
(
|
|
106
|
-
return
|
|
106
|
+
(skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden)))
|
|
107
|
+
return f_entry
|
|
107
108
|
end
|
|
108
109
|
end
|
|
109
110
|
nil
|
|
@@ -120,7 +121,7 @@ module GraphQL
|
|
|
120
121
|
# Choose the most local definition that passes `.visible?` --
|
|
121
122
|
# stop checking for fields by name once one has been found.
|
|
122
123
|
if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
|
|
123
|
-
visible_fields[field_name] = f
|
|
124
|
+
visible_fields[field_name] = f.ensure_loaded
|
|
124
125
|
end
|
|
125
126
|
end
|
|
126
127
|
end
|
|
@@ -134,13 +135,14 @@ module GraphQL
|
|
|
134
135
|
# Objects need to check that the interface implementation is visible, too
|
|
135
136
|
warden = Warden.from_context(context)
|
|
136
137
|
ancs = ancestors
|
|
138
|
+
skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
|
|
137
139
|
i = 0
|
|
138
140
|
while (ancestor = ancs[i])
|
|
139
141
|
if ancestor.respond_to?(:own_fields) &&
|
|
140
142
|
visible_interface_implementation?(ancestor, context, warden) &&
|
|
141
143
|
(f_entry = ancestor.own_fields[field_name]) &&
|
|
142
|
-
(
|
|
143
|
-
return
|
|
144
|
+
(skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden)))
|
|
145
|
+
return (skip_visible ? f_entry : f_entry.ensure_loaded)
|
|
144
146
|
end
|
|
145
147
|
i += 1
|
|
146
148
|
end
|
|
@@ -159,7 +161,7 @@ module GraphQL
|
|
|
159
161
|
# Choose the most local definition that passes `.visible?` --
|
|
160
162
|
# stop checking for fields by name once one has been found.
|
|
161
163
|
if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
|
|
162
|
-
visible_fields[field_name] = f
|
|
164
|
+
visible_fields[field_name] = f.ensure_loaded
|
|
163
165
|
end
|
|
164
166
|
end
|
|
165
167
|
end
|
|
@@ -7,7 +7,11 @@ module GraphQL
|
|
|
7
7
|
module HasUnresolvedTypeError
|
|
8
8
|
private
|
|
9
9
|
def add_unresolved_type_error(child_class)
|
|
10
|
-
child_class.
|
|
10
|
+
if child_class.name # Don't set this for anonymous classes
|
|
11
|
+
child_class.const_set(:UnresolvedTypeError, Class.new(GraphQL::UnresolvedTypeError))
|
|
12
|
+
else
|
|
13
|
+
child_class.const_set(:UnresolvedTypeError, UnresolvedTypeError)
|
|
14
|
+
end
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -8,6 +8,7 @@ module GraphQL
|
|
|
8
8
|
# - Arguments, via `.argument(...)` helper, which will be applied to the field.
|
|
9
9
|
# - Return type, via `.type(..., null: ...)`, which will be applied to the field.
|
|
10
10
|
# - Description, via `.description(...)`, which will be applied to the field
|
|
11
|
+
# - Comment, via `.comment(...)`, which will be applied to the field
|
|
11
12
|
# - Resolution, via `#resolve(**args)` method, which will be called to resolve the field.
|
|
12
13
|
# - `#object` and `#context` accessors for use during `#resolve`.
|
|
13
14
|
#
|
|
@@ -19,7 +20,7 @@ module GraphQL
|
|
|
19
20
|
# @see {GraphQL::Function} `Resolver` is a replacement for `GraphQL::Function`
|
|
20
21
|
class Resolver
|
|
21
22
|
include Schema::Member::GraphQLTypeNames
|
|
22
|
-
# Really we only need description from here, but:
|
|
23
|
+
# Really we only need description & comment from here, but:
|
|
23
24
|
extend Schema::Member::BaseDSLMethods
|
|
24
25
|
extend GraphQL::Schema::Member::HasArguments
|
|
25
26
|
extend GraphQL::Schema::Member::HasValidators
|
|
@@ -36,7 +37,7 @@ module GraphQL
|
|
|
36
37
|
@field = field
|
|
37
38
|
# Since this hash is constantly rebuilt, cache it for this call
|
|
38
39
|
@arguments_by_keyword = {}
|
|
39
|
-
|
|
40
|
+
context.types.arguments(self.class).each do |arg|
|
|
40
41
|
@arguments_by_keyword[arg.keyword] = arg
|
|
41
42
|
end
|
|
42
43
|
@prepared_arguments = nil
|
|
@@ -152,7 +153,7 @@ module GraphQL
|
|
|
152
153
|
# @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.)
|
|
153
154
|
def authorized?(**inputs)
|
|
154
155
|
arg_owner = @field # || self.class
|
|
155
|
-
args =
|
|
156
|
+
args = context.types.arguments(arg_owner)
|
|
156
157
|
authorize_arguments(args, inputs)
|
|
157
158
|
end
|
|
158
159
|
|
|
@@ -169,7 +170,7 @@ module GraphQL
|
|
|
169
170
|
private
|
|
170
171
|
|
|
171
172
|
def authorize_arguments(args, inputs)
|
|
172
|
-
args.
|
|
173
|
+
args.each do |argument|
|
|
173
174
|
arg_keyword = argument.keyword
|
|
174
175
|
if inputs.key?(arg_keyword) && !(arg_value = inputs[arg_keyword]).nil? && (arg_value != argument.default_value)
|
|
175
176
|
auth_result = argument.authorized?(self, arg_value, context)
|
|
@@ -182,10 +183,9 @@ module GraphQL
|
|
|
182
183
|
elsif auth_result == false
|
|
183
184
|
return auth_result
|
|
184
185
|
end
|
|
185
|
-
else
|
|
186
|
-
true
|
|
187
186
|
end
|
|
188
187
|
end
|
|
188
|
+
true
|
|
189
189
|
end
|
|
190
190
|
|
|
191
191
|
def load_arguments(args)
|
|
@@ -409,9 +409,7 @@ module GraphQL
|
|
|
409
409
|
|
|
410
410
|
private
|
|
411
411
|
|
|
412
|
-
|
|
413
|
-
@own_extensions
|
|
414
|
-
end
|
|
412
|
+
attr_reader :own_extensions
|
|
415
413
|
end
|
|
416
414
|
end
|
|
417
415
|
end
|
|
@@ -5,13 +5,13 @@ module GraphQL
|
|
|
5
5
|
module TypeExpression
|
|
6
6
|
# Fetch a type from a type map by its AST specification.
|
|
7
7
|
# Return `nil` if not found.
|
|
8
|
-
# @param type_owner [#
|
|
8
|
+
# @param type_owner [#type] A thing for looking up types by name
|
|
9
9
|
# @param ast_node [GraphQL::Language::Nodes::AbstractNode]
|
|
10
10
|
# @return [Class, GraphQL::Schema::NonNull, GraphQL::Schema:List]
|
|
11
11
|
def self.build_type(type_owner, ast_node)
|
|
12
12
|
case ast_node
|
|
13
13
|
when GraphQL::Language::Nodes::TypeName
|
|
14
|
-
type_owner.
|
|
14
|
+
type_owner.type(ast_node.name) # rubocop:disable Development/ContextIsPassedCop -- this is a `context` or `warden`, it's already query-aware
|
|
15
15
|
when GraphQL::Language::Nodes::NonNullType
|
|
16
16
|
ast_inner_type = ast_node.of_type
|
|
17
17
|
inner_type = build_type(type_owner, ast_inner_type)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphQL
|
|
4
|
+
class Schema
|
|
5
|
+
class Validator
|
|
6
|
+
# Use this to validate each member of an array value.
|
|
7
|
+
#
|
|
8
|
+
# @example validate format of all strings in an array
|
|
9
|
+
#
|
|
10
|
+
# argument :handles, [String],
|
|
11
|
+
# validates: { all: { format: { with: /\A[a-z0-9_]+\Z/ } } }
|
|
12
|
+
#
|
|
13
|
+
# @example multiple validators can be combined
|
|
14
|
+
#
|
|
15
|
+
# argument :handles, [String],
|
|
16
|
+
# validates: { all: { format: { with: /\A[a-z0-9_]+\Z/ }, length: { maximum: 32 } } }
|
|
17
|
+
#
|
|
18
|
+
# @example any type can be used
|
|
19
|
+
#
|
|
20
|
+
# argument :choices, [Integer],
|
|
21
|
+
# validates: { all: { inclusion: { in: 1..12 } } }
|
|
22
|
+
#
|
|
23
|
+
class AllValidator < Validator
|
|
24
|
+
def initialize(validated:, allow_blank: false, allow_null: false, **validators)
|
|
25
|
+
super(validated: validated, allow_blank: allow_blank, allow_null: allow_null)
|
|
26
|
+
|
|
27
|
+
@validators = Validator.from_config(validated, validators)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate(object, context, value)
|
|
31
|
+
return EMPTY_ARRAY if permitted_empty_value?(value)
|
|
32
|
+
|
|
33
|
+
all_errors = EMPTY_ARRAY
|
|
34
|
+
|
|
35
|
+
value.each do |subvalue|
|
|
36
|
+
@validators.each do |validator|
|
|
37
|
+
errors = validator.validate(object, context, subvalue)
|
|
38
|
+
if errors &&
|
|
39
|
+
(errors.is_a?(Array) && errors != EMPTY_ARRAY) ||
|
|
40
|
+
(errors.is_a?(String))
|
|
41
|
+
if all_errors.frozen? # It's empty
|
|
42
|
+
all_errors = []
|
|
43
|
+
end
|
|
44
|
+
if errors.is_a?(String)
|
|
45
|
+
all_errors << errors
|
|
46
|
+
else
|
|
47
|
+
all_errors.concat(errors)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless all_errors.frozen?
|
|
54
|
+
all_errors.uniq!
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
all_errors
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -35,9 +35,10 @@ module GraphQL
|
|
|
35
35
|
# end
|
|
36
36
|
#
|
|
37
37
|
class RequiredValidator < Validator
|
|
38
|
-
# @param one_of [
|
|
38
|
+
# @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
|
|
39
|
+
# @param argument [Symbol] An argument that is required for this field
|
|
39
40
|
# @param message [String]
|
|
40
|
-
def initialize(one_of: nil, argument: nil, message:
|
|
41
|
+
def initialize(one_of: nil, argument: nil, message: nil, **default_options)
|
|
41
42
|
@one_of = if one_of
|
|
42
43
|
one_of
|
|
43
44
|
elsif argument
|
|
@@ -49,7 +50,7 @@ module GraphQL
|
|
|
49
50
|
super(**default_options)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
def validate(_object,
|
|
53
|
+
def validate(_object, context, value)
|
|
53
54
|
matched_conditions = 0
|
|
54
55
|
|
|
55
56
|
if !value.nil?
|
|
@@ -73,9 +74,32 @@ module GraphQL
|
|
|
73
74
|
if matched_conditions == 1
|
|
74
75
|
nil # OK
|
|
75
76
|
else
|
|
76
|
-
@message
|
|
77
|
+
@message || build_message(context)
|
|
77
78
|
end
|
|
78
79
|
end
|
|
80
|
+
|
|
81
|
+
def build_message(context)
|
|
82
|
+
argument_definitions = @validated.arguments(context).values
|
|
83
|
+
required_names = @one_of.map do |arg_keyword|
|
|
84
|
+
if arg_keyword.is_a?(Array)
|
|
85
|
+
names = arg_keyword.map { |arg| arg_keyword_to_grapqhl_name(argument_definitions, arg) }
|
|
86
|
+
"(" + names.join(" and ") + ")"
|
|
87
|
+
else
|
|
88
|
+
arg_keyword_to_grapqhl_name(argument_definitions, arg_keyword)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if required_names.size == 1
|
|
93
|
+
"%{validated} must include the following argument: #{required_names.first}."
|
|
94
|
+
else
|
|
95
|
+
"%{validated} must include exactly one of the following arguments: #{required_names.join(", ")}."
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def arg_keyword_to_grapqhl_name(argument_definitions, arg_keyword)
|
|
100
|
+
argument_definition = argument_definitions.find { |defn| defn.keyword == arg_keyword }
|
|
101
|
+
argument_definition.graphql_name
|
|
102
|
+
end
|
|
79
103
|
end
|
|
80
104
|
end
|
|
81
105
|
end
|
|
@@ -169,3 +169,5 @@ require "graphql/schema/validator/allow_null_validator"
|
|
|
169
169
|
GraphQL::Schema::Validator.install(:allow_null, GraphQL::Schema::Validator::AllowNullValidator)
|
|
170
170
|
require "graphql/schema/validator/allow_blank_validator"
|
|
171
171
|
GraphQL::Schema::Validator.install(:allow_blank, GraphQL::Schema::Validator::AllowBlankValidator)
|
|
172
|
+
require "graphql/schema/validator/all_validator"
|
|
173
|
+
GraphQL::Schema::Validator.install(:all, GraphQL::Schema::Validator::AllValidator)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
class Schema
|
|
4
|
+
class Visibility
|
|
5
|
+
# You can use this to see how {GraphQL::Schema::Warden} and {GraphQL::Schema::Visibility::Profile}
|
|
6
|
+
# handle `.visible?` differently in your schema.
|
|
7
|
+
#
|
|
8
|
+
# It runs the same method on both implementations and raises an error when the results diverge.
|
|
9
|
+
#
|
|
10
|
+
# To fix the error, modify your schema so that both implementations return the same thing.
|
|
11
|
+
# Or, open an issue on GitHub to discuss the difference.
|
|
12
|
+
#
|
|
13
|
+
# This plugin adds overhead to runtime and may cause unexpected crashes -- **don't** use it in production!
|
|
14
|
+
#
|
|
15
|
+
# This plugin adds two keys to `context` when running:
|
|
16
|
+
#
|
|
17
|
+
# - `visibility_migration_running: true`
|
|
18
|
+
# - For the {Schema::Warden} which it instantiates, it adds `visibility_migration_warden_running: true`.
|
|
19
|
+
#
|
|
20
|
+
# Use those keys to modify your `visible?` behavior as needed.
|
|
21
|
+
#
|
|
22
|
+
# Also, in a pinch, you can set `skip_visibility_migration_error: true` in context to turn off this behavior per-query.
|
|
23
|
+
# (In that case, it uses {Profile} directly.)
|
|
24
|
+
#
|
|
25
|
+
# @example Adding this plugin
|
|
26
|
+
#
|
|
27
|
+
# use GraphQL::Schema::Visibility, migration_errors: true
|
|
28
|
+
#
|
|
29
|
+
class Migration < GraphQL::Schema::Visibility::Profile
|
|
30
|
+
class RuntimeTypesMismatchError < GraphQL::Error
|
|
31
|
+
def initialize(method_called, warden_result, profile_result, method_args)
|
|
32
|
+
super(<<~ERR)
|
|
33
|
+
Mismatch in types for `##{method_called}(#{method_args.map(&:inspect).join(", ")})`:
|
|
34
|
+
|
|
35
|
+
#{compare_results(warden_result, profile_result)}
|
|
36
|
+
|
|
37
|
+
Update your `.visible?` implementation to make these implementations return the same value.
|
|
38
|
+
|
|
39
|
+
See: https://graphql-ruby.org/authorization/visibility_migration.html
|
|
40
|
+
ERR
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
def compare_results(warden_result, profile_result)
|
|
45
|
+
if warden_result.is_a?(Array) && profile_result.is_a?(Array)
|
|
46
|
+
all_results = warden_result | profile_result
|
|
47
|
+
all_results.sort_by!(&:graphql_name)
|
|
48
|
+
|
|
49
|
+
entries_text = all_results.map { |entry| "#{entry.graphql_name} (#{entry})"}
|
|
50
|
+
width = entries_text.map(&:size).max
|
|
51
|
+
yes = " ✔ "
|
|
52
|
+
no = " "
|
|
53
|
+
res = "".dup
|
|
54
|
+
res << "#{"Result".center(width)} Warden Profile \n"
|
|
55
|
+
all_results.each_with_index do |entry, idx|
|
|
56
|
+
res << "#{entries_text[idx].ljust(width)}#{warden_result.include?(entry) ? yes : no}#{profile_result.include?(entry) ? yes : no}\n"
|
|
57
|
+
end
|
|
58
|
+
res << "\n"
|
|
59
|
+
else
|
|
60
|
+
"- Warden returned: #{humanize(warden_result)}\n\n- Visibility::Profile returned: #{humanize(profile_result)}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
def humanize(val)
|
|
64
|
+
case val
|
|
65
|
+
when Array
|
|
66
|
+
"#{val.size}: #{val.map { |v| humanize(v) }.sort.inspect}"
|
|
67
|
+
when Module
|
|
68
|
+
if val.respond_to?(:graphql_name)
|
|
69
|
+
"#{val.graphql_name} (#{val.inspect})"
|
|
70
|
+
else
|
|
71
|
+
val.inspect
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
val.inspect
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def initialize(context:, schema:, name: nil)
|
|
80
|
+
@name = name
|
|
81
|
+
@skip_error = context[:skip_visibility_migration_error] || context.is_a?(Query::NullContext) || context.is_a?(Hash)
|
|
82
|
+
@profile_types = GraphQL::Schema::Visibility::Profile.new(context: context, schema: schema)
|
|
83
|
+
if !@skip_error
|
|
84
|
+
context[:visibility_migration_running] = true
|
|
85
|
+
warden_ctx_vals = context.to_h.dup
|
|
86
|
+
warden_ctx_vals[:visibility_migration_warden_running] = true
|
|
87
|
+
if schema.const_defined?(:WardenCompatSchema, false) # don't use a defn from a superclass
|
|
88
|
+
warden_schema = schema.const_get(:WardenCompatSchema, false)
|
|
89
|
+
else
|
|
90
|
+
warden_schema = Class.new(schema)
|
|
91
|
+
warden_schema.use_visibility_profile = false
|
|
92
|
+
# TODO public API
|
|
93
|
+
warden_schema.send(:add_type_and_traverse, [warden_schema.query, warden_schema.mutation, warden_schema.subscription].compact, root: true)
|
|
94
|
+
warden_schema.send(:add_type_and_traverse, warden_schema.directives.values + warden_schema.orphan_types, root: false)
|
|
95
|
+
schema.const_set(:WardenCompatSchema, warden_schema)
|
|
96
|
+
end
|
|
97
|
+
warden_ctx = GraphQL::Query::Context.new(query: context.query, values: warden_ctx_vals)
|
|
98
|
+
warden_ctx.warden = GraphQL::Schema::Warden.new(schema: warden_schema, context: warden_ctx)
|
|
99
|
+
warden_ctx.warden.skip_warning = true
|
|
100
|
+
warden_ctx.types = @warden_types = warden_ctx.warden.visibility_profile
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def loaded_types
|
|
105
|
+
@profile_types.loaded_types
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
PUBLIC_PROFILE_METHODS = [
|
|
109
|
+
:enum_values,
|
|
110
|
+
:interfaces,
|
|
111
|
+
:all_types,
|
|
112
|
+
:all_types_h,
|
|
113
|
+
:fields,
|
|
114
|
+
:loadable?,
|
|
115
|
+
:type,
|
|
116
|
+
:arguments,
|
|
117
|
+
:argument,
|
|
118
|
+
:directive_exists?,
|
|
119
|
+
:directives,
|
|
120
|
+
:field,
|
|
121
|
+
:query_root,
|
|
122
|
+
:mutation_root,
|
|
123
|
+
:possible_types,
|
|
124
|
+
:subscription_root,
|
|
125
|
+
:reachable_type?
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
PUBLIC_PROFILE_METHODS.each do |profile_method|
|
|
129
|
+
define_method(profile_method) do |*args|
|
|
130
|
+
call_method_and_compare(profile_method, args)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def call_method_and_compare(method, args)
|
|
135
|
+
res_1 = @profile_types.public_send(method, *args)
|
|
136
|
+
if @skip_error
|
|
137
|
+
return res_1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
res_2 = @warden_types.public_send(method, *args)
|
|
141
|
+
normalized_res_1 = res_1.is_a?(Array) ? Set.new(res_1) : res_1
|
|
142
|
+
normalized_res_2 = res_2.is_a?(Array) ? Set.new(res_2) : res_2
|
|
143
|
+
if !equivalent_schema_members?(normalized_res_1, normalized_res_2)
|
|
144
|
+
# Raise the errors with the orignally returned values:
|
|
145
|
+
err = RuntimeTypesMismatchError.new(method, res_2, res_1, args)
|
|
146
|
+
raise err
|
|
147
|
+
else
|
|
148
|
+
res_1
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def equivalent_schema_members?(member1, member2)
|
|
153
|
+
if member1.class != member2.class
|
|
154
|
+
return false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
case member1
|
|
158
|
+
when Set
|
|
159
|
+
member1_array = member1.to_a.sort_by(&:graphql_name)
|
|
160
|
+
member2_array = member2.to_a.sort_by(&:graphql_name)
|
|
161
|
+
member1_array.each_with_index do |inner_member1, idx|
|
|
162
|
+
inner_member2 = member2_array[idx]
|
|
163
|
+
equivalent_schema_members?(inner_member1, inner_member2)
|
|
164
|
+
end
|
|
165
|
+
when GraphQL::Schema::Field
|
|
166
|
+
member1.ensure_loaded
|
|
167
|
+
member2.ensure_loaded
|
|
168
|
+
if member1.introspection? && member2.introspection?
|
|
169
|
+
member1.inspect == member2.inspect
|
|
170
|
+
else
|
|
171
|
+
member1 == member2
|
|
172
|
+
end
|
|
173
|
+
when Module
|
|
174
|
+
if member1.introspection? && member2.introspection?
|
|
175
|
+
member1.graphql_name == member2.graphql_name
|
|
176
|
+
else
|
|
177
|
+
member1 == member2
|
|
178
|
+
end
|
|
179
|
+
else
|
|
180
|
+
member1 == member2
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|