graphql 2.5.14 → 2.5.22
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/detailed_trace_generator.rb +77 -0
- data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
- data/lib/graphql/dashboard/application_controller.rb +41 -0
- data/lib/graphql/dashboard/landings_controller.rb +9 -0
- data/lib/graphql/dashboard/statics_controller.rb +31 -0
- data/lib/graphql/dashboard/subscriptions.rb +2 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
- data/lib/graphql/dashboard.rb +11 -73
- data/lib/graphql/dataloader/null_dataloader.rb +7 -3
- data/lib/graphql/date_encoding_error.rb +1 -1
- data/lib/graphql/execution/interpreter.rb +0 -1
- data/lib/graphql/execution/multiplex.rb +1 -1
- data/lib/graphql/execution/next/field_resolve_step.rb +711 -0
- data/lib/graphql/execution/next/load_argument_step.rb +60 -0
- data/lib/graphql/execution/next/prepare_object_step.rb +129 -0
- data/lib/graphql/execution/next/runner.rb +389 -0
- data/lib/graphql/execution/next/selections_step.rb +37 -0
- data/lib/graphql/execution/next.rb +70 -0
- data/lib/graphql/execution.rb +1 -0
- data/lib/graphql/execution_error.rb +13 -10
- data/lib/graphql/introspection/directive_type.rb +7 -3
- data/lib/graphql/introspection/dynamic_fields.rb +5 -1
- data/lib/graphql/introspection/entry_points.rb +11 -3
- data/lib/graphql/introspection/enum_value_type.rb +5 -5
- data/lib/graphql/introspection/field_type.rb +13 -5
- data/lib/graphql/introspection/input_value_type.rb +21 -13
- data/lib/graphql/introspection/type_type.rb +64 -28
- data/lib/graphql/invalid_null_error.rb +11 -5
- data/lib/graphql/language/document_from_schema_definition.rb +2 -1
- data/lib/graphql/language.rb +21 -12
- data/lib/graphql/pagination/connection.rb +2 -0
- data/lib/graphql/pagination/connections.rb +32 -0
- data/lib/graphql/query/context.rb +3 -2
- data/lib/graphql/query/null_context.rb +9 -3
- data/lib/graphql/schema/argument.rb +12 -0
- data/lib/graphql/schema/build_from_definition.rb +7 -0
- data/lib/graphql/schema/directive.rb +8 -1
- data/lib/graphql/schema/field/connection_extension.rb +15 -35
- data/lib/graphql/schema/field/scope_extension.rb +22 -13
- data/lib/graphql/schema/field.rb +80 -48
- data/lib/graphql/schema/field_extension.rb +33 -0
- data/lib/graphql/schema/member/base_dsl_methods.rb +1 -1
- data/lib/graphql/schema/member/has_arguments.rb +37 -14
- data/lib/graphql/schema/member/has_authorization.rb +35 -0
- data/lib/graphql/schema/member/has_dataloader.rb +37 -0
- data/lib/graphql/schema/member/has_fields.rb +81 -4
- data/lib/graphql/schema/member.rb +5 -0
- data/lib/graphql/schema/object.rb +1 -0
- data/lib/graphql/schema/resolver.rb +45 -1
- data/lib/graphql/schema/validator/required_validator.rb +33 -2
- data/lib/graphql/schema/visibility.rb +3 -3
- data/lib/graphql/schema.rb +53 -10
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +2 -2
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
- data/lib/graphql/subscriptions.rb +1 -1
- data/lib/graphql/testing/mock_action_cable.rb +111 -0
- data/lib/graphql/testing.rb +1 -0
- data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
- data/lib/graphql/tracing/detailed_trace.rb +70 -7
- data/lib/graphql/tracing/perfetto_trace.rb +208 -78
- data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
- data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
- data/lib/graphql/types/relay/has_node_field.rb +13 -8
- data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
- data/lib/graphql/types/relay/node_behaviors.rb +13 -2
- data/lib/graphql/unauthorized_error.rb +5 -1
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +8 -2
- metadata +17 -3
|
@@ -5,11 +5,88 @@ module GraphQL
|
|
|
5
5
|
class Member
|
|
6
6
|
# Shared code for Objects, Interfaces, Mutations, Subscriptions
|
|
7
7
|
module HasFields
|
|
8
|
+
include EmptyObjects
|
|
8
9
|
# Add a field to this object or interface with the given definition
|
|
9
|
-
# @
|
|
10
|
+
# @param name_positional [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); `name:` keyword is also accepted
|
|
11
|
+
# @param type_positional [Class, GraphQL::BaseType, Array] The return type of this field; `type:` keyword is also accepted
|
|
12
|
+
# @param desc_positional [String] Field description; `description:` keyword is also accepted
|
|
13
|
+
# @option kwargs [Symbol] :name The underscore-cased version of this field name (will be camelized for the GraphQL API); positional argument also accepted
|
|
14
|
+
# @option kwargs [Class, GraphQL::BaseType, Array] :type The return type of this field; positional argument is also accepted
|
|
15
|
+
# @option kwargs [Boolean] :null (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
|
|
16
|
+
# @option kwargs [String] :description Field description; positional argument also accepted
|
|
17
|
+
# @option kwargs [String] :comment Field comment
|
|
18
|
+
# @option kwargs [String] :deprecation_reason If present, the field is marked "deprecated" with this message
|
|
19
|
+
# @option kwargs [Symbol] :method The method to call on the underlying object to resolve this field (defaults to `name`)
|
|
20
|
+
# @option kwargs [String, Symbol] :hash_key The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
|
|
21
|
+
# @option kwargs [Array<String, Symbol>] :dig The nested hash keys to lookup on the underlying hash to resolve this field using dig
|
|
22
|
+
# @option kwargs [Symbol, true] :resolver_method The method on the type to call to resolve this field (defaults to `name`)
|
|
23
|
+
# @option kwargs [Symbol, true] :resolve_static Used by {Schema.execute_next} to produce a single value, shared by all objects which resolve this field. Called on the owner type class with `context, **arguments`
|
|
24
|
+
# @option kwargs [Symbol, true] :resolve_batch Used by {Schema.execute_next} map `objects` to a same-sized Array of results. Called on the owner type class with `objects, context, **arguments`.
|
|
25
|
+
# @option kwargs [Symbol, true] :resolve_each Used by {Schema.execute_next} to get a value value for each item. Called on the owner type class with `object, context, **arguments`.
|
|
26
|
+
# @option kwargs [Symbol, true] :resolve_legacy_instance_method Used by {Schema.execute_next} to get a value value for each item. Calls an instance method on the object type class.
|
|
27
|
+
# @option kwargs [Boolean] :connection `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
|
|
28
|
+
# @option kwargs [Class] :connection_extension The extension to add, to implement connections. If `nil`, no extension is added.
|
|
29
|
+
# @option kwargs [Integer, nil] :max_page_size For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
|
|
30
|
+
# @option kwargs [Integer, nil] :default_page_size For connections, the default number of items to return from this field, or `nil` to return unlimited results.
|
|
31
|
+
# @option kwargs [Boolean] :introspection If true, this field will be marked as `#introspection?` and the name may begin with `__`
|
|
32
|
+
# @option kwargs [{String=>GraphQL::Schema::Argument, Hash}] :arguments Arguments for this field (may be added in the block, also)
|
|
33
|
+
# @option kwargs [Boolean] :camelize If true, the field name will be camelized when building the schema
|
|
34
|
+
# @option kwargs [Numeric] :complexity When provided, set the complexity for this field
|
|
35
|
+
# @option kwargs [Boolean] :scope If true, the return type's `.scope_items` method will be called on the return value
|
|
36
|
+
# @option kwargs [Symbol, String] :subscription_scope A key in `context` which will be used to scope subscription payloads
|
|
37
|
+
# @option kwargs [Array<Class, Hash<Class => Object>>] :extensions Named extensions to apply to this field (see also {#extension})
|
|
38
|
+
# @option kwargs [Hash{Class => Hash}] :directives Directives to apply to this field
|
|
39
|
+
# @option kwargs [Boolean] :trace If true, a {GraphQL::Tracing} tracer will measure this scalar field
|
|
40
|
+
# @option kwargs [Boolean] :broadcastable Whether or not this field can be distributed in subscription broadcasts
|
|
41
|
+
# @option kwargs [Language::Nodes::FieldDefinition, nil] :ast_node If this schema was parsed from definition, this AST node defined the field
|
|
42
|
+
# @option kwargs [Boolean] :method_conflict_warning If false, skip the warning if this field's method conflicts with a built-in method
|
|
43
|
+
# @option kwargs [Array<Hash>] :validates Configurations for validating this field
|
|
44
|
+
# @option kwargs [Object] :fallback_value A fallback value if the method is not defined
|
|
45
|
+
# @option kwargs [Class<GraphQL::Schema::Mutation>] :mutation
|
|
46
|
+
# @option kwargs [Class<GraphQL::Schema::Resolver>] :resolver
|
|
47
|
+
# @option kwargs [Class<GraphQL::Schema::Subscription>] :subscription
|
|
48
|
+
# @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby)
|
|
49
|
+
# @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby)
|
|
50
|
+
# @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby)
|
|
51
|
+
# @option kwargs [Class, Hash] :dataload Shorthand for dataloader lookups
|
|
52
|
+
# @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field
|
|
53
|
+
# @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled.
|
|
54
|
+
# @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
|
|
55
|
+
# @yieldparam field [GraphQL::Schema::Field] The newly-created field instance
|
|
56
|
+
# @yieldreturn [void]
|
|
10
57
|
# @return [GraphQL::Schema::Field]
|
|
11
|
-
def field(
|
|
12
|
-
|
|
58
|
+
def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &definition_block)
|
|
59
|
+
resolver = kwargs.delete(:resolver)
|
|
60
|
+
mutation = kwargs.delete(:mutation)
|
|
61
|
+
subscription = kwargs.delete(:subscription)
|
|
62
|
+
if (resolver_class = resolver || mutation || subscription)
|
|
63
|
+
# Add a reference to that parent class
|
|
64
|
+
kwargs[:resolver_class] = resolver_class
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
kwargs[:name] ||= name_positional
|
|
68
|
+
if !type_positional.nil?
|
|
69
|
+
if desc_positional
|
|
70
|
+
if kwargs[:description]
|
|
71
|
+
raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{kwargs[:description].inspect})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
kwargs[:description] = desc_positional
|
|
75
|
+
kwargs[:type] = type_positional
|
|
76
|
+
elsif (resolver || mutation) && type_positional.is_a?(String)
|
|
77
|
+
# The return type should be copied from the resolver, and the second positional argument is the description
|
|
78
|
+
kwargs[:description] = type_positional
|
|
79
|
+
else
|
|
80
|
+
kwargs[:type] = type_positional
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if type_positional.is_a?(Class) && type_positional < GraphQL::Schema::Mutation
|
|
84
|
+
raise ArgumentError, "Use `field #{name_positional.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
kwargs[:owner] = self
|
|
89
|
+
field_defn = field_class.new(**kwargs, &definition_block)
|
|
13
90
|
add_field(field_defn)
|
|
14
91
|
field_defn
|
|
15
92
|
end
|
|
@@ -232,7 +309,7 @@ module GraphQL
|
|
|
232
309
|
end
|
|
233
310
|
end
|
|
234
311
|
|
|
235
|
-
# @param [GraphQL::Schema::Field]
|
|
312
|
+
# @param field_defn [GraphQL::Schema::Field]
|
|
236
313
|
# @return [String] A warning to give when this field definition might conflict with a built-in method
|
|
237
314
|
def conflict_field_name_warning(field_defn)
|
|
238
315
|
"#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
require 'graphql/schema/member/base_dsl_methods'
|
|
3
3
|
require 'graphql/schema/member/graphql_type_names'
|
|
4
4
|
require 'graphql/schema/member/has_ast_node'
|
|
5
|
+
require 'graphql/schema/member/has_authorization'
|
|
5
6
|
require 'graphql/schema/member/has_dataloader'
|
|
6
7
|
require 'graphql/schema/member/has_directives'
|
|
7
8
|
require 'graphql/schema/member/has_deprecation_reason'
|
|
@@ -31,6 +32,10 @@ module GraphQL
|
|
|
31
32
|
extend HasPath
|
|
32
33
|
extend HasAstNode
|
|
33
34
|
extend HasDirectives
|
|
35
|
+
|
|
36
|
+
def self.authorizes?(_ctx)
|
|
37
|
+
false
|
|
38
|
+
end
|
|
34
39
|
end
|
|
35
40
|
end
|
|
36
41
|
end
|
|
@@ -5,6 +5,7 @@ require "graphql/query/null_context"
|
|
|
5
5
|
module GraphQL
|
|
6
6
|
class Schema
|
|
7
7
|
class Object < GraphQL::Schema::Member
|
|
8
|
+
extend GraphQL::Schema::Member::HasAuthorization
|
|
8
9
|
extend GraphQL::Schema::Member::HasFields
|
|
9
10
|
extend GraphQL::Schema::Member::HasInterfaces
|
|
10
11
|
include Member::HasDataloader
|
|
@@ -23,6 +23,7 @@ module GraphQL
|
|
|
23
23
|
# Really we only need description & comment from here, but:
|
|
24
24
|
extend Schema::Member::BaseDSLMethods
|
|
25
25
|
extend GraphQL::Schema::Member::HasArguments
|
|
26
|
+
extend GraphQL::Schema::Member::HasAuthorization
|
|
26
27
|
extend GraphQL::Schema::Member::HasValidators
|
|
27
28
|
include Schema::Member::HasPath
|
|
28
29
|
extend Schema::Member::HasPath
|
|
@@ -45,8 +46,10 @@ module GraphQL
|
|
|
45
46
|
@prepared_arguments = nil
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
attr_accessor :exec_result, :exec_index, :field_resolve_step
|
|
50
|
+
|
|
48
51
|
# @return [Object] The application object this field is being resolved on
|
|
49
|
-
|
|
52
|
+
attr_accessor :object
|
|
50
53
|
|
|
51
54
|
# @return [GraphQL::Query::Context]
|
|
52
55
|
attr_reader :context
|
|
@@ -54,6 +57,47 @@ module GraphQL
|
|
|
54
57
|
# @return [GraphQL::Schema::Field]
|
|
55
58
|
attr_reader :field
|
|
56
59
|
|
|
60
|
+
attr_writer :prepared_arguments
|
|
61
|
+
|
|
62
|
+
def call
|
|
63
|
+
if self.class < Schema::HasSingleInputArgument
|
|
64
|
+
@prepared_arguments = @prepared_arguments[:input]
|
|
65
|
+
end
|
|
66
|
+
q = context.query
|
|
67
|
+
trace_objs = [object]
|
|
68
|
+
q.current_trace.begin_execute_field(field, @prepared_arguments, trace_objs, q)
|
|
69
|
+
is_authed, new_return_value = authorized?(**@prepared_arguments)
|
|
70
|
+
|
|
71
|
+
if (runner = @field_resolve_step.runner).resolves_lazies && runner.schema.lazy?(is_authed)
|
|
72
|
+
is_authed, new_return_value = runner.schema.sync_lazy(is_authed)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
result = if is_authed
|
|
76
|
+
Schema::Validator.validate!(self.class.validators, object, context, @prepared_arguments, as: @field)
|
|
77
|
+
call_resolve(@prepared_arguments)
|
|
78
|
+
else
|
|
79
|
+
new_return_value
|
|
80
|
+
end
|
|
81
|
+
q = context.query
|
|
82
|
+
q.current_trace.end_execute_field(field, @prepared_arguments, trace_objs, q, [result])
|
|
83
|
+
|
|
84
|
+
exec_result[exec_index] = result
|
|
85
|
+
rescue RuntimeError => err
|
|
86
|
+
exec_result[exec_index] = err
|
|
87
|
+
rescue StandardError => stderr
|
|
88
|
+
exec_result[exec_index] = begin
|
|
89
|
+
context.query.handle_or_reraise(stderr)
|
|
90
|
+
rescue GraphQL::ExecutionError => ex_err
|
|
91
|
+
ex_err
|
|
92
|
+
end
|
|
93
|
+
ensure
|
|
94
|
+
field_pending_steps = field_resolve_step.pending_steps
|
|
95
|
+
field_pending_steps.delete(self)
|
|
96
|
+
if field_pending_steps.size == 0 && field_resolve_step.field_results
|
|
97
|
+
field_resolve_step.runner.add_step(field_resolve_step)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
57
101
|
def arguments
|
|
58
102
|
@prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
|
|
59
103
|
end
|
|
@@ -8,6 +8,13 @@ module GraphQL
|
|
|
8
8
|
#
|
|
9
9
|
# (This is for specifying mutually exclusive sets of arguments.)
|
|
10
10
|
#
|
|
11
|
+
# If you use {GraphQL::Schema::Visibility} to hide all the arguments in a `one_of: [..]` set,
|
|
12
|
+
# then a developer-facing {GraphQL::Error} will be raised during execution. Pass `allow_all_hidden: true` to
|
|
13
|
+
# skip validation in this case instead.
|
|
14
|
+
#
|
|
15
|
+
# This validator also implements `argument ... required: :nullable`. If an argument has `required: :nullable`
|
|
16
|
+
# but it's hidden with {GraphQL::Schema::Visibility}, then this validator doesn't run.
|
|
17
|
+
#
|
|
11
18
|
# @example Require exactly one of these arguments
|
|
12
19
|
#
|
|
13
20
|
# field :update_amount, IngredientAmount, null: false do
|
|
@@ -37,15 +44,17 @@ module GraphQL
|
|
|
37
44
|
class RequiredValidator < Validator
|
|
38
45
|
# @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
|
|
39
46
|
# @param argument [Symbol] An argument that is required for this field
|
|
47
|
+
# @param allow_all_hidden [Boolean] If `true`, then this validator won't run if all the `one_of: ...` arguments have been hidden
|
|
40
48
|
# @param message [String]
|
|
41
|
-
def initialize(one_of: nil, argument: nil, message: nil, **default_options)
|
|
49
|
+
def initialize(one_of: nil, argument: nil, allow_all_hidden: nil, message: nil, **default_options)
|
|
42
50
|
@one_of = if one_of
|
|
43
51
|
one_of
|
|
44
52
|
elsif argument
|
|
45
|
-
[argument]
|
|
53
|
+
[ argument ]
|
|
46
54
|
else
|
|
47
55
|
raise ArgumentError, "`one_of:` or `argument:` must be given in `validates required: {...}`"
|
|
48
56
|
end
|
|
57
|
+
@allow_all_hidden = allow_all_hidden.nil? ? !!argument : allow_all_hidden
|
|
49
58
|
@message = message
|
|
50
59
|
super(**default_options)
|
|
51
60
|
end
|
|
@@ -54,10 +63,17 @@ module GraphQL
|
|
|
54
63
|
fully_matched_conditions = 0
|
|
55
64
|
partially_matched_conditions = 0
|
|
56
65
|
|
|
66
|
+
visible_keywords = context.types.arguments(@validated).map(&:keyword)
|
|
67
|
+
no_visible_conditions = true
|
|
68
|
+
|
|
57
69
|
if !value.nil?
|
|
58
70
|
@one_of.each do |one_of_condition|
|
|
59
71
|
case one_of_condition
|
|
60
72
|
when Symbol
|
|
73
|
+
if no_visible_conditions && visible_keywords.include?(one_of_condition)
|
|
74
|
+
no_visible_conditions = false
|
|
75
|
+
end
|
|
76
|
+
|
|
61
77
|
if value.key?(one_of_condition)
|
|
62
78
|
fully_matched_conditions += 1
|
|
63
79
|
end
|
|
@@ -66,6 +82,9 @@ module GraphQL
|
|
|
66
82
|
full_match = true
|
|
67
83
|
|
|
68
84
|
one_of_condition.each do |k|
|
|
85
|
+
if no_visible_conditions && visible_keywords.include?(k)
|
|
86
|
+
no_visible_conditions = false
|
|
87
|
+
end
|
|
69
88
|
if value.key?(k)
|
|
70
89
|
any_match = true
|
|
71
90
|
else
|
|
@@ -88,6 +107,18 @@ module GraphQL
|
|
|
88
107
|
end
|
|
89
108
|
end
|
|
90
109
|
|
|
110
|
+
if no_visible_conditions
|
|
111
|
+
if @allow_all_hidden
|
|
112
|
+
return nil
|
|
113
|
+
else
|
|
114
|
+
raise GraphQL::Error, <<~ERR
|
|
115
|
+
#{@validated.path} validates `required: ...` but all required arguments were hidden.
|
|
116
|
+
|
|
117
|
+
Update your schema definition to allow the client to see some fields or skip validation by adding `required: { ..., allow_all_hidden: true }`
|
|
118
|
+
ERR
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
91
122
|
if fully_matched_conditions == 1 && partially_matched_conditions == 0
|
|
92
123
|
nil # OK
|
|
93
124
|
else
|
|
@@ -10,9 +10,9 @@ module GraphQL
|
|
|
10
10
|
class Visibility
|
|
11
11
|
# @param schema [Class<GraphQL::Schema>]
|
|
12
12
|
# @param profiles [Hash<Symbol => Hash>] A hash of `name => context` pairs for preloading visibility profiles
|
|
13
|
-
# @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?`)
|
|
13
|
+
# @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?` and `Rails.env.staging?`)
|
|
14
14
|
# @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
|
|
15
|
-
def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? Rails.env.production? : nil), migration_errors: false)
|
|
15
|
+
def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? (Rails.env.production? || Rails.env.staging?) : nil), migration_errors: false)
|
|
16
16
|
profiles&.each { |name, ctx|
|
|
17
17
|
ctx[:visibility_profile] = name
|
|
18
18
|
ctx.freeze
|
|
@@ -191,7 +191,7 @@ module GraphQL
|
|
|
191
191
|
if refresh
|
|
192
192
|
@top_level_profile = nil
|
|
193
193
|
end
|
|
194
|
-
@top_level_profile ||= @schema.visibility_profile_class.new(context:
|
|
194
|
+
@top_level_profile ||= @schema.visibility_profile_class.new(context: @schema.null_context, schema: @schema, visibility: self)
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
private
|
data/lib/graphql/schema.rb
CHANGED
|
@@ -330,10 +330,16 @@ module GraphQL
|
|
|
330
330
|
find_inherited_value(:plugins, EMPTY_ARRAY) + own_plugins
|
|
331
331
|
end
|
|
332
332
|
|
|
333
|
+
attr_writer :null_context
|
|
334
|
+
|
|
335
|
+
def null_context
|
|
336
|
+
@null_context || GraphQL::Query::NullContext.instance
|
|
337
|
+
end
|
|
338
|
+
|
|
333
339
|
# Build a map of `{ name => type }` and return it
|
|
334
340
|
# @return [Hash<String => Class>] A dictionary of type classes by their GraphQL name
|
|
335
341
|
# @see get_type Which is more efficient for finding _one type_ by name, because it doesn't merge hashes.
|
|
336
|
-
def types(context =
|
|
342
|
+
def types(context = null_context)
|
|
337
343
|
if use_visibility_profile?
|
|
338
344
|
types = Visibility::Profile.from_context(context, self)
|
|
339
345
|
return types.all_types_h
|
|
@@ -366,7 +372,7 @@ module GraphQL
|
|
|
366
372
|
# @param context [GraphQL::Query::Context] Used for filtering definitions at query-time
|
|
367
373
|
# @param use_visibility_profile Private, for migration to {Schema::Visibility}
|
|
368
374
|
# @return [Module, nil] A type, or nil if there's no type called `type_name`
|
|
369
|
-
def get_type(type_name, context =
|
|
375
|
+
def get_type(type_name, context = null_context, use_visibility_profile = use_visibility_profile?)
|
|
370
376
|
if use_visibility_profile
|
|
371
377
|
profile = Visibility::Profile.from_context(context, self)
|
|
372
378
|
return profile.type(type_name)
|
|
@@ -617,7 +623,7 @@ module GraphQL
|
|
|
617
623
|
# @param use_visibility_profile Private, for migration to {Schema::Visibility}
|
|
618
624
|
# @return [Hash<String, Module>] All possible types, if no `type` is given.
|
|
619
625
|
# @return [Array<Module>] Possible types for `type`, if it's given.
|
|
620
|
-
def possible_types(type = nil, context =
|
|
626
|
+
def possible_types(type = nil, context = null_context, use_visibility_profile = use_visibility_profile?)
|
|
621
627
|
if use_visibility_profile
|
|
622
628
|
if type
|
|
623
629
|
return Visibility::Profile.from_context(context, self).possible_types(type)
|
|
@@ -701,7 +707,7 @@ module GraphQL
|
|
|
701
707
|
GraphQL::Schema::TypeExpression.build_type(context.query.types, ast_node)
|
|
702
708
|
end
|
|
703
709
|
|
|
704
|
-
def get_field(type_or_name, field_name, context =
|
|
710
|
+
def get_field(type_or_name, field_name, context = null_context, use_visibility_profile = use_visibility_profile?)
|
|
705
711
|
if use_visibility_profile
|
|
706
712
|
profile = Visibility::Profile.from_context(context, self)
|
|
707
713
|
parent_type = case type_or_name
|
|
@@ -738,7 +744,7 @@ module GraphQL
|
|
|
738
744
|
end
|
|
739
745
|
end
|
|
740
746
|
|
|
741
|
-
def get_fields(type, context =
|
|
747
|
+
def get_fields(type, context = null_context)
|
|
742
748
|
type.fields(context)
|
|
743
749
|
end
|
|
744
750
|
|
|
@@ -1228,6 +1234,7 @@ module GraphQL
|
|
|
1228
1234
|
vis = self.visibility
|
|
1229
1235
|
child_class.visibility = vis.dup_for(child_class)
|
|
1230
1236
|
end
|
|
1237
|
+
child_class.null_context = Query::NullContext.new(schema: child_class)
|
|
1231
1238
|
super
|
|
1232
1239
|
end
|
|
1233
1240
|
|
|
@@ -1329,10 +1336,11 @@ module GraphQL
|
|
|
1329
1336
|
def type_error(type_error, context)
|
|
1330
1337
|
case type_error
|
|
1331
1338
|
when GraphQL::InvalidNullError
|
|
1332
|
-
execution_error = GraphQL::ExecutionError.new(type_error.message,
|
|
1333
|
-
execution_error.path = context[:current_path]
|
|
1339
|
+
execution_error = GraphQL::ExecutionError.new(type_error.message, ast_nodes: type_error.ast_nodes)
|
|
1340
|
+
execution_error.path = type_error.path || context[:current_path]
|
|
1334
1341
|
|
|
1335
1342
|
context.errors << execution_error
|
|
1343
|
+
execution_error
|
|
1336
1344
|
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
|
|
1337
1345
|
raise type_error
|
|
1338
1346
|
when GraphQL::IntegerDecodingError
|
|
@@ -1354,6 +1362,24 @@ module GraphQL
|
|
|
1354
1362
|
lazy_methods.set(lazy_class, value_method)
|
|
1355
1363
|
end
|
|
1356
1364
|
|
|
1365
|
+
def uses_raw_value?
|
|
1366
|
+
!!@uses_raw_value
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
def uses_raw_value(new_val)
|
|
1370
|
+
@uses_raw_value = new_val
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
def resolves_lazies?
|
|
1374
|
+
lazy_method_count = 0
|
|
1375
|
+
lazy_methods.each do |k, v|
|
|
1376
|
+
if !v.nil?
|
|
1377
|
+
lazy_method_count += 1
|
|
1378
|
+
end
|
|
1379
|
+
end
|
|
1380
|
+
lazy_method_count > 2
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1357
1383
|
def instrument(instrument_step, instrumenter, options = {})
|
|
1358
1384
|
warn <<~WARN
|
|
1359
1385
|
Schema.instrument is deprecated, use `trace_with` instead: https://graphql-ruby.org/queries/tracing.html"
|
|
@@ -1708,7 +1734,7 @@ module GraphQL
|
|
|
1708
1734
|
# If you need to support previous, non-spec behavior which allowed selecting union fields
|
|
1709
1735
|
# but *not* selecting any fields on that union, set this to `true` to continue allowing that behavior.
|
|
1710
1736
|
#
|
|
1711
|
-
# If this is `true`, then {.
|
|
1737
|
+
# If this is `true`, then {.legacy_invalid_empty_selections_on_union_with_type} will be called with {Query} objects
|
|
1712
1738
|
# with that kind of selections. You must implement that method
|
|
1713
1739
|
# @param new_value [Boolean]
|
|
1714
1740
|
# @return [true, false, nil]
|
|
@@ -1724,6 +1750,22 @@ module GraphQL
|
|
|
1724
1750
|
end
|
|
1725
1751
|
end
|
|
1726
1752
|
|
|
1753
|
+
# This method is called during validation when a previously-allowed, but non-spec
|
|
1754
|
+
# query is encountered where a union field has no child selections on it.
|
|
1755
|
+
#
|
|
1756
|
+
# If `legacy_invalid_empty_selections_on_union_with_type` is overridden, this method will not be called.
|
|
1757
|
+
#
|
|
1758
|
+
# You should implement this method or `legacy_invalid_empty_selections_on_union_with_type`
|
|
1759
|
+
# to log the violation so that you can contact clients and notify them about changing their queries.
|
|
1760
|
+
# Then return a suitable value to tell GraphQL-Ruby how to continue.
|
|
1761
|
+
# @param query [GraphQL::Query]
|
|
1762
|
+
# @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
|
|
1763
|
+
# @return [String] A validation error to return for this query
|
|
1764
|
+
# @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
|
|
1765
|
+
def legacy_invalid_empty_selections_on_union(query)
|
|
1766
|
+
raise "Implement `def self.legacy_invalid_empty_selections_on_union_with_type(query, type)` or `def self.legacy_invalid_empty_selections_on_union(query)` to handle this scenario"
|
|
1767
|
+
end
|
|
1768
|
+
|
|
1727
1769
|
# This method is called during validation when a previously-allowed, but non-spec
|
|
1728
1770
|
# query is encountered where a union field has no child selections on it.
|
|
1729
1771
|
#
|
|
@@ -1731,11 +1773,12 @@ module GraphQL
|
|
|
1731
1773
|
# and notify them about changing their queries. Then return a suitable value to
|
|
1732
1774
|
# tell GraphQL-Ruby how to continue.
|
|
1733
1775
|
# @param query [GraphQL::Query]
|
|
1776
|
+
# @param type [Module] A GraphQL type definition
|
|
1734
1777
|
# @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
|
|
1735
1778
|
# @return [String] A validation error to return for this query
|
|
1736
1779
|
# @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
|
|
1737
|
-
def
|
|
1738
|
-
|
|
1780
|
+
def legacy_invalid_empty_selections_on_union_with_type(query, type)
|
|
1781
|
+
legacy_invalid_empty_selections_on_union(query)
|
|
1739
1782
|
end
|
|
1740
1783
|
|
|
1741
1784
|
# This setting controls how GraphQL-Ruby handles overlapping selections on scalar types when the types
|
|
@@ -49,7 +49,7 @@ module GraphQL
|
|
|
49
49
|
if !resolved_type.kind.fields?
|
|
50
50
|
case @schema.allow_legacy_invalid_empty_selections_on_union
|
|
51
51
|
when true
|
|
52
|
-
legacy_invalid_empty_selection_result = @schema.
|
|
52
|
+
legacy_invalid_empty_selection_result = @schema.legacy_invalid_empty_selections_on_union_with_type(@context.query, resolved_type)
|
|
53
53
|
case legacy_invalid_empty_selection_result
|
|
54
54
|
when :return_validation_error
|
|
55
55
|
# keep `return_validation_error = true`
|
|
@@ -61,7 +61,7 @@ module GraphQL
|
|
|
61
61
|
return_validation_error = false
|
|
62
62
|
legacy_invalid_empty_selection_result = nil
|
|
63
63
|
else
|
|
64
|
-
raise GraphQL::InvariantError, "Unexpected return value from
|
|
64
|
+
raise GraphQL::InvariantError, "Unexpected return value from legacy_invalid_empty_selections_on_union_with_type, must be `:return_validation_error`, String, or nil (got: #{legacy_invalid_empty_selection_result.inspect})"
|
|
65
65
|
end
|
|
66
66
|
when false
|
|
67
67
|
# pass -- error below
|
|
@@ -80,7 +80,7 @@ module GraphQL
|
|
|
80
80
|
|
|
81
81
|
# Normalize symbol-keyed args to strings, try camelizing them
|
|
82
82
|
# Should this accept a real context somehow?
|
|
83
|
-
normalized_args = normalize_arguments(normalized_event_name, field, args,
|
|
83
|
+
normalized_args = normalize_arguments(normalized_event_name, field, args, @schema.null_context)
|
|
84
84
|
|
|
85
85
|
event = Subscriptions::Event.new(
|
|
86
86
|
name: normalized_event_name,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module Testing
|
|
4
|
+
# A stub implementation of ActionCable.
|
|
5
|
+
# Any methods to support the mock backend have `mock` in the name.
|
|
6
|
+
#
|
|
7
|
+
# @example Configuring your schema to use MockActionCable in the test environment
|
|
8
|
+
# class MySchema < GraphQL::Schema
|
|
9
|
+
# # Use MockActionCable in test:
|
|
10
|
+
# use GraphQL::Subscriptions::ActionCableSubscriptions,
|
|
11
|
+
# action_cable: Rails.env.test? ? GraphQL::Testing::MockActionCable : ActionCable
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Clearing old data before each test
|
|
15
|
+
# setup do
|
|
16
|
+
# GraphQL::Testing::MockActionCable.clear_mocks
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Using MockActionCable in a test case
|
|
20
|
+
# # Create a channel to use in the test, pass it to GraphQL
|
|
21
|
+
# mock_channel = GraphQL::Testing::MockActionCable.get_mock_channel
|
|
22
|
+
# ActionCableTestSchema.execute("subscription { newsFlash { text } }", context: { channel: mock_channel })
|
|
23
|
+
#
|
|
24
|
+
# # Trigger a subscription update
|
|
25
|
+
# ActionCableTestSchema.subscriptions.trigger(:news_flash, {}, {text: "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"})
|
|
26
|
+
#
|
|
27
|
+
# # Check messages on the channel
|
|
28
|
+
# expected_msg = {
|
|
29
|
+
# result: {
|
|
30
|
+
# "data" => {
|
|
31
|
+
# "newsFlash" => {
|
|
32
|
+
# "text" => "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"
|
|
33
|
+
# }
|
|
34
|
+
# }
|
|
35
|
+
# },
|
|
36
|
+
# more: true,
|
|
37
|
+
# }
|
|
38
|
+
# assert_equal [expected_msg], mock_channel.mock_broadcasted_messages
|
|
39
|
+
#
|
|
40
|
+
class MockActionCable
|
|
41
|
+
class MockChannel
|
|
42
|
+
def initialize
|
|
43
|
+
@mock_broadcasted_messages = []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Array<Hash>] Payloads "sent" to this channel by GraphQL-Ruby
|
|
47
|
+
attr_reader :mock_broadcasted_messages
|
|
48
|
+
|
|
49
|
+
# Called by ActionCableSubscriptions. Implements a Rails API.
|
|
50
|
+
def stream_from(stream_name, coder: nil, &block)
|
|
51
|
+
# Rails uses `coder`, we don't
|
|
52
|
+
block ||= ->(msg) { @mock_broadcasted_messages << msg }
|
|
53
|
+
MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Used by mock code
|
|
58
|
+
# @api private
|
|
59
|
+
class MockStream
|
|
60
|
+
def initialize
|
|
61
|
+
@mock_channels = {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def add_mock_channel(channel, handler)
|
|
65
|
+
@mock_channels[channel] = handler
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def mock_broadcast(message)
|
|
69
|
+
@mock_channels.each do |channel, handler|
|
|
70
|
+
handler && handler.call(message)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
# Call this before each test run to make sure that MockActionCable's data is empty
|
|
77
|
+
def clear_mocks
|
|
78
|
+
@mock_streams = {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Implements Rails API
|
|
82
|
+
def server
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Implements Rails API
|
|
87
|
+
def broadcast(stream_name, message)
|
|
88
|
+
stream = @mock_streams[stream_name]
|
|
89
|
+
stream && stream.mock_broadcast(message)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Used by mock code
|
|
93
|
+
def mock_stream_for(stream_name)
|
|
94
|
+
@mock_streams[stream_name] ||= MockStream.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Use this as `context[:channel]` to simulate an ActionCable channel
|
|
98
|
+
#
|
|
99
|
+
# @return [GraphQL::Testing::MockActionCable::MockChannel]
|
|
100
|
+
def get_mock_channel
|
|
101
|
+
MockChannel.new
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [Array<String>] Streams that currently have subscribers
|
|
105
|
+
def mock_stream_names
|
|
106
|
+
@mock_streams.keys
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/graphql/testing.rb
CHANGED