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.
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