graphql 1.5.15 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -19
  3. data/lib/graphql/analysis/analyze_query.rb +27 -2
  4. data/lib/graphql/analysis/query_complexity.rb +10 -11
  5. data/lib/graphql/argument.rb +7 -6
  6. data/lib/graphql/backwards_compatibility.rb +47 -0
  7. data/lib/graphql/compatibility/execution_specification.rb +14 -0
  8. data/lib/graphql/compatibility/execution_specification/specification_schema.rb +6 -1
  9. data/lib/graphql/compatibility/lazy_execution_specification.rb +19 -0
  10. data/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb +15 -6
  11. data/lib/graphql/directive.rb +1 -6
  12. data/lib/graphql/execution.rb +1 -0
  13. data/lib/graphql/execution/execute.rb +174 -160
  14. data/lib/graphql/execution/field_result.rb +5 -1
  15. data/lib/graphql/execution/lazy.rb +2 -2
  16. data/lib/graphql/execution/lazy/resolve.rb +8 -11
  17. data/lib/graphql/execution/multiplex.rb +134 -0
  18. data/lib/graphql/execution/selection_result.rb +5 -0
  19. data/lib/graphql/field.rb +1 -8
  20. data/lib/graphql/filter.rb +53 -0
  21. data/lib/graphql/internal_representation/node.rb +11 -6
  22. data/lib/graphql/internal_representation/rewrite.rb +3 -3
  23. data/lib/graphql/query.rb +160 -78
  24. data/lib/graphql/query/arguments.rb +14 -25
  25. data/lib/graphql/query/arguments_cache.rb +6 -13
  26. data/lib/graphql/query/context.rb +28 -10
  27. data/lib/graphql/query/executor.rb +1 -0
  28. data/lib/graphql/query/literal_input.rb +10 -4
  29. data/lib/graphql/query/null_context.rb +1 -1
  30. data/lib/graphql/query/serial_execution/field_resolution.rb +5 -1
  31. data/lib/graphql/query/validation_pipeline.rb +12 -7
  32. data/lib/graphql/query/variables.rb +1 -1
  33. data/lib/graphql/rake_task.rb +140 -0
  34. data/lib/graphql/relay/array_connection.rb +29 -48
  35. data/lib/graphql/relay/base_connection.rb +9 -7
  36. data/lib/graphql/relay/mutation.rb +0 -11
  37. data/lib/graphql/relay/mutation/instrumentation.rb +2 -2
  38. data/lib/graphql/relay/mutation/resolve.rb +7 -10
  39. data/lib/graphql/relay/relation_connection.rb +98 -61
  40. data/lib/graphql/scalar_type.rb +1 -15
  41. data/lib/graphql/schema.rb +90 -25
  42. data/lib/graphql/schema/build_from_definition.rb +22 -23
  43. data/lib/graphql/schema/build_from_definition/resolve_map.rb +70 -0
  44. data/lib/graphql/schema/build_from_definition/resolve_map/default_resolve.rb +47 -0
  45. data/lib/graphql/schema/middleware_chain.rb +1 -1
  46. data/lib/graphql/schema/printer.rb +2 -1
  47. data/lib/graphql/schema/timeout_middleware.rb +6 -6
  48. data/lib/graphql/schema/type_map.rb +1 -1
  49. data/lib/graphql/schema/warden.rb +5 -9
  50. data/lib/graphql/static_validation/definition_dependencies.rb +1 -1
  51. data/lib/graphql/version.rb +1 -1
  52. data/spec/graphql/analysis/analyze_query_spec.rb +2 -2
  53. data/spec/graphql/analysis/max_query_complexity_spec.rb +28 -0
  54. data/spec/graphql/argument_spec.rb +3 -3
  55. data/spec/graphql/execution/lazy_spec.rb +8 -114
  56. data/spec/graphql/execution/multiplex_spec.rb +131 -0
  57. data/spec/graphql/internal_representation/rewrite_spec.rb +10 -0
  58. data/spec/graphql/query/arguments_spec.rb +14 -16
  59. data/spec/graphql/query/context_spec.rb +14 -1
  60. data/spec/graphql/query/literal_input_spec.rb +19 -13
  61. data/spec/graphql/query/variables_spec.rb +1 -1
  62. data/spec/graphql/query_spec.rb +12 -1
  63. data/spec/graphql/rake_task_spec.rb +57 -0
  64. data/spec/graphql/relay/array_connection_spec.rb +24 -3
  65. data/spec/graphql/relay/connection_instrumentation_spec.rb +23 -0
  66. data/spec/graphql/relay/mutation_spec.rb +2 -10
  67. data/spec/graphql/relay/page_info_spec.rb +2 -2
  68. data/spec/graphql/relay/relation_connection_spec.rb +167 -3
  69. data/spec/graphql/schema/build_from_definition_spec.rb +93 -19
  70. data/spec/graphql/schema/warden_spec.rb +80 -0
  71. data/spec/graphql/schema_spec.rb +26 -2
  72. data/spec/spec_helper.rb +4 -2
  73. data/spec/support/lazy_helpers.rb +152 -0
  74. data/spec/support/star_wars/schema.rb +23 -0
  75. metadata +28 -3
  76. 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
