graphql 1.11.8 → 1.12.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/generators/graphql/install_generator.rb +5 -5
- data/lib/generators/graphql/relay_generator.rb +63 -0
- data/lib/generators/graphql/templates/base_connection.erb +8 -0
- data/lib/generators/graphql/templates/base_edge.erb +8 -0
- data/lib/generators/graphql/templates/node_type.erb +9 -0
- data/lib/generators/graphql/templates/object.erb +1 -1
- data/lib/generators/graphql/templates/query_type.erb +1 -3
- data/lib/generators/graphql/templates/schema.erb +8 -35
- data/lib/graphql.rb +38 -4
- data/lib/graphql/analysis/analyze_query.rb +7 -0
- data/lib/graphql/analysis/ast.rb +11 -2
- data/lib/graphql/analysis/ast/visitor.rb +9 -1
- data/lib/graphql/backtrace.rb +28 -19
- data/lib/graphql/backtrace/legacy_tracer.rb +56 -0
- data/lib/graphql/backtrace/table.rb +22 -2
- data/lib/graphql/backtrace/tracer.rb +40 -9
- data/lib/graphql/backwards_compatibility.rb +1 -0
- data/lib/graphql/compatibility/execution_specification.rb +1 -0
- data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -0
- data/lib/graphql/compatibility/query_parser_specification.rb +2 -0
- data/lib/graphql/compatibility/schema_parser_specification.rb +2 -0
- data/lib/graphql/dataloader.rb +197 -0
- data/lib/graphql/dataloader/null_dataloader.rb +21 -0
- data/lib/graphql/dataloader/request.rb +24 -0
- data/lib/graphql/dataloader/request_all.rb +22 -0
- data/lib/graphql/dataloader/source.rb +93 -0
- data/lib/graphql/define/assign_global_id_field.rb +1 -1
- data/lib/graphql/define/instance_definable.rb +32 -2
- data/lib/graphql/define/type_definer.rb +5 -5
- data/lib/graphql/deprecated_dsl.rb +5 -0
- data/lib/graphql/enum_type.rb +2 -0
- data/lib/graphql/execution/errors.rb +4 -0
- data/lib/graphql/execution/execute.rb +7 -0
- data/lib/graphql/execution/interpreter.rb +10 -6
- data/lib/graphql/execution/interpreter/arguments.rb +51 -14
- data/lib/graphql/execution/interpreter/handles_raw_value.rb +0 -7
- data/lib/graphql/execution/interpreter/runtime.rb +210 -124
- data/lib/graphql/execution/multiplex.rb +20 -6
- data/lib/graphql/function.rb +4 -0
- data/lib/graphql/input_object_type.rb +2 -0
- data/lib/graphql/interface_type.rb +3 -1
- data/lib/graphql/language/document_from_schema_definition.rb +50 -23
- data/lib/graphql/object_type.rb +2 -0
- data/lib/graphql/pagination/connection.rb +5 -1
- data/lib/graphql/pagination/connections.rb +6 -16
- data/lib/graphql/query.rb +2 -0
- data/lib/graphql/query/context.rb +4 -0
- data/lib/graphql/query/serial_execution.rb +1 -0
- data/lib/graphql/relay/base_connection.rb +7 -0
- data/lib/graphql/relay/connection_instrumentation.rb +4 -4
- data/lib/graphql/relay/connection_type.rb +1 -1
- data/lib/graphql/relay/mutation.rb +1 -0
- data/lib/graphql/relay/node.rb +3 -0
- data/lib/graphql/relay/type_extensions.rb +2 -0
- data/lib/graphql/scalar_type.rb +2 -0
- data/lib/graphql/schema.rb +69 -32
- data/lib/graphql/schema/argument.rb +25 -7
- data/lib/graphql/schema/build_from_definition.rb +139 -51
- data/lib/graphql/schema/directive.rb +76 -0
- data/lib/graphql/schema/directive/flagged.rb +57 -0
- data/lib/graphql/schema/enum.rb +3 -0
- data/lib/graphql/schema/enum_value.rb +12 -6
- data/lib/graphql/schema/field.rb +28 -9
- data/lib/graphql/schema/field/connection_extension.rb +3 -2
- data/lib/graphql/schema/input_object.rb +33 -22
- data/lib/graphql/schema/interface.rb +1 -0
- data/lib/graphql/schema/member.rb +4 -0
- data/lib/graphql/schema/member/base_dsl_methods.rb +1 -0
- data/lib/graphql/schema/member/build_type.rb +3 -3
- data/lib/graphql/schema/member/has_arguments.rb +24 -6
- data/lib/graphql/schema/member/has_deprecation_reason.rb +25 -0
- data/lib/graphql/schema/member/has_directives.rb +98 -0
- data/lib/graphql/schema/member/has_validators.rb +31 -0
- data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
- data/lib/graphql/schema/object.rb +11 -0
- data/lib/graphql/schema/printer.rb +5 -4
- data/lib/graphql/schema/resolver.rb +7 -0
- data/lib/graphql/schema/resolver/has_payload_type.rb +2 -0
- data/lib/graphql/schema/subscription.rb +19 -1
- data/lib/graphql/schema/timeout_middleware.rb +2 -0
- data/lib/graphql/schema/validation.rb +2 -0
- data/lib/graphql/schema/validator.rb +163 -0
- data/lib/graphql/schema/validator/exclusion_validator.rb +31 -0
- data/lib/graphql/schema/validator/format_validator.rb +49 -0
- data/lib/graphql/schema/validator/inclusion_validator.rb +33 -0
- data/lib/graphql/schema/validator/length_validator.rb +57 -0
- data/lib/graphql/schema/validator/numericality_validator.rb +71 -0
- data/lib/graphql/schema/validator/required_validator.rb +68 -0
- data/lib/graphql/static_validation/validator.rb +2 -0
- data/lib/graphql/subscriptions.rb +17 -20
- data/lib/graphql/tracing.rb +2 -2
- data/lib/graphql/tracing/appoptics_tracing.rb +3 -1
- data/lib/graphql/tracing/platform_tracing.rb +3 -1
- data/lib/graphql/tracing/skylight_tracing.rb +1 -1
- data/lib/graphql/types/relay.rb +11 -3
- data/lib/graphql/types/relay/base_connection.rb +2 -92
- data/lib/graphql/types/relay/base_edge.rb +2 -35
- data/lib/graphql/types/relay/connection_behaviors.rb +123 -0
- data/lib/graphql/types/relay/default_relay.rb +27 -0
- data/lib/graphql/types/relay/edge_behaviors.rb +42 -0
- data/lib/graphql/types/relay/has_node_field.rb +41 -0
- data/lib/graphql/types/relay/has_nodes_field.rb +41 -0
- data/lib/graphql/types/relay/node.rb +2 -4
- data/lib/graphql/types/relay/node_behaviors.rb +15 -0
- data/lib/graphql/types/relay/node_field.rb +1 -19
- data/lib/graphql/types/relay/nodes_field.rb +1 -19
- data/lib/graphql/types/relay/page_info.rb +2 -14
- data/lib/graphql/types/relay/page_info_behaviors.rb +25 -0
- data/lib/graphql/union_type.rb +2 -0
- data/lib/graphql/upgrader/member.rb +1 -0
- data/lib/graphql/upgrader/schema.rb +1 -0
- data/lib/graphql/version.rb +1 -1
- metadata +34 -9
- data/lib/graphql/types/relay/base_field.rb +0 -22
- data/lib/graphql/types/relay/base_interface.rb +0 -29
- data/lib/graphql/types/relay/base_object.rb +0 -26
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
class Dataloader
|
5
|
+
class Source
|
6
|
+
# @api private
|
7
|
+
attr_reader :results
|
8
|
+
|
9
|
+
# Called by {Dataloader} to prepare the {Source}'s internal state
|
10
|
+
# @api private
|
11
|
+
def setup(dataloader)
|
12
|
+
@pending_keys = []
|
13
|
+
@results = {}
|
14
|
+
@dataloader = dataloader
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :dataloader
|
18
|
+
|
19
|
+
# @return [Dataloader::Request] a pending request for a value from `key`. Call `.load` on that object to wait for the result.
|
20
|
+
def request(key)
|
21
|
+
if !@results.key?(key)
|
22
|
+
@pending_keys << key
|
23
|
+
end
|
24
|
+
Dataloader::Request.new(self, key)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Dataloader::Request] a pending request for a values from `keys`. Call `.load` on that object to wait for the results.
|
28
|
+
def request_all(keys)
|
29
|
+
pending_keys = keys.select { |k| !@results.key?(k) }
|
30
|
+
@pending_keys.concat(pending_keys)
|
31
|
+
Dataloader::RequestAll.new(self, keys)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param key [Object] A loading key which will be passed to {#fetch} if it isn't already in the internal cache.
|
35
|
+
# @return [Object] The result from {#fetch} for `key`. If `key` hasn't been loaded yet, the Fiber will yield until it's loaded.
|
36
|
+
def load(key)
|
37
|
+
if @results.key?(key)
|
38
|
+
@results[key]
|
39
|
+
else
|
40
|
+
@pending_keys << key
|
41
|
+
sync
|
42
|
+
@results[key]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param keys [Array<Object>] Loading keys which will be passed to `#fetch` (or read from the internal cache).
|
47
|
+
# @return [Object] The result from {#fetch} for `keys`. If `keys` haven't been loaded yet, the Fiber will yield until they're loaded.
|
48
|
+
def load_all(keys)
|
49
|
+
if keys.any? { |k| !@results.key?(k) }
|
50
|
+
pending_keys = keys.select { |k| !@results.key?(k) }
|
51
|
+
@pending_keys.concat(pending_keys)
|
52
|
+
sync
|
53
|
+
end
|
54
|
+
|
55
|
+
keys.map { |k| @results[k] }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Subclasses must implement this method to return a value for each of `keys`
|
59
|
+
# @param keys [Array<Object>] keys passed to {#load}, {#load_all}, {#request}, or {#request_all}
|
60
|
+
# @return [Array<Object>] A loaded value for each of `keys`. The array must match one-for-one to the list of `keys`.
|
61
|
+
def fetch(keys)
|
62
|
+
# somehow retrieve these from the backend
|
63
|
+
raise "Implement `#{self.class}#fetch(#{keys.inspect}) to return a record for each of the keys"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Wait for a batch, if there's anything to batch.
|
67
|
+
# Then run the batch and update the cache.
|
68
|
+
# @return [void]
|
69
|
+
def sync
|
70
|
+
@dataloader.yield
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Boolean] True if this source has any pending requests for data.
|
74
|
+
def pending?
|
75
|
+
@pending_keys.any?
|
76
|
+
end
|
77
|
+
|
78
|
+
# Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
|
79
|
+
# @api private
|
80
|
+
# @return [void]
|
81
|
+
def run_pending_keys
|
82
|
+
return if @pending_keys.empty?
|
83
|
+
fetch_keys = @pending_keys.uniq
|
84
|
+
@pending_keys = []
|
85
|
+
results = fetch(fetch_keys)
|
86
|
+
fetch_keys.each_with_index do |key, idx|
|
87
|
+
@results[key] = results[idx]
|
88
|
+
end
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -4,7 +4,7 @@ module GraphQL
|
|
4
4
|
module AssignGlobalIdField
|
5
5
|
def self.call(type_defn, field_name, **field_kwargs)
|
6
6
|
resolve = GraphQL::Relay::GlobalIdResolve.new(type: type_defn)
|
7
|
-
GraphQL::Define::AssignObjectField.call(type_defn, field_name, **field_kwargs, type: GraphQL::
|
7
|
+
GraphQL::Define::AssignObjectField.call(type_defn, field_name, **field_kwargs, type: GraphQL::DEPRECATED_ID_TYPE.to_non_null_type, resolve: resolve)
|
8
8
|
end
|
9
9
|
end
|
10
10
|
end
|
@@ -3,6 +3,23 @@ module GraphQL
|
|
3
3
|
module Define
|
4
4
|
# @api deprecated
|
5
5
|
module InstanceDefinable
|
6
|
+
module DeprecatedDefine
|
7
|
+
def define(**kwargs, &block)
|
8
|
+
deprecated_caller = caller(1, 1).first
|
9
|
+
if deprecated_caller.include?("lib/graphql")
|
10
|
+
deprecated_caller = caller(2, 10).find { |c| !c.include?("lib/graphql") }
|
11
|
+
end
|
12
|
+
|
13
|
+
if deprecated_caller
|
14
|
+
warn <<-ERR
|
15
|
+
#{self}.define will be removed in GraphQL-Ruby 2.0; use a class-based definition instead. See https://graphql-ruby.org/schema/class_based_api.html.
|
16
|
+
-> called from #{deprecated_caller}
|
17
|
+
ERR
|
18
|
+
end
|
19
|
+
deprecated_define(**kwargs, &block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
6
23
|
def self.included(base)
|
7
24
|
base.extend(ClassMethods)
|
8
25
|
base.ensure_defined(:metadata)
|
@@ -14,7 +31,7 @@ module GraphQL
|
|
14
31
|
end
|
15
32
|
|
16
33
|
# @api deprecated
|
17
|
-
def
|
34
|
+
def deprecated_define(**kwargs, &block)
|
18
35
|
# make sure the previous definition_proc was executed:
|
19
36
|
ensure_defined
|
20
37
|
stash_dependent_methods
|
@@ -22,11 +39,16 @@ module GraphQL
|
|
22
39
|
nil
|
23
40
|
end
|
24
41
|
|
42
|
+
# @api deprecated
|
43
|
+
def define(**kwargs, &block)
|
44
|
+
deprecated_define(**kwargs, &block)
|
45
|
+
end
|
46
|
+
|
25
47
|
# @api deprecated
|
26
48
|
def redefine(**kwargs, &block)
|
27
49
|
ensure_defined
|
28
50
|
new_inst = self.dup
|
29
|
-
new_inst.
|
51
|
+
new_inst.deprecated_define(**kwargs, &block)
|
30
52
|
new_inst
|
31
53
|
end
|
32
54
|
|
@@ -125,8 +147,16 @@ module GraphQL
|
|
125
147
|
module ClassMethods
|
126
148
|
# Create a new instance
|
127
149
|
# and prepare a definition using its {.definitions}.
|
150
|
+
# @api deprecated
|
128
151
|
# @param kwargs [Hash] Key-value pairs corresponding to defininitions from `accepts_definitions`
|
129
152
|
# @param block [Proc] Block which calls helper methods from `accepts_definitions`
|
153
|
+
def deprecated_define(**kwargs, &block)
|
154
|
+
instance = self.new
|
155
|
+
instance.deprecated_define(**kwargs, &block)
|
156
|
+
instance
|
157
|
+
end
|
158
|
+
|
159
|
+
# @api deprecated
|
130
160
|
def define(**kwargs, &block)
|
131
161
|
instance = self.new
|
132
162
|
instance.define(**kwargs, &block)
|
@@ -7,11 +7,11 @@ module GraphQL
|
|
7
7
|
class TypeDefiner
|
8
8
|
include Singleton
|
9
9
|
# rubocop:disable Naming/MethodName
|
10
|
-
def Int; GraphQL::
|
11
|
-
def String; GraphQL::
|
12
|
-
def Float; GraphQL::
|
13
|
-
def Boolean; GraphQL::
|
14
|
-
def ID; GraphQL::
|
10
|
+
def Int; GraphQL::DEPRECATED_INT_TYPE; end
|
11
|
+
def String; GraphQL::DEPRECATED_STRING_TYPE; end
|
12
|
+
def Float; GraphQL::DEPRECATED_FLOAT_TYPE; end
|
13
|
+
def Boolean; GraphQL::DEPRECATED_BOOLEAN_TYPE; end
|
14
|
+
def ID; GraphQL::DEPRECATED_ID_TYPE; end
|
15
15
|
# rubocop:enable Naming/MethodName
|
16
16
|
|
17
17
|
# Make a {ListType} which wraps the input type
|
@@ -23,12 +23,17 @@ module GraphQL
|
|
23
23
|
]
|
24
24
|
|
25
25
|
def self.activate
|
26
|
+
deprecated_caller = caller(1, 1).first
|
27
|
+
warn "DeprecatedDSL will be removed from GraphQL-Ruby 2.0, use `.to_non_null_type` instead of `!` and remove `.activate` from #{deprecated_caller}"
|
26
28
|
TYPE_CLASSES.each { |c| c.extend(Methods) }
|
27
29
|
GraphQL::Schema::List.include(Methods)
|
28
30
|
GraphQL::Schema::NonNull.include(Methods)
|
29
31
|
end
|
32
|
+
|
30
33
|
module Methods
|
31
34
|
def !
|
35
|
+
deprecated_caller = caller(1, 1).first
|
36
|
+
warn "DeprecatedDSL will be removed from GraphQL-Ruby 2.0, use `.to_non_null_type` instead of `!` at #{deprecated_caller}"
|
32
37
|
to_non_null_type
|
33
38
|
end
|
34
39
|
end
|
data/lib/graphql/enum_type.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
module GraphQL
|
3
3
|
# @api deprecated
|
4
4
|
class EnumType < GraphQL::BaseType
|
5
|
+
extend Define::InstanceDefinable::DeprecatedDefine
|
6
|
+
|
5
7
|
accepts_definitions :values, value: GraphQL::Define::AssignEnumValue
|
6
8
|
ensure_defined(:values, :validate_non_null_input, :coerce_non_null_input, :coerce_result)
|
7
9
|
attr_accessor :ast_node
|
@@ -18,6 +18,10 @@ module GraphQL
|
|
18
18
|
#
|
19
19
|
class Errors
|
20
20
|
def self.use(schema)
|
21
|
+
if schema.plugins.any? { |(plugin, kwargs)| plugin == self }
|
22
|
+
definition_line = caller(2, 1).first
|
23
|
+
warn("GraphQL::Execution::Errors is now installed by default, remove `use GraphQL::Execution::Errors` from #{definition_line}")
|
24
|
+
end
|
21
25
|
schema.error_handler = self.new(schema)
|
22
26
|
end
|
23
27
|
|
@@ -18,7 +18,14 @@ module GraphQL
|
|
18
18
|
# @api private
|
19
19
|
PROPAGATE_NULL = PropagateNull.new
|
20
20
|
|
21
|
+
def self.use(schema_class)
|
22
|
+
schema_class.query_execution_strategy(self)
|
23
|
+
schema_class.mutation_execution_strategy(self)
|
24
|
+
schema_class.subscription_execution_strategy(self)
|
25
|
+
end
|
26
|
+
|
21
27
|
def execute(ast_operation, root_type, query)
|
28
|
+
warn "#{self.class} will be removed in GraphQL-Ruby 2.0, please upgrade to the Interpreter: https://graphql-ruby.org/queries/interpreter.html"
|
22
29
|
result = resolve_root_selection(query)
|
23
30
|
lazy_resolve_root_selection(result, **{query: query})
|
24
31
|
GraphQL::Execution::Flatten.call(query.context)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "fiber"
|
2
3
|
require "graphql/execution/interpreter/argument_value"
|
3
4
|
require "graphql/execution/interpreter/arguments"
|
4
5
|
require "graphql/execution/interpreter/arguments_cache"
|
@@ -22,12 +23,15 @@ module GraphQL
|
|
22
23
|
end
|
23
24
|
|
24
25
|
def self.use(schema_class)
|
25
|
-
schema_class.interpreter
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
if schema_class.interpreter?
|
27
|
+
definition_line = caller(2, 1).first
|
28
|
+
warn("GraphQL::Execution::Interpreter is now the default; remove `use GraphQL::Execution::Interpreter` from the schema definition (#{definition_line})")
|
29
|
+
else
|
30
|
+
schema_class.query_execution_strategy(self)
|
31
|
+
schema_class.mutation_execution_strategy(self)
|
32
|
+
schema_class.subscription_execution_strategy(self)
|
33
|
+
schema_class.add_subscription_extension_if_necessary
|
34
|
+
end
|
31
35
|
end
|
32
36
|
|
33
37
|
def self.begin_multiplex(multiplex)
|
@@ -5,6 +5,9 @@ module GraphQL
|
|
5
5
|
class Interpreter
|
6
6
|
# A wrapper for argument hashes in GraphQL queries.
|
7
7
|
#
|
8
|
+
# This object is immutable so that the runtime code can be sure that
|
9
|
+
# modifications don't leak from one use to another
|
10
|
+
#
|
8
11
|
# @see GraphQL::Query#arguments_for to get access to these objects.
|
9
12
|
class Arguments
|
10
13
|
extend Forwardable
|
@@ -14,26 +17,43 @@ module GraphQL
|
|
14
17
|
# This hash is the one used at runtime.
|
15
18
|
#
|
16
19
|
# @return [Hash<Symbol, Object>]
|
17
|
-
|
18
|
-
@keyword_arguments ||= begin
|
19
|
-
kwargs = {}
|
20
|
-
argument_values.each do |name, arg_val|
|
21
|
-
kwargs[name] = arg_val.value
|
22
|
-
end
|
23
|
-
kwargs
|
24
|
-
end
|
25
|
-
end
|
20
|
+
attr_reader :keyword_arguments
|
26
21
|
|
27
22
|
# @param argument_values [nil, Hash{Symbol => ArgumentValue}]
|
28
|
-
|
29
|
-
|
23
|
+
# @param keyword_arguments [nil, Hash{Symbol => Object}]
|
24
|
+
def initialize(keyword_arguments: nil, argument_values:)
|
30
25
|
@empty = argument_values.nil? || argument_values.empty?
|
26
|
+
# This is only present when `extras` have been merged in:
|
27
|
+
if keyword_arguments
|
28
|
+
# This is a little crazy. We expect the `:argument_details` extra to _include extras_,
|
29
|
+
# but the object isn't created until _after_ extras are put together.
|
30
|
+
# So, we have to use a special flag here to say, "at the last minute, add yourself to the keyword args."
|
31
|
+
#
|
32
|
+
# Otherwise:
|
33
|
+
# - We can't access the final Arguments instance _while_ we're preparing extras
|
34
|
+
# - After we _can_ access it, it's frozen, so we can't add anything.
|
35
|
+
#
|
36
|
+
# So, this flag gives us a chance to sneak it in before freezing, _and_ while we have access
|
37
|
+
# to the new Arguments instance itself.
|
38
|
+
if keyword_arguments[:argument_details] == :__arguments_add_self
|
39
|
+
keyword_arguments[:argument_details] = self
|
40
|
+
end
|
41
|
+
@keyword_arguments = keyword_arguments.freeze
|
42
|
+
elsif !@empty
|
43
|
+
@keyword_arguments = {}
|
44
|
+
argument_values.each do |name, arg_val|
|
45
|
+
@keyword_arguments[name] = arg_val.value
|
46
|
+
end
|
47
|
+
@keyword_arguments.freeze
|
48
|
+
else
|
49
|
+
@keyword_arguments = NO_ARGS
|
50
|
+
end
|
51
|
+
@argument_values = argument_values ? argument_values.freeze : NO_ARGS
|
52
|
+
freeze
|
31
53
|
end
|
32
54
|
|
33
55
|
# @return [Hash{Symbol => ArgumentValue}]
|
34
|
-
|
35
|
-
@argument_values ||= {}
|
36
|
-
end
|
56
|
+
attr_reader :argument_values
|
37
57
|
|
38
58
|
def empty?
|
39
59
|
@empty
|
@@ -45,6 +65,23 @@ module GraphQL
|
|
45
65
|
def inspect
|
46
66
|
"#<#{self.class} @keyword_arguments=#{keyword_arguments.inspect}>"
|
47
67
|
end
|
68
|
+
|
69
|
+
# Create a new arguments instance which includes these extras.
|
70
|
+
#
|
71
|
+
# This is called by the runtime to implement field `extras: [...]`
|
72
|
+
#
|
73
|
+
# @param extra_args [Hash<Symbol => Object>]
|
74
|
+
# @return [Interpreter::Arguments]
|
75
|
+
# @api private
|
76
|
+
def merge_extras(extra_args)
|
77
|
+
self.class.new(
|
78
|
+
argument_values: argument_values,
|
79
|
+
keyword_arguments: keyword_arguments.merge(extra_args)
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
NO_ARGS = {}.freeze
|
84
|
+
EMPTY = self.new(argument_values: nil, keyword_arguments: NO_ARGS).freeze
|
48
85
|
end
|
49
86
|
end
|
50
87
|
end
|
@@ -19,8 +19,10 @@ module GraphQL
|
|
19
19
|
|
20
20
|
def initialize(query:, response:)
|
21
21
|
@query = query
|
22
|
+
@dataloader = query.multiplex.dataloader
|
22
23
|
@schema = query.schema
|
23
24
|
@context = query.context
|
25
|
+
@multiplex_context = query.multiplex.context
|
24
26
|
@interpreter_context = @context.namespace(:interpreter)
|
25
27
|
@response = response
|
26
28
|
@dead_paths = {}
|
@@ -54,7 +56,17 @@ module GraphQL
|
|
54
56
|
# Root .authorized? returned false.
|
55
57
|
write_in_response(path, nil)
|
56
58
|
else
|
57
|
-
|
59
|
+
# Prepare this runtime state to be encapsulated in a Fiber
|
60
|
+
@progress_path = path
|
61
|
+
@progress_scoped_context = context.scoped_context
|
62
|
+
@progress_object = object_proxy
|
63
|
+
@progress_object_type = root_type
|
64
|
+
@progress_index = nil
|
65
|
+
@progress_is_eager_selection = root_op_type == "mutation"
|
66
|
+
@progress_selections = gather_selections(object_proxy, root_type, root_operation.selections)
|
67
|
+
|
68
|
+
# Make the first fiber which will begin execution
|
69
|
+
enqueue_selections_fiber
|
58
70
|
end
|
59
71
|
delete_interpreter_context(:current_path)
|
60
72
|
delete_interpreter_context(:current_field)
|
@@ -63,7 +75,33 @@ module GraphQL
|
|
63
75
|
nil
|
64
76
|
end
|
65
77
|
|
66
|
-
|
78
|
+
# Use `@dataloader` to enqueue a fiber that will pick up from the current point.
|
79
|
+
# @return [void]
|
80
|
+
def enqueue_selections_fiber
|
81
|
+
# Read these into local variables so that later assignments don't affect the block below.
|
82
|
+
path = @progress_path
|
83
|
+
scoped_context = @progress_scoped_context
|
84
|
+
owner_object = @progress_object
|
85
|
+
owner_type = @progress_object_type
|
86
|
+
idx = @progress_index
|
87
|
+
is_eager_selection = @progress_is_eager_selection
|
88
|
+
gathered_selections = @progress_selections
|
89
|
+
|
90
|
+
@dataloader.enqueue {
|
91
|
+
evaluate_selections(
|
92
|
+
path,
|
93
|
+
scoped_context,
|
94
|
+
owner_object,
|
95
|
+
owner_type,
|
96
|
+
is_eager_selection: is_eager_selection,
|
97
|
+
after: idx,
|
98
|
+
gathered_selections: gathered_selections,
|
99
|
+
)
|
100
|
+
}
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
|
67
105
|
selections.each do |node|
|
68
106
|
# Skip gathering this if the directive says so
|
69
107
|
if !directives_include?(node, owner_object, owner_type)
|
@@ -115,145 +153,184 @@ module GraphQL
|
|
115
153
|
raise "Invariant: unexpected selection class: #{node.class}"
|
116
154
|
end
|
117
155
|
end
|
156
|
+
selections_by_name
|
118
157
|
end
|
119
158
|
|
120
159
|
NO_ARGS = {}.freeze
|
121
160
|
|
122
|
-
|
161
|
+
# @return [void]
|
162
|
+
def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection:, gathered_selections:, after:)
|
123
163
|
set_all_interpreter_context(owner_object, nil, nil, path)
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
164
|
+
|
165
|
+
@progress_path = path
|
166
|
+
@progress_scoped_context = scoped_context
|
167
|
+
@progress_object = owner_object
|
168
|
+
@progress_object_type = owner_type
|
169
|
+
@progress_index = nil
|
170
|
+
@progress_is_eager_selection = is_eager_selection
|
171
|
+
@progress_selections = gathered_selections
|
172
|
+
|
173
|
+
# Track `idx` manually to avoid an allocation on this hot path
|
174
|
+
idx = 0
|
175
|
+
gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
|
176
|
+
prev_idx = idx
|
177
|
+
idx += 1
|
178
|
+
# TODO: this is how a `progress` resumes where this left off.
|
179
|
+
# Is there a better way to seek in the hash?
|
180
|
+
# I think we could also use the array of keys; it supports seeking just fine.
|
181
|
+
if after && prev_idx <= after
|
182
|
+
next
|
136
183
|
end
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
dynamic_field
|
147
|
-
else
|
148
|
-
raise "Invariant: no field for #{owner_type}.#{field_name}"
|
149
|
-
end
|
184
|
+
@progress_index = prev_idx
|
185
|
+
# This is how the current runtime gives itself to `dataloader`
|
186
|
+
# so that the dataloader can enqueue another fiber to resume if needed.
|
187
|
+
@dataloader.current_runtime = self
|
188
|
+
evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection)
|
189
|
+
# The dataloader knows if ^^ that selection halted and later selections were executed in another fiber.
|
190
|
+
# If that's the case, then don't continue execution here.
|
191
|
+
if @dataloader.yielded?
|
192
|
+
break
|
150
193
|
end
|
151
|
-
|
194
|
+
end
|
195
|
+
nil
|
196
|
+
end
|
152
197
|
|
153
|
-
|
154
|
-
|
155
|
-
|
198
|
+
# @return [void]
|
199
|
+
def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
|
200
|
+
# As a performance optimization, the hash key will be a `Node` if
|
201
|
+
# there's only one selection of the field. But if there are multiple
|
202
|
+
# selections of the field, it will be an Array of nodes
|
203
|
+
if field_ast_nodes_or_ast_node.is_a?(Array)
|
204
|
+
field_ast_nodes = field_ast_nodes_or_ast_node
|
205
|
+
ast_node = field_ast_nodes.first
|
206
|
+
else
|
207
|
+
field_ast_nodes = nil
|
208
|
+
ast_node = field_ast_nodes_or_ast_node
|
209
|
+
end
|
210
|
+
field_name = ast_node.name
|
211
|
+
field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
|
212
|
+
is_introspection = false
|
213
|
+
if field_defn.nil?
|
214
|
+
field_defn = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
|
215
|
+
is_introspection = true
|
216
|
+
entry_point_field
|
217
|
+
elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name))
|
218
|
+
is_introspection = true
|
219
|
+
dynamic_field
|
220
|
+
else
|
221
|
+
raise "Invariant: no field for #{owner_type}.#{field_name}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
return_type = field_defn.type
|
156
225
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
set_type_at_path(next_path, return_type)
|
161
|
-
# Set this before calling `run_with_directives`, so that the directive can have the latest path
|
162
|
-
set_all_interpreter_context(nil, field_defn, nil, next_path)
|
226
|
+
next_path = path.dup
|
227
|
+
next_path << result_name
|
228
|
+
next_path.freeze
|
163
229
|
|
164
|
-
|
165
|
-
|
230
|
+
# This seems janky, but we need to know
|
231
|
+
# the field's return type at this path in order
|
232
|
+
# to propagate `null`
|
233
|
+
set_type_at_path(next_path, return_type)
|
234
|
+
# Set this before calling `run_with_directives`, so that the directive can have the latest path
|
235
|
+
set_all_interpreter_context(nil, field_defn, nil, next_path)
|
166
236
|
|
167
|
-
|
168
|
-
|
169
|
-
end
|
237
|
+
context.scoped_context = scoped_context
|
238
|
+
object = owner_object
|
170
239
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
240
|
+
if is_introspection
|
241
|
+
object = authorized_new(field_defn.owner, object, context, next_path)
|
242
|
+
end
|
243
|
+
|
244
|
+
begin
|
245
|
+
kwarg_arguments = arguments(object, field_defn, ast_node)
|
246
|
+
rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => e
|
247
|
+
continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
|
248
|
+
return
|
249
|
+
end
|
250
|
+
|
251
|
+
after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
|
252
|
+
if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
|
253
|
+
continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
|
175
254
|
next
|
176
255
|
end
|
177
256
|
|
178
|
-
|
179
|
-
case
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
kwarg_arguments[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
|
197
|
-
when :path
|
198
|
-
kwarg_arguments[:path] = next_path
|
199
|
-
when :lookahead
|
200
|
-
if !field_ast_nodes
|
201
|
-
field_ast_nodes = [ast_node]
|
202
|
-
end
|
203
|
-
kwarg_arguments[:lookahead] = Execution::Lookahead.new(
|
204
|
-
query: query,
|
205
|
-
ast_nodes: field_ast_nodes,
|
206
|
-
field: field_defn,
|
207
|
-
)
|
208
|
-
when :argument_details
|
209
|
-
kwarg_arguments[:argument_details] = resolved_arguments
|
210
|
-
else
|
211
|
-
kwarg_arguments[extra] = field_defn.fetch_extra(extra, context)
|
257
|
+
kwarg_arguments = if resolved_arguments.empty? && field_defn.extras.empty?
|
258
|
+
# We can avoid allocating the `{ Symbol => Object }` hash in this case
|
259
|
+
NO_ARGS
|
260
|
+
else
|
261
|
+
# Bundle up the extras, then make a new arguments instance
|
262
|
+
# that includes the extras, too.
|
263
|
+
extra_args = {}
|
264
|
+
field_defn.extras.each do |extra|
|
265
|
+
case extra
|
266
|
+
when :ast_node
|
267
|
+
extra_args[:ast_node] = ast_node
|
268
|
+
when :execution_errors
|
269
|
+
extra_args[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
|
270
|
+
when :path
|
271
|
+
extra_args[:path] = next_path
|
272
|
+
when :lookahead
|
273
|
+
if !field_ast_nodes
|
274
|
+
field_ast_nodes = [ast_node]
|
212
275
|
end
|
276
|
+
|
277
|
+
extra_args[:lookahead] = Execution::Lookahead.new(
|
278
|
+
query: query,
|
279
|
+
ast_nodes: field_ast_nodes,
|
280
|
+
field: field_defn,
|
281
|
+
)
|
282
|
+
when :argument_details
|
283
|
+
# Use this flag to tell Interpreter::Arguments to add itself
|
284
|
+
# to the keyword args hash _before_ freezing everything.
|
285
|
+
extra_args[:argument_details] = :__arguments_add_self
|
286
|
+
else
|
287
|
+
extra_args[extra] = field_defn.fetch_extra(extra, context)
|
213
288
|
end
|
214
289
|
end
|
290
|
+
resolved_arguments = resolved_arguments.merge_extras(extra_args)
|
291
|
+
resolved_arguments.keyword_arguments
|
292
|
+
end
|
215
293
|
|
216
|
-
|
294
|
+
set_all_interpreter_context(nil, nil, kwarg_arguments, nil)
|
217
295
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
296
|
+
# Optimize for the case that field is selected only once
|
297
|
+
if field_ast_nodes.nil? || field_ast_nodes.size == 1
|
298
|
+
next_selections = ast_node.selections
|
299
|
+
else
|
300
|
+
next_selections = []
|
301
|
+
field_ast_nodes.each { |f| next_selections.concat(f.selections) }
|
302
|
+
end
|
225
303
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
end
|
304
|
+
field_result = resolve_with_directives(object, ast_node) do
|
305
|
+
# Actually call the field resolver and capture the result
|
306
|
+
app_result = begin
|
307
|
+
query.with_error_handling do
|
308
|
+
query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments}) do
|
309
|
+
field_defn.resolve(object, kwarg_arguments, context)
|
233
310
|
end
|
234
|
-
rescue GraphQL::ExecutionError => err
|
235
|
-
err
|
236
311
|
end
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
312
|
+
rescue GraphQL::ExecutionError => err
|
313
|
+
err
|
314
|
+
end
|
315
|
+
after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
|
316
|
+
continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
|
317
|
+
if RawValue === continue_value
|
318
|
+
# Write raw value directly to the response without resolving nested objects
|
319
|
+
write_in_response(next_path, continue_value.resolve)
|
320
|
+
elsif HALT != continue_value
|
321
|
+
continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
|
245
322
|
end
|
246
323
|
end
|
324
|
+
end
|
247
325
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
else
|
254
|
-
field_result
|
255
|
-
end
|
326
|
+
# If this field is a root mutation field, immediately resolve
|
327
|
+
# all of its child fields before moving on to the next root mutation field.
|
328
|
+
# (Subselections of this mutation will still be resolved level-by-level.)
|
329
|
+
if is_eager_field
|
330
|
+
Interpreter::Resolve.resolve_all([field_result])
|
256
331
|
end
|
332
|
+
|
333
|
+
nil
|
257
334
|
end
|
258
335
|
end
|
259
336
|
|
@@ -314,7 +391,7 @@ module GraphQL
|
|
314
391
|
resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
|
315
392
|
resolved_value ||= value
|
316
393
|
|
317
|
-
after_lazy(resolved_type_or_lazy, owner: current_type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |resolved_type|
|
394
|
+
after_lazy(resolved_type_or_lazy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |resolved_type|
|
318
395
|
possible_types = query.possible_types(current_type)
|
319
396
|
|
320
397
|
if !possible_types.include?(resolved_type)
|
@@ -334,12 +411,13 @@ module GraphQL
|
|
334
411
|
rescue GraphQL::ExecutionError => err
|
335
412
|
err
|
336
413
|
end
|
337
|
-
after_lazy(object_proxy, owner: current_type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |inner_object|
|
414
|
+
after_lazy(object_proxy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |inner_object|
|
338
415
|
continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
|
339
416
|
if HALT != continue_value
|
340
417
|
response_hash = {}
|
341
418
|
write_in_response(path, response_hash)
|
342
|
-
|
419
|
+
gathered_selections = gather_selections(continue_value, current_type, next_selections)
|
420
|
+
evaluate_selections(path, context.scoped_context, continue_value, current_type, is_eager_selection: false, gathered_selections: gathered_selections, after: nil)
|
343
421
|
response_hash
|
344
422
|
end
|
345
423
|
end
|
@@ -357,7 +435,7 @@ module GraphQL
|
|
357
435
|
idx += 1
|
358
436
|
set_type_at_path(next_path, inner_type)
|
359
437
|
# This will update `response_list` with the lazy
|
360
|
-
after_lazy(inner_value, owner: inner_type, path: next_path, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
|
438
|
+
after_lazy(inner_value, owner: inner_type, path: next_path, ast_node: ast_node, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
|
361
439
|
continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
|
362
440
|
if HALT != continue_value
|
363
441
|
continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
|
@@ -440,7 +518,7 @@ module GraphQL
|
|
440
518
|
# @param eager [Boolean] Set to `true` for mutation root fields only
|
441
519
|
# @param trace [Boolean] If `false`, don't wrap this with field tracing
|
442
520
|
# @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
|
443
|
-
def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, eager: false, trace: true, &block)
|
521
|
+
def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, eager: false, trace: true, &block)
|
444
522
|
set_all_interpreter_context(owner_object, field, arguments, path)
|
445
523
|
if schema.lazy?(lazy_obj)
|
446
524
|
lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
|
@@ -451,7 +529,7 @@ module GraphQL
|
|
451
529
|
inner_obj = begin
|
452
530
|
query.with_error_handling do
|
453
531
|
if trace
|
454
|
-
query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments}) do
|
532
|
+
query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
|
455
533
|
schema.sync_lazy(lazy_obj)
|
456
534
|
end
|
457
535
|
else
|
@@ -461,7 +539,7 @@ module GraphQL
|
|
461
539
|
rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
|
462
540
|
err
|
463
541
|
end
|
464
|
-
after_lazy(inner_obj, owner: owner, field: field, path: path, scoped_context: context.scoped_context, owner_object: owner_object, arguments: arguments, eager: eager, trace: trace, &block)
|
542
|
+
after_lazy(inner_obj, owner: owner, field: field, path: path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: owner_object, arguments: arguments, eager: eager, trace: trace, &block)
|
465
543
|
end
|
466
544
|
|
467
545
|
if eager
|
@@ -476,9 +554,7 @@ module GraphQL
|
|
476
554
|
end
|
477
555
|
|
478
556
|
def arguments(graphql_object, arg_owner, ast_node)
|
479
|
-
|
480
|
-
if arg_owner.arguments_statically_coercible? &&
|
481
|
-
(!arg_owner.is_a?(GraphQL::Schema::Field) || (arg_owner.extras.empty? && arg_owner.extensions.empty?))
|
557
|
+
if arg_owner.arguments_statically_coercible?
|
482
558
|
query.arguments_for(ast_node, arg_owner)
|
483
559
|
else
|
484
560
|
# The arguments must be prepared in the context of the given object
|
@@ -519,6 +595,16 @@ module GraphQL
|
|
519
595
|
end
|
520
596
|
end
|
521
597
|
|
598
|
+
def value_at(path)
|
599
|
+
i = 0
|
600
|
+
value = @response.final_value
|
601
|
+
while value && (part = path[i])
|
602
|
+
value = value[part]
|
603
|
+
i += 1
|
604
|
+
end
|
605
|
+
value
|
606
|
+
end
|
607
|
+
|
522
608
|
# To propagate nulls, we have to know what the field type was
|
523
609
|
# at previous parts of the response.
|
524
610
|
# This hash matches the response
|