graphql 1.6.8 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +5 -0
  3. data/lib/graphql/analysis/analyze_query.rb +21 -17
  4. data/lib/graphql/argument.rb +6 -2
  5. data/lib/graphql/backtrace.rb +50 -0
  6. data/lib/graphql/backtrace/inspect_result.rb +51 -0
  7. data/lib/graphql/backtrace/table.rb +120 -0
  8. data/lib/graphql/backtrace/traced_error.rb +55 -0
  9. data/lib/graphql/backtrace/tracer.rb +50 -0
  10. data/lib/graphql/enum_type.rb +1 -10
  11. data/lib/graphql/execution.rb +1 -2
  12. data/lib/graphql/execution/execute.rb +98 -89
  13. data/lib/graphql/execution/flatten.rb +40 -0
  14. data/lib/graphql/execution/lazy/resolve.rb +7 -7
  15. data/lib/graphql/execution/multiplex.rb +29 -29
  16. data/lib/graphql/field.rb +5 -1
  17. data/lib/graphql/internal_representation/node.rb +16 -0
  18. data/lib/graphql/invalid_name_error.rb +11 -0
  19. data/lib/graphql/language/parser.rb +11 -5
  20. data/lib/graphql/language/parser.y +11 -5
  21. data/lib/graphql/name_validator.rb +16 -0
  22. data/lib/graphql/object_type.rb +5 -0
  23. data/lib/graphql/query.rb +28 -7
  24. data/lib/graphql/query/context.rb +155 -52
  25. data/lib/graphql/query/literal_input.rb +36 -9
  26. data/lib/graphql/query/null_context.rb +7 -1
  27. data/lib/graphql/query/result.rb +63 -0
  28. data/lib/graphql/query/serial_execution/field_resolution.rb +3 -4
  29. data/lib/graphql/query/serial_execution/value_resolution.rb +3 -4
  30. data/lib/graphql/query/variables.rb +1 -1
  31. data/lib/graphql/schema.rb +31 -0
  32. data/lib/graphql/schema/traversal.rb +16 -1
  33. data/lib/graphql/schema/warden.rb +40 -4
  34. data/lib/graphql/static_validation/validator.rb +20 -18
  35. data/lib/graphql/subscriptions.rb +129 -0
  36. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +122 -0
  37. data/lib/graphql/subscriptions/event.rb +52 -0
  38. data/lib/graphql/subscriptions/instrumentation.rb +68 -0
  39. data/lib/graphql/tracing.rb +80 -0
  40. data/lib/graphql/tracing/active_support_notifications_tracing.rb +31 -0
  41. data/lib/graphql/version.rb +1 -1
  42. data/readme.md +1 -1
  43. data/spec/graphql/analysis/analyze_query_spec.rb +19 -0
  44. data/spec/graphql/argument_spec.rb +28 -0
  45. data/spec/graphql/backtrace_spec.rb +144 -0
  46. data/spec/graphql/define/assign_argument_spec.rb +12 -0
  47. data/spec/graphql/enum_type_spec.rb +1 -1
  48. data/spec/graphql/execution/execute_spec.rb +66 -0
  49. data/spec/graphql/execution/lazy_spec.rb +4 -3
  50. data/spec/graphql/language/parser_spec.rb +16 -0
  51. data/spec/graphql/object_type_spec.rb +14 -0
  52. data/spec/graphql/query/context_spec.rb +134 -27
  53. data/spec/graphql/query/result_spec.rb +29 -0
  54. data/spec/graphql/query/variables_spec.rb +13 -0
  55. data/spec/graphql/query_spec.rb +22 -0
  56. data/spec/graphql/schema/build_from_definition_spec.rb +2 -0
  57. data/spec/graphql/schema/traversal_spec.rb +70 -12
  58. data/spec/graphql/schema/warden_spec.rb +67 -1
  59. data/spec/graphql/schema_spec.rb +29 -0
  60. data/spec/graphql/static_validation/validator_spec.rb +16 -0
  61. data/spec/graphql/subscriptions_spec.rb +331 -0
  62. data/spec/graphql/tracing/active_support_notifications_tracing_spec.rb +57 -0
  63. data/spec/graphql/tracing_spec.rb +47 -0
  64. data/spec/spec_helper.rb +32 -0
  65. data/spec/support/star_wars/schema.rb +39 -0
  66. metadata +27 -4
  67. data/lib/graphql/execution/field_result.rb +0 -54
  68. data/lib/graphql/execution/selection_result.rb +0 -90