- if new_value == GraphQL::Execution::Execute::PROPAGATE_NULL
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
@@ -40,9 +40,9 @@ module GraphQL
40
40
  end
41
41
 
42
42
  # @return [Lazy] A {Lazy} whose value depends on another {Lazy}, plus any transformations in `block`
43
- def then
43
+ def then(&block)
44
44
  self.class.new {
45
- yield(value)
45
+ block.call(value)
46
46
  }
47
47
  end
48
48
  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
- acc.each do |field_result|
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
- # add any {Lazy} instances in the collection
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(acc, value)
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(acc, field_result)
36
+ each_lazy(field_result, &block)
40
37
  end
41
38
  when Array
42
39
  value.each do |field_result|
43
- each_lazy(acc, field_result)
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
- acc << value
45
+ yield(value)
49
46
  else
50
- each_lazy(acc, field_value)
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.
@@ -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, :default_arguments
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(&DEFAULT_TYPED_CHILDREN)
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.unwrap
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: owner_type,
184
+ return_type: @context.type_definition,
185
185
  )
186
186
 
187
187
  @definitions[defn_name] = node
@@ -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 GraphQL::Delegate
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, :document, :context, :fragments, :operations, :root_value, :query_string, :warden, :provided_variables, :operation_name
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
- mask = GraphQL::Schema::Mask.combine(schema.default_mask, except: except, only: only)
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
- @document && @document.definitions.each do |part|
69
- case part
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
- operation_name_error = nil
86
- @operation_name = nil
81
+ @operation_name = operation_name
82
+ @prepared_ast = false
87
83
 
88
- if @operations.any?
89
- selected_operation = find_operation(@operations, operation_name)
90
- if selected_operation.nil?
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
- # Get the result for this query, executing it once
113
- # @return [Hash] A GraphQL response, with `"data"` and/or `"errors"` keys
114
- def result
92
+ # @api private
93
+ def result=(result_hash)
115
94
  if @executed
116
- @result
95
+ raise "Invariant: Can't reassign result"
117
96
  else
118
97
  @executed = true
119
- instrumenters = @schema.instrumenters[:query]
120
- begin
121
- instrumenters.each { |i| i.before_query(self) }
122
- @result = if !valid?
123
- all_errors = validation_errors + analysis_errors + context.errors
124
- if all_errors.any?
125
- { "errors" => all_errors.map(&:to_h) }
126
- else
127
- nil
128
- end
129
- else
130
- Executor.new(self).result
131
- end
132
- ensure
133
- instrumenters.each { |i| i.after_query(self) }
134
- end
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
- attr_reader :selected_operation
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
- vars = GraphQL::Query::Variables.new(
153
- @context,
154
- @ast_variables,
155
- @provided_variables,
156
- )
157
- vars
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::Document, nil]
173
- attr_reader :selected_operation
161
+ # @return [GraphQL::Language::Nodes::OperationDefinition, nil]
162
+ def selected_operation
163
+ with_prepared_ast { @selected_operation }
164
+ end
174
165
 
175
- def_delegators :@validation_pipeline, :valid?, :analysis_errors, :validation_errors, :internal_representation
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 :@warden, :get_type, :get_field, :possible_types, :root_type_for_operation
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