graphql 1.6.8 → 1.7.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 (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) }