graphql 1.5.15 → 1.6.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/graphql.rb +4 -19
- data/lib/graphql/analysis/analyze_query.rb +27 -2
- data/lib/graphql/analysis/query_complexity.rb +10 -11
- data/lib/graphql/argument.rb +7 -6
- data/lib/graphql/backwards_compatibility.rb +47 -0
- data/lib/graphql/compatibility/execution_specification.rb +14 -0
- data/lib/graphql/compatibility/execution_specification/specification_schema.rb +6 -1
- data/lib/graphql/compatibility/lazy_execution_specification.rb +19 -0
- data/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb +15 -6
- data/lib/graphql/directive.rb +1 -6
- data/lib/graphql/execution.rb +1 -0
- data/lib/graphql/execution/execute.rb +174 -160
- data/lib/graphql/execution/field_result.rb +5 -1
- data/lib/graphql/execution/lazy.rb +2 -2
- data/lib/graphql/execution/lazy/resolve.rb +8 -11
- data/lib/graphql/execution/multiplex.rb +134 -0
- data/lib/graphql/execution/selection_result.rb +5 -0
- data/lib/graphql/field.rb +1 -8
- data/lib/graphql/filter.rb +53 -0
- data/lib/graphql/internal_representation/node.rb +11 -6
- data/lib/graphql/internal_representation/rewrite.rb +3 -3
- data/lib/graphql/query.rb +160 -78
- data/lib/graphql/query/arguments.rb +14 -25
- data/lib/graphql/query/arguments_cache.rb +6 -13
- data/lib/graphql/query/context.rb +28 -10
- data/lib/graphql/query/executor.rb +1 -0
- data/lib/graphql/query/literal_input.rb +10 -4
- data/lib/graphql/query/null_context.rb +1 -1
- data/lib/graphql/query/serial_execution/field_resolution.rb +5 -1
- data/lib/graphql/query/validation_pipeline.rb +12 -7
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/rake_task.rb +140 -0
- data/lib/graphql/relay/array_connection.rb +29 -48
- data/lib/graphql/relay/base_connection.rb +9 -7
- data/lib/graphql/relay/mutation.rb +0 -11
- data/lib/graphql/relay/mutation/instrumentation.rb +2 -2
- data/lib/graphql/relay/mutation/resolve.rb +7 -10
- data/lib/graphql/relay/relation_connection.rb +98 -61
- data/lib/graphql/scalar_type.rb +1 -15
- data/lib/graphql/schema.rb +90 -25
- data/lib/graphql/schema/build_from_definition.rb +22 -23
- data/lib/graphql/schema/build_from_definition/resolve_map.rb +70 -0
- data/lib/graphql/schema/build_from_definition/resolve_map/default_resolve.rb +47 -0
- data/lib/graphql/schema/middleware_chain.rb +1 -1
- data/lib/graphql/schema/printer.rb +2 -1
- data/lib/graphql/schema/timeout_middleware.rb +6 -6
- data/lib/graphql/schema/type_map.rb +1 -1
- data/lib/graphql/schema/warden.rb +5 -9
- data/lib/graphql/static_validation/definition_dependencies.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- data/spec/graphql/analysis/analyze_query_spec.rb +2 -2
- data/spec/graphql/analysis/max_query_complexity_spec.rb +28 -0
- data/spec/graphql/argument_spec.rb +3 -3
- data/spec/graphql/execution/lazy_spec.rb +8 -114
- data/spec/graphql/execution/multiplex_spec.rb +131 -0
- data/spec/graphql/internal_representation/rewrite_spec.rb +10 -0
- data/spec/graphql/query/arguments_spec.rb +14 -16
- data/spec/graphql/query/context_spec.rb +14 -1
- data/spec/graphql/query/literal_input_spec.rb +19 -13
- data/spec/graphql/query/variables_spec.rb +1 -1
- data/spec/graphql/query_spec.rb +12 -1
- data/spec/graphql/rake_task_spec.rb +57 -0
- data/spec/graphql/relay/array_connection_spec.rb +24 -3
- data/spec/graphql/relay/connection_instrumentation_spec.rb +23 -0
- data/spec/graphql/relay/mutation_spec.rb +2 -10
- data/spec/graphql/relay/page_info_spec.rb +2 -2
- data/spec/graphql/relay/relation_connection_spec.rb +167 -3
- data/spec/graphql/schema/build_from_definition_spec.rb +93 -19
- data/spec/graphql/schema/warden_spec.rb +80 -0
- data/spec/graphql/schema_spec.rb +26 -2
- data/spec/spec_helper.rb +4 -2
- data/spec/support/lazy_helpers.rb +152 -0
- data/spec/support/star_wars/schema.rb +23 -0
- metadata +28 -3
- data/lib/graphql/schema/mask.rb +0 -55
@@ -21,6 +21,7 @@ module GraphQL
|
|
21
21
|
# If it is {Execute::PROPAGATE_NULL}, tell the owner to propagate null.
|
22
22
|
# If the value is a {SelectionResult}, make a link with it, and if it's already null,
|
23
23
|
# propagate the null as needed.
|
24
|
+
# If it's {Execute::Execution::SKIP}, remove this field result from its parent
|
24
25
|
# @param new_value [Any] The GraphQL-ready value
|
25
26
|
def value=(new_value)
|
26
27
|
if new_value.is_a?(SelectionResult)
|
@@ -31,12 +32,15 @@ module GraphQL
|
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
|
-
|
35
|
+
case new_value
|
36
|
+
when GraphQL::Execution::Execute::PROPAGATE_NULL
|
35
37
|
if @type.kind.non_null?
|
36
38
|
@owner.propagate_null
|
37
39
|
else
|
38
40
|
@value = nil
|
39
41
|
end
|
42
|
+
when GraphQL::Execution::Execute::SKIP
|
43
|
+
@owner.delete(self)
|
40
44
|
else
|
41
45
|
@value = new_value
|
42
46
|
end
|
@@ -14,10 +14,8 @@ module GraphQL
|
|
14
14
|
|
15
15
|
def self.resolve_in_place(value)
|
16
16
|
lazies = []
|
17
|
-
acc = []
|
18
|
-
each_lazy(acc, value)
|
19
17
|
|
20
|
-
|
18
|
+
each_lazy(value) do |field_result|
|
21
19
|
inner_lazy = field_result.value.then do |inner_v|
|
22
20
|
field_result.value = inner_v
|
23
21
|
resolve_in_place(inner_v)
|
@@ -28,26 +26,25 @@ module GraphQL
|
|
28
26
|
Lazy.new { lazies.map(&:value) }
|
29
27
|
end
|
30
28
|
|
31
|
-
# If `value` is a collection,
|
32
|
-
#
|
33
|
-
# to `acc`
|
29
|
+
# If `value` is a collection, call `block`
|
30
|
+
# with any {Lazy} instances in the collection
|
34
31
|
# @return [void]
|
35
|
-
def self.each_lazy(
|
32
|
+
def self.each_lazy(value, &block)
|
36
33
|
case value
|
37
34
|
when SelectionResult
|
38
35
|
value.each do |key, field_result|
|
39
|
-
each_lazy(
|
36
|
+
each_lazy(field_result, &block)
|
40
37
|
end
|
41
38
|
when Array
|
42
39
|
value.each do |field_result|
|
43
|
-
each_lazy(
|
40
|
+
each_lazy(field_result, &block)
|
44
41
|
end
|
45
42
|
when FieldResult
|
46
43
|
field_value = value.value
|
47
44
|
if field_value.is_a?(Lazy)
|
48
|
-
|
45
|
+
yield(value)
|
49
46
|
else
|
50
|
-
each_lazy(
|
47
|
+
each_lazy(field_value, &block)
|
51
48
|
end
|
52
49
|
end
|
53
50
|
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module Execution
|
4
|
+
# Execute multiple queries under the same multiplex "umbrella".
|
5
|
+
# They can share a batching context and reduce redundant database hits.
|
6
|
+
#
|
7
|
+
# The flow is:
|
8
|
+
#
|
9
|
+
# - Multiplex instrumentation setup
|
10
|
+
# - Query instrumentation setup
|
11
|
+
# - Analyze the multiplex + each query
|
12
|
+
# - Begin each query
|
13
|
+
# - Resolve lazy values, breadth-first across all queries
|
14
|
+
# - Finish each query (eg, get errors)
|
15
|
+
# - Query instrumentation teardown
|
16
|
+
# - Multiplex instrumentation teardown
|
17
|
+
#
|
18
|
+
# If one query raises an application error, all queries will be in undefined states.
|
19
|
+
#
|
20
|
+
# Validation errors and {GraphQL::ExecutionError}s are handled in isolation:
|
21
|
+
# one of these errors in one query will not affect the other queries.
|
22
|
+
#
|
23
|
+
# @see {Schema#multiplex} for public API
|
24
|
+
# @api private
|
25
|
+
class Multiplex
|
26
|
+
# Used internally to signal that the query shouldn't be executed
|
27
|
+
# @api private
|
28
|
+
NO_OPERATION = {}.freeze
|
29
|
+
|
30
|
+
attr_reader :context, :queries, :schema
|
31
|
+
def initialize(schema:, queries:, context:)
|
32
|
+
@schema = schema
|
33
|
+
@queries = queries
|
34
|
+
@context = context
|
35
|
+
end
|
36
|
+
|
37
|
+
class << self
|
38
|
+
def run_all(schema, query_options, *rest)
|
39
|
+
queries = query_options.map { |opts| GraphQL::Query.new(schema, nil, opts) }
|
40
|
+
run_queries(schema, queries, *rest)
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_queries(schema, queries, context: {}, max_complexity: nil)
|
44
|
+
query_instrumenters = schema.instrumenters[:query]
|
45
|
+
multiplex_instrumenters = schema.instrumenters[:multiplex]
|
46
|
+
multiplex = self.new(schema: schema, queries: queries, context: context)
|
47
|
+
|
48
|
+
# First, run multiplex instrumentation, then query instrumentation for each query
|
49
|
+
multiplex_instrumenters.each { |i| i.before_multiplex(multiplex) }
|
50
|
+
queries.each do |query|
|
51
|
+
query_instrumenters.each { |i| i.before_query(query) }
|
52
|
+
end
|
53
|
+
|
54
|
+
multiplex_analyzers = schema.multiplex_analyzers
|
55
|
+
if max_complexity ||= schema.max_complexity
|
56
|
+
multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity.new(max_complexity)]
|
57
|
+
end
|
58
|
+
|
59
|
+
GraphQL::Analysis.analyze_multiplex(multiplex, multiplex_analyzers)
|
60
|
+
|
61
|
+
# Then, do as much eager evaluation of the query as possible
|
62
|
+
results = queries.map do |query|
|
63
|
+
begin_query(query)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Then, work through lazy results in a breadth-first way
|
67
|
+
GraphQL::Execution::Lazy.resolve(results)
|
68
|
+
|
69
|
+
# Then, find all errors and assign the result to the query object
|
70
|
+
results.each_with_index.map do |data_result, idx|
|
71
|
+
query = queries[idx]
|
72
|
+
finish_query(data_result, query)
|
73
|
+
end
|
74
|
+
ensure
|
75
|
+
# Finally, run teardown instrumentation for each query + the multiplex
|
76
|
+
# Use `reverse_each` so instrumenters are treated like a stack
|
77
|
+
queries.each do |query|
|
78
|
+
query_instrumenters.reverse_each { |i| i.after_query(query) }
|
79
|
+
end
|
80
|
+
multiplex_instrumenters.reverse_each { |i| i.after_multiplex(multiplex) }
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# @param query [GraphQL::Query]
|
86
|
+
# @return [Hash] The initial result (may not be finished if there are lazy values)
|
87
|
+
def begin_query(query)
|
88
|
+
operation = query.selected_operation
|
89
|
+
if operation.nil? || !query.valid?
|
90
|
+
NO_OPERATION
|
91
|
+
else
|
92
|
+
begin
|
93
|
+
op_type = operation.operation_type
|
94
|
+
root_type = query.root_type_for_operation(op_type)
|
95
|
+
GraphQL::Execution::Execute::ExecutionFunctions.resolve_selection(
|
96
|
+
query.root_value,
|
97
|
+
root_type,
|
98
|
+
query.irep_selection,
|
99
|
+
query.context,
|
100
|
+
mutation: query.mutation?
|
101
|
+
)
|
102
|
+
rescue GraphQL::ExecutionError => err
|
103
|
+
query.context.errors << err
|
104
|
+
{}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param data_result [Hash] The result for the "data" key, if any
|
110
|
+
# @param query [GraphQL::Query] The query which was run
|
111
|
+
# @return [Hash] final result of this query, including all values and errors
|
112
|
+
def finish_query(data_result, query)
|
113
|
+
# Assign the result so that it can be accessed in instrumentation
|
114
|
+
query.result = if data_result.equal?(NO_OPERATION)
|
115
|
+
if !query.valid?
|
116
|
+
{ "errors" => query.static_errors.map(&:to_h) }
|
117
|
+
else
|
118
|
+
{}
|
119
|
+
end
|
120
|
+
else
|
121
|
+
result = { "data" => data_result.to_h }
|
122
|
+
error_result = query.context.errors.map(&:to_h)
|
123
|
+
|
124
|
+
if error_result.any?
|
125
|
+
result["errors"] = error_result
|
126
|
+
end
|
127
|
+
|
128
|
+
result
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -38,6 +38,11 @@ module GraphQL
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
# TODO this should delete by key, ya dummy
|
42
|
+
def delete(field_result)
|
43
|
+
@storage.delete_if { |k, v| v == field_result }
|
44
|
+
end
|
45
|
+
|
41
46
|
# A field has been unexpectedly nullified.
|
42
47
|
# Tell the owner {FieldResult} if it is present.
|
43
48
|
# Record {#invalid_null} in case an owner is added later.
|
data/lib/graphql/field.rb
CHANGED
@@ -135,7 +135,7 @@ module GraphQL
|
|
135
135
|
:mutation, :arguments, :complexity, :function,
|
136
136
|
:resolve, :resolve=, :lazy_resolve, :lazy_resolve=, :lazy_resolve_proc, :resolve_proc,
|
137
137
|
:type, :type=, :name=, :property=, :hash_key=,
|
138
|
-
:relay_node_field, :relay_nodes_field
|
138
|
+
:relay_node_field, :relay_nodes_field
|
139
139
|
)
|
140
140
|
|
141
141
|
# @return [Boolean] True if this is the Relay find-by-id field
|
@@ -193,7 +193,6 @@ module GraphQL
|
|
193
193
|
@resolve_proc = build_default_resolver
|
194
194
|
@lazy_resolve_proc = DefaultLazyResolve
|
195
195
|
@relay_node_field = false
|
196
|
-
@default_arguments = nil
|
197
196
|
@connection = false
|
198
197
|
@connection_max_page_size = nil
|
199
198
|
end
|
@@ -201,7 +200,6 @@ module GraphQL
|
|
201
200
|
def initialize_copy(other)
|
202
201
|
super
|
203
202
|
@arguments = other.arguments.dup
|
204
|
-
@default_arguments = nil
|
205
203
|
end
|
206
204
|
|
207
205
|
# Get a value for this field
|
@@ -282,11 +280,6 @@ module GraphQL
|
|
282
280
|
}
|
283
281
|
end
|
284
282
|
|
285
|
-
# @return [GraphQL::Query::Arguments] Arguments to use when no args are provided in the query
|
286
|
-
def default_arguments
|
287
|
-
@default_arguments ||= GraphQL::Query::LiteralInput.defaults_for(self.arguments)
|
288
|
-
end
|
289
|
-
|
290
283
|
private
|
291
284
|
|
292
285
|
def build_default_resolver
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
# @api private
|
4
|
+
class Filter
|
5
|
+
def initialize(only: nil, except: nil)
|
6
|
+
@only = only
|
7
|
+
@except = except
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns true if `member, ctx` passes this filter
|
11
|
+
def call(member, ctx)
|
12
|
+
(@only ? @only.call(member, ctx) : true) &&
|
13
|
+
(@except ? !@except.call(member, ctx) : true)
|
14
|
+
end
|
15
|
+
|
16
|
+
def merge(only: nil, except: nil)
|
17
|
+
onlies = [self].concat(Array(only))
|
18
|
+
merged_only = MergedOnly.build(onlies)
|
19
|
+
merged_except = MergedExcept.build(Array(except))
|
20
|
+
self.class.new(only: merged_only, except: merged_except)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
class MergedOnly
|
26
|
+
def initialize(first, second)
|
27
|
+
@first = first
|
28
|
+
@second = second
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(member, ctx)
|
32
|
+
@first.call(member, ctx) && @second.call(member, ctx)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.build(onlies)
|
36
|
+
case onlies
|
37
|
+
when 0
|
38
|
+
nil
|
39
|
+
when 1
|
40
|
+
onlies[0]
|
41
|
+
else
|
42
|
+
onlies.reduce { |memo, only| self.new(memo, only) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class MergedExcept < MergedOnly
|
48
|
+
def call(member, ctx)
|
49
|
+
@first.call(member, ctx) || @second.call(member, ctx)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -2,8 +2,6 @@
|
|
2
2
|
module GraphQL
|
3
3
|
module InternalRepresentation
|
4
4
|
class Node
|
5
|
-
# @api private
|
6
|
-
DEFAULT_TYPED_CHILDREN = Proc.new { |h, k| h[k] = {} }
|
7
5
|
# @return [String] the name this node has in the response
|
8
6
|
attr_reader :name
|
9
7
|
|
@@ -17,13 +15,13 @@ module GraphQL
|
|
17
15
|
# @return [Hash<GraphQL::ObjectType, Hash<String => Node>>]
|
18
16
|
def typed_children
|
19
17
|
@typed_childen ||= begin
|
20
|
-
new_tc = Hash.new
|
18
|
+
new_tc = Hash.new { |h, k| h[k] = {} }
|
21
19
|
if @scoped_children.any?
|
22
20
|
all_object_types = Set.new
|
23
21
|
scoped_children.each_key { |t| all_object_types.merge(@query.possible_types(t)) }
|
24
22
|
# Remove any scoped children which don't follow this return type
|
25
23
|
# (This can happen with fragment merging where lexical scope is lost)
|
26
|
-
all_object_types &= @query.possible_types(@return_type)
|
24
|
+
all_object_types &= @query.possible_types(@return_type.unwrap)
|
27
25
|
all_object_types.each do |t|
|
28
26
|
new_tc[t] = get_typed_children(t)
|
29
27
|
end
|
@@ -47,7 +45,7 @@ module GraphQL
|
|
47
45
|
# @return [Array<GraphQL::Field>] Field definitions for this node (there should only be one!)
|
48
46
|
attr_reader :definitions
|
49
47
|
|
50
|
-
# @return [GraphQL::BaseType]
|
48
|
+
# @return [GraphQL::BaseType] The expected wrapped type this node must return.
|
51
49
|
attr_reader :return_type
|
52
50
|
|
53
51
|
# @return [InternalRepresentation::Node, nil]
|
@@ -97,6 +95,10 @@ module GraphQL
|
|
97
95
|
definition && definition.name
|
98
96
|
end
|
99
97
|
|
98
|
+
def arguments
|
99
|
+
@query.arguments_for(self, definition)
|
100
|
+
end
|
101
|
+
|
100
102
|
def definition
|
101
103
|
@definition ||= begin
|
102
104
|
first_def = @definitions.first
|
@@ -121,7 +123,7 @@ module GraphQL
|
|
121
123
|
@ast_nodes |= new_parent.ast_nodes
|
122
124
|
@definitions |= new_parent.definitions
|
123
125
|
end
|
124
|
-
scope ||= Scope.new(@query, @return_type)
|
126
|
+
scope ||= Scope.new(@query, @return_type.unwrap)
|
125
127
|
new_parent.scoped_children.each do |obj_type, new_fields|
|
126
128
|
inner_scope = scope.enter(obj_type)
|
127
129
|
inner_scope.each do |scoped_type|
|
@@ -138,6 +140,9 @@ module GraphQL
|
|
138
140
|
end
|
139
141
|
end
|
140
142
|
|
143
|
+
# @return [GraphQL::Query]
|
144
|
+
attr_reader :query
|
145
|
+
|
141
146
|
protected
|
142
147
|
|
143
148
|
attr_writer :owner_type, :parent
|
@@ -93,7 +93,7 @@ module GraphQL
|
|
93
93
|
# It's a non-existent field
|
94
94
|
new_scope = nil
|
95
95
|
else
|
96
|
-
field_return_type = field_defn.type
|
96
|
+
field_return_type = field_defn.type
|
97
97
|
scopes_stack.last.each do |scope_type|
|
98
98
|
parent_nodes.each do |parent_node|
|
99
99
|
node = parent_node.scoped_children[scope_type][node_name] ||= Node.new(
|
@@ -108,7 +108,7 @@ module GraphQL
|
|
108
108
|
next_nodes << node
|
109
109
|
end
|
110
110
|
end
|
111
|
-
new_scope = Scope.new(query, field_return_type)
|
111
|
+
new_scope = Scope.new(query, field_return_type.unwrap)
|
112
112
|
end
|
113
113
|
|
114
114
|
nodes_stack.push(next_nodes)
|
@@ -181,7 +181,7 @@ module GraphQL
|
|
181
181
|
owner_type: owner_type,
|
182
182
|
query: @query,
|
183
183
|
ast_nodes: [ast_node],
|
184
|
-
return_type:
|
184
|
+
return_type: @context.type_definition,
|
185
185
|
)
|
186
186
|
|
187
187
|
@definitions[defn_name] = node
|
data/lib/graphql/query.rb
CHANGED
@@ -14,7 +14,7 @@ require "graphql/query/validation_pipeline"
|
|
14
14
|
module GraphQL
|
15
15
|
# A combination of query string and {Schema} instance which can be reduced to a {#result}.
|
16
16
|
class Query
|
17
|
-
extend
|
17
|
+
extend Forwardable
|
18
18
|
|
19
19
|
class OperationNameMissingError < GraphQL::ExecutionError
|
20
20
|
def initialize(name)
|
@@ -27,7 +27,19 @@ module GraphQL
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
attr_reader :schema, :
|
30
|
+
attr_reader :schema, :context, :root_value, :warden, :provided_variables
|
31
|
+
|
32
|
+
attr_accessor :query_string
|
33
|
+
|
34
|
+
# @return [GraphQL::Language::Nodes::Document]
|
35
|
+
def document
|
36
|
+
with_prepared_ast { @document }
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String, nil] The name of the operation to run (may be inferred)
|
40
|
+
def operation_name
|
41
|
+
with_prepared_ast { @operation_name }
|
42
|
+
end
|
31
43
|
|
32
44
|
# Prepare query `query_string` on `schema`
|
33
45
|
# @param schema [GraphQL::Schema]
|
@@ -40,39 +52,23 @@ module GraphQL
|
|
40
52
|
# @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value)
|
41
53
|
# @param except [<#call(schema_member, context)>] If provided, objects will be hidden from the schema when `.call(schema_member, context)` returns truthy
|
42
54
|
# @param only [<#call(schema_member, context)>] If provided, objects will be hidden from the schema when `.call(schema_member, context)` returns false
|
43
|
-
def initialize(schema, query_string = nil, document: nil, context: nil, variables: {}, validate: true, operation_name: nil, root_value: nil, max_depth: nil, max_complexity: nil, except: nil, only: nil)
|
44
|
-
fail ArgumentError, "a query string or document is required" unless query_string || document
|
45
|
-
|
55
|
+
def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: {}, validate: true, operation_name: nil, root_value: nil, max_depth: nil, max_complexity: nil, except: nil, only: nil)
|
46
56
|
@schema = schema
|
47
|
-
|
57
|
+
@filter = schema.default_filter.merge(except: except, only: only)
|
48
58
|
@context = Context.new(query: self, values: context)
|
49
|
-
@warden = GraphQL::Schema::Warden.new(mask, schema: @schema, context: @context)
|
50
59
|
@root_value = root_value
|
51
|
-
@fragments =
|
52
|
-
@operations =
|
60
|
+
@fragments = nil
|
61
|
+
@operations = nil
|
62
|
+
|
63
|
+
@analysis_errors = []
|
53
64
|
if variables.is_a?(String)
|
54
65
|
raise ArgumentError, "Query variables should be a Hash, not a String. Try JSON.parse to prepare variables."
|
55
66
|
else
|
56
67
|
@provided_variables = variables
|
57
68
|
end
|
58
|
-
@query_string = query_string
|
59
|
-
parse_error = nil
|
60
|
-
@document = document || begin
|
61
|
-
GraphQL.parse(query_string)
|
62
|
-
rescue GraphQL::ParseError => err
|
63
|
-
parse_error = err
|
64
|
-
@schema.parse_error(err, @context)
|
65
|
-
nil
|
66
|
-
end
|
67
69
|
|
68
|
-
@
|
69
|
-
|
70
|
-
when GraphQL::Language::Nodes::FragmentDefinition
|
71
|
-
@fragments[part.name] = part
|
72
|
-
when GraphQL::Language::Nodes::OperationDefinition
|
73
|
-
@operations[part.name] = part
|
74
|
-
end
|
75
|
-
end
|
70
|
+
@query_string = query_string || query
|
71
|
+
@document = document
|
76
72
|
|
77
73
|
@resolved_types_cache = Hash.new { |h, k| h[k] = @schema.resolve_type(k, @context) }
|
78
74
|
|
@@ -82,64 +78,56 @@ module GraphQL
|
|
82
78
|
# with no operations returns an empty hash
|
83
79
|
@ast_variables = []
|
84
80
|
@mutation = false
|
85
|
-
|
86
|
-
@
|
81
|
+
@operation_name = operation_name
|
82
|
+
@prepared_ast = false
|
87
83
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
operation_name_error = GraphQL::Query::OperationNameMissingError.new(operation_name)
|
92
|
-
else
|
93
|
-
@operation_name = selected_operation.name
|
94
|
-
@ast_variables = selected_operation.variables
|
95
|
-
@mutation = selected_operation.operation_type == "mutation"
|
96
|
-
@selected_operation = selected_operation
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
@validation_pipeline = GraphQL::Query::ValidationPipeline.new(
|
101
|
-
query: self,
|
102
|
-
parse_error: parse_error,
|
103
|
-
operation_name_error: operation_name_error,
|
104
|
-
max_depth: max_depth || schema.max_depth,
|
105
|
-
max_complexity: max_complexity || schema.max_complexity,
|
106
|
-
)
|
84
|
+
@validation_pipeline = nil
|
85
|
+
@max_depth = max_depth || schema.max_depth
|
86
|
+
@max_complexity = max_complexity || schema.max_complexity
|
107
87
|
|
108
88
|
@result = nil
|
109
89
|
@executed = false
|
110
90
|
end
|
111
91
|
|
112
|
-
#
|
113
|
-
|
114
|
-
def result
|
92
|
+
# @api private
|
93
|
+
def result=(result_hash)
|
115
94
|
if @executed
|
116
|
-
|
95
|
+
raise "Invariant: Can't reassign result"
|
117
96
|
else
|
118
97
|
@executed = true
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
98
|
+
@result = result_hash
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def fragments
|
103
|
+
with_prepared_ast { @fragments }
|
104
|
+
end
|
105
|
+
|
106
|
+
def operations
|
107
|
+
with_prepared_ast { @operations }
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get the result for this query, executing it once
|
111
|
+
# @return [Hash] A GraphQL response, with `"data"` and/or `"errors"` keys
|
112
|
+
def result
|
113
|
+
if !@executed
|
114
|
+
with_prepared_ast {
|
115
|
+
Execution::Multiplex.run_queries(@schema, [self])
|
116
|
+
}
|
135
117
|
end
|
118
|
+
@result
|
136
119
|
end
|
137
120
|
|
121
|
+
def static_errors
|
122
|
+
validation_errors + analysis_errors + context.errors
|
123
|
+
end
|
138
124
|
|
139
125
|
# This is the operation to run for this query.
|
140
126
|
# If more than one operation is present, it must be named at runtime.
|
141
127
|
# @return [GraphQL::Language::Nodes::OperationDefinition, nil]
|
142
|
-
|
128
|
+
def selected_operation
|
129
|
+
with_prepared_ast { @selected_operation }
|
130
|
+
end
|
143
131
|
|
144
132
|
# Determine the values for variables of this query, using default values
|
145
133
|
# if a value isn't provided at runtime.
|
@@ -149,12 +137,13 @@ module GraphQL
|
|
149
137
|
# @return [GraphQL::Query::Variables] Variables to apply to this query
|
150
138
|
def variables
|
151
139
|
@variables ||= begin
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
140
|
+
with_prepared_ast {
|
141
|
+
GraphQL::Query::Variables.new(
|
142
|
+
@context,
|
143
|
+
@ast_variables,
|
144
|
+
@provided_variables,
|
145
|
+
)
|
146
|
+
}
|
158
147
|
end
|
159
148
|
end
|
160
149
|
|
@@ -169,12 +158,27 @@ module GraphQL
|
|
169
158
|
@arguments_cache[irep_or_ast_node][definition]
|
170
159
|
end
|
171
160
|
|
172
|
-
# @return [GraphQL::Language::Nodes::
|
173
|
-
|
161
|
+
# @return [GraphQL::Language::Nodes::OperationDefinition, nil]
|
162
|
+
def selected_operation
|
163
|
+
with_prepared_ast { @selected_operation }
|
164
|
+
end
|
174
165
|
|
175
|
-
|
166
|
+
def validation_pipeline
|
167
|
+
with_prepared_ast { @validation_pipeline }
|
168
|
+
end
|
169
|
+
|
170
|
+
def_delegators :validation_pipeline, :validation_errors, :internal_representation, :analyzers
|
171
|
+
|
172
|
+
attr_accessor :analysis_errors
|
173
|
+
def valid?
|
174
|
+
validation_pipeline.valid? && analysis_errors.none?
|
175
|
+
end
|
176
|
+
|
177
|
+
def warden
|
178
|
+
with_prepared_ast { @warden }
|
179
|
+
end
|
176
180
|
|
177
|
-
def_delegators
|
181
|
+
def_delegators :warden, :get_type, :get_field, :possible_types, :root_type_for_operation
|
178
182
|
|
179
183
|
# @param value [Object] Any runtime value
|
180
184
|
# @return [GraphQL::ObjectType, nil] The runtime type of `value` from {Schema#resolve_type}
|
@@ -187,6 +191,16 @@ module GraphQL
|
|
187
191
|
@mutation
|
188
192
|
end
|
189
193
|
|
194
|
+
# @return [void]
|
195
|
+
def merge_filters(only: nil, except: nil)
|
196
|
+
if @prepared_ast
|
197
|
+
raise "Can't add filters after preparing the query"
|
198
|
+
else
|
199
|
+
@filter = @filter.merge(only: only, except: except)
|
200
|
+
end
|
201
|
+
nil
|
202
|
+
end
|
203
|
+
|
190
204
|
private
|
191
205
|
|
192
206
|
def find_operation(operations, operation_name)
|
@@ -198,5 +212,73 @@ module GraphQL
|
|
198
212
|
operations.fetch(operation_name)
|
199
213
|
end
|
200
214
|
end
|
215
|
+
|
216
|
+
def prepare_ast
|
217
|
+
@prepared_ast = true
|
218
|
+
@warden = GraphQL::Schema::Warden.new(@filter, schema: @schema, context: @context)
|
219
|
+
|
220
|
+
parse_error = nil
|
221
|
+
@document ||= begin
|
222
|
+
if query_string
|
223
|
+
GraphQL.parse(query_string)
|
224
|
+
end
|
225
|
+
rescue GraphQL::ParseError => err
|
226
|
+
parse_error = err
|
227
|
+
@schema.parse_error(err, @context)
|
228
|
+
nil
|
229
|
+
end
|
230
|
+
|
231
|
+
@fragments = {}
|
232
|
+
@operations = {}
|
233
|
+
if @document
|
234
|
+
@document.definitions.each do |part|
|
235
|
+
case part
|
236
|
+
when GraphQL::Language::Nodes::FragmentDefinition
|
237
|
+
@fragments[part.name] = part
|
238
|
+
when GraphQL::Language::Nodes::OperationDefinition
|
239
|
+
@operations[part.name] = part
|
240
|
+
end
|
241
|
+
end
|
242
|
+
elsif parse_error
|
243
|
+
# This will be handled later
|
244
|
+
else
|
245
|
+
raise ArgumentError, "a query string or document is required"
|
246
|
+
end
|
247
|
+
|
248
|
+
# Trying to execute a document
|
249
|
+
# with no operations returns an empty hash
|
250
|
+
@ast_variables = []
|
251
|
+
@mutation = false
|
252
|
+
operation_name_error = nil
|
253
|
+
if @operations.any?
|
254
|
+
@selected_operation = find_operation(@operations, @operation_name)
|
255
|
+
if @selected_operation.nil?
|
256
|
+
operation_name_error = GraphQL::Query::OperationNameMissingError.new(@operation_name)
|
257
|
+
else
|
258
|
+
if @operation_name.nil?
|
259
|
+
@operation_name = @selected_operation.name
|
260
|
+
end
|
261
|
+
@ast_variables = @selected_operation.variables
|
262
|
+
@mutation = @selected_operation.operation_type == "mutation"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
@validation_pipeline = GraphQL::Query::ValidationPipeline.new(
|
267
|
+
query: self,
|
268
|
+
parse_error: parse_error,
|
269
|
+
operation_name_error: operation_name_error,
|
270
|
+
max_depth: @max_depth,
|
271
|
+
max_complexity: @max_complexity || schema.max_complexity,
|
272
|
+
)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Since the query string is processed at the last possible moment,
|
276
|
+
# any internal values which depend on it should be accessed within this wrapper.
|
277
|
+
def with_prepared_ast
|
278
|
+
if !@prepared_ast
|
279
|
+
prepare_ast
|
280
|
+
end
|
281
|
+
yield
|
282
|
+
end
|
201
283
|
end
|
202
284
|
end
|