@@ -141,13 +141,7 @@ module GraphQL
141
141
 
142
142
  def name=(new_name)
143
143
  # Validate that the name is correct
144
- unless new_name =~ /^[_a-zA-Z][_a-zA-Z0-9]*$/
145
- raise(
146
- GraphQL::EnumType::InvalidEnumNameError,
147
- "Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but '#{new_name}' does not"
148
- )
149
- end
150
-
144
+ GraphQL::NameValidator.validate!(new_name)
151
145
  @name = new_name
152
146
  end
153
147
  end
@@ -155,9 +149,6 @@ module GraphQL
155
149
  class UnresolvedValueError < GraphQL::Error
156
150
  end
157
151
 
158
- class InvalidEnumNameError < GraphQL::Error
159
- end
160
-
161
152
  private
162
153
 
163
154
  # Get the underlying value for this enum value
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/execution/directive_checks"
3
3
  require "graphql/execution/execute"
4
- require "graphql/execution/field_result"
4
+ require "graphql/execution/flatten"
5
5
  require "graphql/execution/lazy"
6
6
  require "graphql/execution/multiplex"
7
- require "graphql/execution/selection_result"
8
7
  require "graphql/execution/typecast"
@@ -13,104 +13,126 @@ module GraphQL
13
13
  SKIP = Skip.new
14
14
 
15
15
  # @api private
16
- PROPAGATE_NULL = Object.new
16
+ class PropagateNull
17
+ end
18
+ # @api private
19
+ PROPAGATE_NULL = PropagateNull.new
17
20
 
18
21
  def execute(ast_operation, root_type, query)
19
- result = resolve_selection(
20
- query.root_value,
21
- root_type,
22
- query.irep_selection,
23
- query.context,
24
- mutation: query.mutation?
25
- )
26
-
27
- GraphQL::Execution::Lazy.resolve(result)
28
-
29
- result.to_h
22
+ result = resolve_root_selection(query)
23
+ lazy_resolve_root_selection(result, {query: query})
24
+ GraphQL::Execution::Flatten.call(query.context)
30
25
  end
31
26
 
32
27
  # @api private
33
28
  module ExecutionFunctions
34
29
  module_function
35
30
 
36
- def resolve_selection(object, current_type, selection, query_ctx, mutation: false )
37
- selection_result = SelectionResult.new
38
-
39
- selection.typed_children[current_type].each do |name, subselection|
40
- field_result = resolve_field(
41
- selection_result,
42
- subselection,
43
- current_type,
44
- subselection.definition,
45
- object,
46
- query_ctx
31
+ def resolve_root_selection(query)
32
+ GraphQL::Tracing.trace("execute_query", query: query) do
33
+ operation = query.selected_operation
34
+ op_type = operation.operation_type
35
+ root_type = query.root_type_for_operation(op_type)
36
+ resolve_selection(
37
+ query.root_value,
38
+ root_type,
39
+ query.context,
40
+ mutation: query.mutation?
47
41
  )
42
+ end
43
+ end
44
+
45
+ def lazy_resolve_root_selection(result, query: nil, queries: nil)
46
+ if query.nil? && queries.length == 1
47
+ query = queries[0]
48
+ end
49
+
50
+ GraphQL::Tracing.trace("execute_query_lazy", {queries: queries, query: query}) do
51
+ GraphQL::Execution::Lazy.resolve(result)
52
+ end
53
+ end
54
+
55
+ def resolve_selection(object, current_type, current_ctx, mutation: false )
56
+ # Assign this _before_ resolving the children
57
+ # so that when a child propagates null, the selection result is
58
+ # ready for it.
59
+ current_ctx.value = {}
60
+
61
+ selections_on_type = current_ctx.irep_node.typed_children[current_type]
62
+
63
+ selections_on_type.each do |name, child_irep_node|
64
+ field_ctx = current_ctx.spawn_child(
65
+ key: name,
66
+ object: object,
67
+ irep_node: child_irep_node,
68
+ )
69
+
70
+ field_result = GraphQL::Tracing.trace("execute_field", { context: field_ctx }) do
71
+ resolve_field(
72
+ object,
73
+ field_ctx
74
+ )
75
+ end
48
76
 
49
77
  if field_result.is_a?(Skip)
50
78
  next
51
79
  end
52
80
 
53
81
  if mutation
54
- GraphQL::Execution::Lazy.resolve(field_result)
82
+ GraphQL::Execution::Lazy.resolve(field_ctx)
55
83
  end
56
84
 
57
- selection_result.set(name, field_result)
58
85
 
59
86
  # If the last subselection caused a null to propagate to _this_ selection,
60
87
  # then we may as well quit executing fields because they
61
88
  # won't be in the response
62
- if selection_result.invalid_null?
89
+ if current_ctx.invalid_null?
63
90
  break
91
+ else
92
+ current_ctx.value[name] = field_ctx
64
93
  end
65
94
  end
66
95
 
67
- selection_result
96
+ current_ctx.value
68
97
  end
69
98
 
70
- def resolve_field(owner, selection, parent_type, field, object, query_ctx)
71
- query = query_ctx.query
72
- field_ctx = query_ctx.spawn(
73
- parent_type: parent_type,
74
- field: field,
75
- key: selection.name,
76
- selection: selection,
77
- )
99
+ def resolve_field(object, field_ctx)
100
+ query = field_ctx.query
101
+ irep_node = field_ctx.irep_node
102
+ parent_type = irep_node.owner_type
103
+ field = field_ctx.field
78
104
 
79
105
  raw_value = begin
80
- arguments = query.arguments_for(selection, field)
81
- query_ctx.schema.middleware.invoke([parent_type, object, field, arguments, field_ctx])
106
+ arguments = query.arguments_for(irep_node, field)
107
+ field_ctx.schema.middleware.invoke([parent_type, object, field, arguments, field_ctx])
82
108
  rescue GraphQL::ExecutionError => err
83
109
  err
84
110
  end
85
111
 
86
- result = if query.schema.lazy?(raw_value)
87
- field.prepare_lazy(raw_value, arguments, field_ctx).then { |inner_value|
88
- continue_resolve_field(owner, selection, parent_type, field, inner_value, field_ctx)
112
+ # If the returned object is lazy (unfinished),
113
+ # assign the lazy object to `.value=` so we can resolve it later.
114
+ # When we resolve it later, reassign it to `.value=` so that
115
+ # the finished value replaces the unfinished one.
116
+ #
117
+ # If the returned object is finished, continue to coerce
118
+ # and resolve child fields
119
+ if query.schema.lazy?(raw_value)
120
+ field_ctx.value = field.prepare_lazy(raw_value, arguments, field_ctx).then { |inner_value|
121
+ field_ctx.value = continue_resolve_field(inner_value, field_ctx)
89
122
  }
90
123
  elsif raw_value.is_a?(GraphQL::Execution::Lazy)
91
124
  # It came from a connection resolve, assume it was already instrumented
92
- raw_value.then { |inner_value|
93
- continue_resolve_field(owner, selection, parent_type, field, inner_value, field_ctx)
125
+ field_ctx.value = raw_value.then { |inner_value|
126
+ field_ctx.value = continue_resolve_field(inner_value, field_ctx)
94
127
  }
95
128
  else
96
- continue_resolve_field(owner, selection, parent_type, field, raw_value, field_ctx)
97
- end
98
-
99
- case result
100
- when PROPAGATE_NULL, GraphQL::Execution::Lazy, SelectionResult
101
- FieldResult.new(
102
- owner: owner,
103
- type: field.type,
104
- value: result,
105
- )
106
- else
107
- result
129
+ field_ctx.value = continue_resolve_field(raw_value, field_ctx)
108
130
  end
109
131
  end
110
132
 
111
- def continue_resolve_field(owner, selection, parent_type, field, raw_value, field_ctx)
112
- if owner.invalid_null?
113
- return
133
+ def continue_resolve_field(raw_value, field_ctx)
134
+ if field_ctx.parent.invalid_null?
135
+ return nil
114
136
  end
115
137
  query = field_ctx.query
116
138
 
@@ -131,19 +153,18 @@ module GraphQL
131
153
  end
132
154
 
133
155
  resolve_value(
134
- owner,
135
- parent_type,
136
- field,
137
- field.type,
138
156
  raw_value,
139
- selection,
157
+ field_ctx.type,
140
158
  field_ctx,
141
159
  )
142
160
  end
143
161
 
144
- def resolve_value(owner, parent_type, field_defn, field_type, value, selection, field_ctx)
162
+ def resolve_value(value, field_type, field_ctx)
163
+ field_defn = field_ctx.field
164
+
145
165
  if value.nil?
146
166
  if field_type.kind.non_null?
167
+ parent_type = field_ctx.irep_node.owner_type
147
168
  type_error = GraphQL::InvalidNullError.new(parent_type, field_defn, value)
148
169
  field_ctx.schema.type_error(type_error, field_ctx)
149
170
  PROPAGATE_NULL
@@ -157,55 +178,46 @@ module GraphQL
157
178
  nil
158
179
  end
159
180
  elsif value.is_a?(Skip)
160
- value
181
+ field_ctx.value = value
161
182
  else
162
183
  case field_type.kind
163
- when GraphQL::TypeKinds::SCALAR
164
- field_type.coerce_result(value, field_ctx)
165
- when GraphQL::TypeKinds::ENUM
184
+ when GraphQL::TypeKinds::SCALAR, GraphQL::TypeKinds::ENUM
166
185
  field_type.coerce_result(value, field_ctx)
167
186
  when GraphQL::TypeKinds::LIST
168
187
  inner_type = field_type.of_type
169
188
  i = 0
170
189
  result = []
190
+ field_ctx.value = result
191
+
171
192
  value.each do |inner_value|
172
- inner_ctx = field_ctx.spawn(
193
+ inner_ctx = field_ctx.spawn_child(
173
194
  key: i,
174
- selection: selection,
175
- parent_type: parent_type,
176
- field: field_defn,
195
+ object: inner_value,
196
+ irep_node: field_ctx.irep_node,
177
197
  )
178
198
 
179
199
  inner_result = resolve_value(
180
- owner,
181
- parent_type,
182
- field_defn,
183
- inner_type,
184
200
  inner_value,
185
- selection,
201
+ inner_type,
186
202
  inner_ctx,
187
203
  )
188
204
 
189
- result << GraphQL::Execution::FieldResult.new(type: inner_type, owner: owner, value: inner_result)
205
+ inner_ctx.value = inner_result
206
+ result << inner_ctx
190
207
  i += 1
191
208
  end
192
209
  result
193
210
  when GraphQL::TypeKinds::NON_NULL
194
- wrapped_type = field_type.of_type
211
+ inner_type = field_type.of_type
195
212
  resolve_value(
196
- owner,
197
- parent_type,
198
- field_defn,
199
- wrapped_type,
200
213
  value,
201
- selection,
214
+ inner_type,
202
215
  field_ctx,
203
216
  )
204
217
  when GraphQL::TypeKinds::OBJECT
205
218
  resolve_selection(
206
219
  value,
207
220
  field_type,
208
- selection,
209
221
  field_ctx
210
222
  )
211
223
  when GraphQL::TypeKinds::UNION, GraphQL::TypeKinds::INTERFACE
@@ -214,17 +226,14 @@ module GraphQL
214
226
  possible_types = query.possible_types(field_type)
215
227
 
216
228
  if !possible_types.include?(resolved_type)
229
+ parent_type = field_ctx.irep_node.owner_type
217
230
  type_error = GraphQL::UnresolvedTypeError.new(value, field_defn, parent_type, resolved_type, possible_types)
218
231
  field_ctx.schema.type_error(type_error, field_ctx)
219
232
  PROPAGATE_NULL
220
233
  else
221
234
  resolve_value(
222
- owner,
223
- parent_type,
224
- field_defn,
225
- resolved_type,
226
235
  value,
227
- selection,
236
+ resolved_type,
228
237
  field_ctx,
229
238
  )
230
239
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Execution
4
+ # Starting from a root context,
5
+ # create a hash out of the context tree.
6
+ # @api private
7
+ module Flatten
8
+ def self.call(ctx)
9
+ flatten(ctx)
10
+ end
11
+
12
+ class << self
13
+ private
14
+
15
+ def flatten(obj)
16
+ case obj
17
+ when Hash
18
+ flattened = {}
19
+ obj.each do |key, val|
20
+ flattened[key] = flatten(val)
21
+ end
22
+ flattened
23
+ when Array
24
+ obj.map { |v| flatten(v) }
25
+ when Query::Context::SharedMethods
26
+ if obj.invalid_null?
27
+ nil
28
+ elsif obj.skipped? && obj.value.empty?
29
+ nil
30
+ else
31
+ flatten(obj.value)
32
+ end
33
+ else
34
+ obj
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -36,10 +36,10 @@ module GraphQL
36
36
  Lazy::NullResult
37
37
  else
38
38
  Lazy.new {
39
- acc.each_with_index { |field_result, idx|
40
- inner_v = field_result.value.value
41
- field_result.value = inner_v
42
- acc[idx] = inner_v
39
+ acc.each_with_index { |ctx, idx|
40
+ acc[idx] = GraphQL::Tracing.trace("execute_field_lazy", { context: ctx }) do
41
+ ctx.value.value
42
+ end
43
43
  }
44
44
  resolve_in_place(acc)
45
45
  }
@@ -52,7 +52,7 @@ module GraphQL
52
52
  # @return [void]
53
53
  def self.each_lazy(acc, value)
54
54
  case value
55
- when SelectionResult
55
+ when Hash
56
56
  value.each do |key, field_result|
57
57
  acc = each_lazy(acc, field_result)
58
58
  end
@@ -60,12 +60,12 @@ module GraphQL
60
60
  value.each do |field_result|
61
61
  acc = each_lazy(acc, field_result)
62
62
  end
63
- when FieldResult
63
+ when Query::Context::SharedMethods
64
64
  field_value = value.value
65
65
  case field_value
66
66
  when Lazy
67
67
  acc = acc << value
68
- when SelectionResult
68
+ when Enumerable # shortcut for Hash & Array
69
69
  acc = each_lazy(acc, field_value)
70
70
  end
71
71
  end
@@ -46,19 +46,21 @@ module GraphQL
46
46
  # @param max_complexity [Integer, nil]
47
47
  # @return [Array<Hash>] One result per query
48
48
  def run_queries(schema, queries, context: {}, max_complexity: schema.max_complexity)
49
-
50
- if has_custom_strategy?(schema)
51
- if queries.length != 1
52
- raise ArgumentError, "Multiplexing doesn't support custom execution strategies, run one query at a time instead"
49
+ multiplex = self.new(schema: schema, queries: queries, context: context)
50
+ GraphQL::Tracing.trace("execute_multiplex", { multiplex: multiplex }) do
51
+ if has_custom_strategy?(schema)
52
+ if queries.length != 1
53
+ raise ArgumentError, "Multiplexing doesn't support custom execution strategies, run one query at a time instead"
54
+ else
55
+ with_instrumentation(multiplex, max_complexity: max_complexity) do
56
+ [run_one_legacy(schema, queries.first)]
57
+ end
58
+ end
53
59
  else
54
- with_instrumentation(schema, queries, context: context, max_complexity: max_complexity) do
55
- [run_one_legacy(schema, queries.first)]
60
+ with_instrumentation(multiplex, max_complexity: max_complexity) do
61
+ run_as_multiplex(queries)
56
62
  end
57
63
  end
58
- else
59
- with_instrumentation(schema, queries, context: context, max_complexity: max_complexity) do
60
- run_as_multiplex(queries)
61
- end
62
64
  end
63
65
  end
64
66
 
@@ -71,12 +73,14 @@ module GraphQL
71
73
  end
72
74
 
73
75
  # Then, work through lazy results in a breadth-first way
74
- GraphQL::Execution::Lazy.resolve(results)
76
+ GraphQL::Execution::Execute::ExecutionFunctions.lazy_resolve_root_selection(results, { queries: queries })
75
77
 
76
78
  # Then, find all errors and assign the result to the query object
77
79
  results.each_with_index.map do |data_result, idx|
78
80
  query = queries[idx]
79
81
  finish_query(data_result, query)
82
+ # Get the Query::Result, not the Hash
83
+ query.result
80
84
  end
81
85
  end
82
86
 
@@ -88,18 +92,10 @@ module GraphQL
88
92
  NO_OPERATION
89
93
  else
90
94
  begin
91
- op_type = operation.operation_type
92
- root_type = query.root_type_for_operation(op_type)
93
- GraphQL::Execution::Execute::ExecutionFunctions.resolve_selection(
94
- query.root_value,
95
- root_type,
96
- query.irep_selection,
97
- query.context,
98
- mutation: query.mutation?
99
- )
95
+ GraphQL::Execution::Execute::ExecutionFunctions.resolve_root_selection(query)
100
96
  rescue GraphQL::ExecutionError => err
101
97
  query.context.errors << err
102
- {}
98
+ NO_OPERATION
103
99
  end
104
100
  end
105
101
  end
@@ -109,17 +105,20 @@ module GraphQL
109
105
  # @return [Hash] final result of this query, including all values and errors
110
106
  def finish_query(data_result, query)
111
107
  # Assign the result so that it can be accessed in instrumentation
112
- query.result = if data_result.equal?(NO_OPERATION)
108
+ query.result_values = if data_result.equal?(NO_OPERATION)
113
109
  if !query.valid?
114
110
  { "errors" => query.static_errors.map(&:to_h) }
115
111
  else
116
- {}
112
+ data_result
117
113
  end
118
114
  else
119
- result = { "data" => data_result.to_h }
120
- error_result = query.context.errors.map(&:to_h)
115
+ # Use `context.value` which was assigned during execution
116
+ result = {
117
+ "data" => Execution::Flatten.call(query.context)
118
+ }
121
119
 
122
- if error_result.any?
120
+ if query.context.errors.any?
121
+ error_result = query.context.errors.map(&:to_h)
123
122
  result["errors"] = error_result
124
123
  end
125
124
 
@@ -129,7 +128,7 @@ module GraphQL
129
128
 
130
129
  # use the old `query_execution_strategy` etc to run this query
131
130
  def run_one_legacy(schema, query)
132
- query.result = if !query.valid?
131
+ query.result_values = if !query.valid?
133
132
  all_errors = query.validation_errors + query.analysis_errors + query.context.errors
134
133
  if all_errors.any?
135
134
  { "errors" => all_errors.map(&:to_h) }
@@ -150,10 +149,11 @@ module GraphQL
150
149
  # Apply multiplex & query instrumentation to `queries`.
151
150
  #
152
151
  # It yields when the queries should be executed, then runs teardown.
153
- def with_instrumentation(schema, queries, context:, max_complexity:)
152
+ def with_instrumentation(multiplex, max_complexity:)
153
+ schema = multiplex.schema
154
+ queries = multiplex.queries
154
155
  query_instrumenters = schema.instrumenters[:query]
155
156
  multiplex_instrumenters = schema.instrumenters[:multiplex]
156
- multiplex = self.new(schema: schema, queries: queries, context: context)
157
157
 
158
158
  # First, run multiplex instrumentation, then query instrumentation for each query
159
159
  multiplex_instrumenters.each { |i| i.before_multiplex(multiplex) }