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
data/lib/graphql/field.rb CHANGED
@@ -129,6 +129,7 @@ module GraphQL
129
129
  :edge_class,
130
130
  :relay_node_field,
131
131
  :relay_nodes_field,
132
+ :subscription_scope,
132
133
  argument: GraphQL::Define::AssignArgument
133
134
 
134
135
  ensure_defined(
@@ -136,7 +137,7 @@ module GraphQL
136
137
  :mutation, :arguments, :complexity, :function,
137
138
  :resolve, :resolve=, :lazy_resolve, :lazy_resolve=, :lazy_resolve_proc, :resolve_proc,
138
139
  :type, :type=, :name=, :property=, :hash_key=,
139
- :relay_node_field, :relay_nodes_field, :edges?, :edge_class
140
+ :relay_node_field, :relay_nodes_field, :edges?, :edge_class, :subscription_scope
140
141
  )
141
142
 
142
143
  # @return [Boolean] True if this is the Relay find-by-id field
@@ -180,6 +181,9 @@ module GraphQL
180
181
 
181
182
  attr_writer :connection
182
183
 
184
+ # @return [nil, String] Prefix for subscription names from this field
185
+ attr_accessor :subscription_scope
186
+
183
187
  # @return [Boolean]
184
188
  def connection?
185
189
  @connection
@@ -145,6 +145,22 @@ module GraphQL
145
145
  # @return [GraphQL::Query]
146
146
  attr_reader :query
147
147
 
148
+ def subscription_topic
149
+ @subscription_topic ||= begin
150
+ scope = if definition.subscription_scope
151
+ @query.context[definition.subscription_scope]
152
+ else
153
+ nil
154
+ end
155
+ Subscriptions::Event.serialize(
156
+ definition_name,
157
+ @query.arguments_for(self, definition),
158
+ definition,
159
+ scope: scope
160
+ )
161
+ end
162
+ end
163
+
148
164
  protected
149
165
 
150
166
  attr_writer :owner_type, :parent
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class InvalidNameError < GraphQL::ExecutionError
4
+ attr_reader :name, :valid_regex
5
+ def initialize(name, valid_regex)
6
+ @name = name
7
+ @valid_regex = valid_regex
8
+ super("Names must match #{@valid_regex.inspect} but '#{@name}' does not")
9
+ end
10
+ end
11
+ end
@@ -21,11 +21,17 @@ end
21
21
 
22
22
  def parse_document
23
23
  @document ||= begin
24
- @tokens ||= GraphQL.scan(@query_string)
25
- if @tokens.none?
26
- make_node(:Document, definitions: [], filename: @filename)
27
- else
28
- do_parse
24
+ # Break the string into tokens
25
+ GraphQL::Tracing.trace("lex", {query_string: @query_string}) do
26
+ @tokens ||= GraphQL.scan(@query_string)
27
+ end
28
+ # From the tokens, build an AST
29
+ GraphQL::Tracing.trace("parse", {query_string: @query_string}) do
30
+ if @tokens.none?
31
+ make_node(:Document, definitions: [], filename: @filename)
32
+ else
33
+ do_parse
34
+ end
29
35
  end
30
36
  end
31
37
  end
@@ -361,11 +361,17 @@ end
361
361
 
362
362
  def parse_document
363
363
  @document ||= begin
364
- @tokens ||= GraphQL.scan(@query_string)
365
- if @tokens.none?
366
- make_node(:Document, definitions: [], filename: @filename)
367
- else
368
- do_parse
364
+ # Break the string into tokens
365
+ GraphQL::Tracing.trace("lex", {query_string: @query_string}) do
366
+ @tokens ||= GraphQL.scan(@query_string)
367
+ end
368
+ # From the tokens, build an AST
369
+ GraphQL::Tracing.trace("parse", {query_string: @query_string}) do
370
+ if @tokens.none?
371
+ make_node(:Document, definitions: [], filename: @filename)
372
+ else
373
+ do_parse
374
+ end
369
375
  end
370
376
  end
371
377
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class NameValidator
4
+ VALID_NAME_REGEX = /^[_a-zA-Z][_a-zA-Z0-9]*$/
5
+
6
+ def self.validate!(name)
7
+ raise GraphQL::InvalidNameError.new(name, VALID_NAME_REGEX) unless valid?(name)
8
+ end
9
+
10
+ private
11
+
12
+ def self.valid?(name)
13
+ name =~ VALID_NAME_REGEX
14
+ end
15
+ end
16
+ end
@@ -102,6 +102,11 @@ module GraphQL
102
102
  dirty_ifaces.concat(interfaces)
103
103
  end
104
104
 
105
+ def name=(name)
106
+ GraphQL::NameValidator.validate!(name)
107
+ @name = name
108
+ end
109
+
105
110
  protected
106
111
 
107
112
  attr_reader :dirty_interfaces, :dirty_inherited_interfaces
data/lib/graphql/query.rb CHANGED
@@ -5,6 +5,7 @@ require "graphql/query/context"
5
5
  require "graphql/query/executor"
6
6
  require "graphql/query/literal_input"
7
7
  require "graphql/query/null_context"
8
+ require "graphql/query/result"
8
9
  require "graphql/query/serial_execution"
9
10
  require "graphql/query/variables"
10
11
  require "graphql/query/input_validation_result"
@@ -48,6 +49,12 @@ module GraphQL
48
49
  selected_operation.name
49
50
  end
50
51
 
52
+ # @return [String, nil] the triggered event, if this query is a subscription update
53
+ attr_reader :subscription_topic
54
+
55
+ # @return [String, nil]
56
+ attr_reader :operation_name
57
+
51
58
  # Prepare query `query_string` on `schema`
52
59
  # @param schema [GraphQL::Schema]
53
60
  # @param query_string [String]
@@ -59,10 +66,11 @@ module GraphQL
59
66
  # @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value)
60
67
  # @param except [<#call(schema_member, context)>] If provided, objects will be hidden from the schema when `.call(schema_member, context)` returns truthy
61
68
  # @param only [<#call(schema_member, context)>] If provided, objects will be hidden from the schema when `.call(schema_member, context)` returns false
62
- 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)
69
+ def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: {}, validate: true, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: nil, max_complexity: nil, except: nil, only: nil)
63
70
  @schema = schema
64
71
  @filter = schema.default_filter.merge(except: except, only: only)
65
- @context = Context.new(query: self, values: context)
72
+ @context = Context.new(query: self, object: root_value, values: context)
73
+ @subscription_topic = subscription_topic
66
74
  @root_value = root_value
67
75
  @fragments = nil
68
76
  @operations = nil
@@ -78,6 +86,10 @@ module GraphQL
78
86
  @query_string = query_string || query
79
87
  @document = document
80
88
 
89
+ if @query_string && @document
90
+ raise ArgumentError, "Query should only be provided a query string or a document, not both."
91
+ end
92
+
81
93
  # A two-layer cache of type resolution:
82
94
  # { abstract_type => { value => resolved_type } }
83
95
  @resolved_types_cache = Hash.new do |h1, k1|
@@ -94,22 +106,25 @@ module GraphQL
94
106
  @mutation = false
95
107
  @operation_name = operation_name
96
108
  @prepared_ast = false
97
-
98
109
  @validation_pipeline = nil
99
110
  @max_depth = max_depth || schema.max_depth
100
111
  @max_complexity = max_complexity || schema.max_complexity
101
112
 
102
- @result = nil
113
+ @result_values = nil
103
114
  @executed = false
104
115
  end
105
116
 
117
+ def subscription_update?
118
+ @subscription_topic && subscription?
119
+ end
120
+
106
121
  # @api private
107
- def result=(result_hash)
122
+ def result_values=(result_hash)
108
123
  if @executed
109
124
  raise "Invariant: Can't reassign result"
110
125
  else
111
126
  @executed = true
112
- @result = result_hash
127
+ @result_values = result_hash
113
128
  end
114
129
  end
115
130
 
@@ -129,7 +144,7 @@ module GraphQL
129
144
  Execution::Multiplex.run_queries(@schema, [self])
130
145
  }
131
146
  end
132
- @result
147
+ @result ||= Query::Result.new(query: self, values: @result_values)
133
148
  end
134
149
 
135
150
  def static_errors
@@ -228,6 +243,10 @@ module GraphQL
228
243
  nil
229
244
  end
230
245
 
246
+ def subscription?
247
+ with_prepared_ast { @subscription }
248
+ end
249
+
231
250
  private
232
251
 
233
252
  def find_operation(operations, operation_name)
@@ -277,6 +296,7 @@ module GraphQL
277
296
  # with no operations returns an empty hash
278
297
  @ast_variables = []
279
298
  @mutation = false
299
+ @subscription = false
280
300
  operation_name_error = nil
281
301
  if @operations.any?
282
302
  @selected_operation = find_operation(@operations, @operation_name)
@@ -289,6 +309,7 @@ module GraphQL
289
309
  @ast_variables = @selected_operation.variables
290
310
  @mutation = @selected_operation.operation_type == "mutation"
291
311
  @query = @selected_operation.operation_type == "query"
312
+ @subscription = @selected_operation.operation_type == "subscription"
292
313
  end
293
314
  end
294
315
 
@@ -1,10 +1,80 @@
1
1
  # frozen_string_literal: true
2
+ # test_via: ../execution/execute.rb
3
+ # test_via: ../execution/lazy.rb
2
4
  module GraphQL
3
5
  class Query
4
6
  # Expose some query-specific info to field resolve functions.
5
7
  # It delegates `[]` to the hash that's passed to `GraphQL::Query#initialize`.
6
8
  class Context
9
+ module SharedMethods
10
+ # @return [Object] The target for field resultion
11
+ attr_accessor :object
12
+
13
+ # @return [Hash, Array, String, Integer, Float, Boolean, nil] The resolved value for this field
14
+ attr_reader :value
15
+
16
+ # @return [Boolean] were any fields of this selection skipped?
17
+ attr_reader :skipped
18
+ alias :skipped? :skipped
19
+
20
+ # @api private
21
+ attr_writer :skipped
22
+
23
+ # Return this value to tell the runtime
24
+ # to exclude this field from the response altogether
25
+ def skip
26
+ GraphQL::Execution::Execute::SKIP
27
+ end
28
+
29
+ # @return [Boolean] True if this selection has been nullified by a null child
30
+ def invalid_null?
31
+ @invalid_null
32
+ end
33
+
34
+ # Remove this child from the result value
35
+ # (used for null propagation and skip)
36
+ # @api private
37
+ def delete(child_ctx)
38
+ @value.delete(child_ctx.key)
39
+ end
40
+
41
+ # Create a child context to use for `key`
42
+ # @param key [String, Integer] The key in the response (name or index)
43
+ # @param irep_node [InternalRepresentation::Node] The node being evaluated
44
+ # @api private
45
+ def spawn_child(key:, irep_node:, object:)
46
+ FieldResolutionContext.new(
47
+ context: @context,
48
+ parent: self,
49
+ object: object,
50
+ key: key,
51
+ irep_node: irep_node,
52
+ )
53
+ end
54
+
55
+ # Add error at query-level.
56
+ # @param error [GraphQL::ExecutionError] an execution error
57
+ # @return [void]
58
+ def add_error(error)
59
+ if !error.is_a?(ExecutionError)
60
+ raise TypeError, "expected error to be a ExecutionError, but was #{error.class}"
61
+ end
62
+ errors << error
63
+ nil
64
+ end
65
+
66
+ # @example Print the GraphQL backtrace during field resolution
67
+ # puts ctx.backtrace
68
+ #
69
+ # @return [GraphQL::Backtrace] The backtrace for this point in query execution
70
+ def backtrace
71
+ GraphQL::Backtrace.new(self)
72
+ end
73
+ end
74
+
75
+ include SharedMethods
7
76
  extend GraphQL::Delegate
77
+
8
78
  attr_reader :execution_strategy
9
79
  # `strategy` is required by GraphQL::Batch
10
80
  alias_method :strategy, :execution_strategy
@@ -17,7 +87,9 @@ module GraphQL
17
87
  end
18
88
 
19
89
  # @return [GraphQL::InternalRepresentation::Node] The internal representation for this query node
20
- attr_accessor :irep_node
90
+ def irep_node
91
+ @irep_node ||= query.irep_selection
92
+ end
21
93
 
22
94
  # @return [GraphQL::Language::Nodes::Field] The AST node for the currently-executing field
23
95
  def ast_node
@@ -39,17 +111,23 @@ module GraphQL
39
111
  # Make a new context which delegates key lookup to `values`
40
112
  # @param query [GraphQL::Query] the query who owns this context
41
113
  # @param values [Hash] A hash of arbitrary values which will be accessible at query-time
42
- def initialize(query:, values:)
114
+ def initialize(query:, values: , object:)
43
115
  @query = query
44
116
  @schema = query.schema
45
117
  @provided_values = values || {}
118
+ @object = object
46
119
  # Namespaced storage, where user-provided values are in `nil` namespace:
47
120
  @storage = Hash.new { |h, k| h[k] = {} }
48
121
  @storage[nil] = @provided_values
49
122
  @errors = []
50
123
  @path = []
124
+ @value = nil
125
+ @context = self # for SharedMethods
51
126
  end
52
127
 
128
+ # @api private
129
+ attr_writer :value
130
+
53
131
  def_delegators :@provided_values, :[], :[]=, :to_h, :key?, :fetch
54
132
 
55
133
  # @!method [](key)
@@ -58,6 +136,7 @@ module GraphQL
58
136
  # @!method []=(key, value)
59
137
  # Reassign `key` to the hash passed to {Schema#execute} as `context:`
60
138
 
139
+
61
140
  # @return [GraphQL::Schema::Warden]
62
141
  def warden
63
142
  @warden ||= @query.warden
@@ -70,46 +149,32 @@ module GraphQL
70
149
  @storage[ns]
71
150
  end
72
151
 
73
- def spawn(key:, selection:, parent_type:, field:)
74
- FieldResolutionContext.new(
75
- context: self,
76
- parent: self,
77
- key: key,
78
- selection: selection,
79
- parent_type: parent_type,
80
- field: field,
81
- )
152
+ def inspect
153
+ "#<Query::Context ...>"
82
154
  end
83
155
 
84
- # Return this value to tell the runtime
85
- # to exclude this field from the response altogether
86
- def skip
87
- GraphQL::Execution::Execute::SKIP
88
- end
89
-
90
- # Add error at query-level.
91
- # @param error [GraphQL::ExecutionError] an execution error
92
- # @return [void]
93
- def add_error(error)
94
- if !error.is_a?(ExecutionError)
95
- raise TypeError, "expected error to be a ExecutionError, but was #{error.class}"
96
- end
97
- errors << error
98
- nil
156
+ # @api private
157
+ def received_null_child
158
+ @invalid_null = true
159
+ @value = nil
99
160
  end
100
161
 
101
162
  class FieldResolutionContext
163
+ include SharedMethods
102
164
  extend GraphQL::Delegate
103
165
 
104
- attr_reader :selection, :field, :parent_type, :query, :schema
166
+ attr_reader :irep_node, :field, :parent_type, :query, :schema, :parent, :key, :type
167
+ alias :selection :irep_node
105
168
 
106
- def initialize(context:, key:, selection:, parent:, field:, parent_type:)
169
+ def initialize(context:, key:, irep_node:, parent:, object:)
107
170
  @context = context
108
171
  @key = key
109
172
  @parent = parent
110
- @selection = selection
111
- @field = field
112
- @parent_type = parent_type
173
+ @object = object
174
+ @irep_node = irep_node
175
+ @field = irep_node.definition
176
+ @parent_type = irep_node.owner_type
177
+ @type = field.type
113
178
  # This is needed constantly, so set it ahead of time:
114
179
  @query = context.query
115
180
  @schema = context.schema
@@ -122,41 +187,79 @@ module GraphQL
122
187
  def_delegators :@context,
123
188
  :[], :[]=, :key?, :fetch, :to_h, :namespace,
124
189
  :spawn, :schema, :warden, :errors,
125
- :execution_strategy, :strategy, :skip
190
+ :execution_strategy, :strategy
126
191
 
127
192
  # @return [GraphQL::Language::Nodes::Field] The AST node for the currently-executing field
128
193
  def ast_node
129
- @selection.ast_node
130
- end
131
-
132
- # @return [GraphQL::InternalRepresentation::Node]
133
- def irep_node
134
- @selection
194
+ @irep_node.ast_node
135
195
  end
136
196
 
137
197
  # Add error to current field resolution.
138
198
  # @param error [GraphQL::ExecutionError] an execution error
139
199
  # @return [void]
140
200
  def add_error(error)
141
- if !error.is_a?(ExecutionError)
142
- raise TypeError, "expected error to be a ExecutionError, but was #{error.class}"
143
- end
144
-
201
+ super
145
202
  error.ast_node ||= irep_node.ast_node
146
203
  error.path ||= path
147
- errors << error
148
204
  nil
149
205
  end
150
206
 
151
- def spawn(key:, selection:, parent_type:, field:)
152
- FieldResolutionContext.new(
153
- context: @context,
154
- parent: self,
155
- key: key,
156
- selection: selection,
157
- parent_type: parent_type,
158
- field: field,
159
- )
207
+ def inspect
208
+ "#<GraphQL Context @ #{irep_node.owner_type.name}.#{field.name}>"
209
+ end
210
+
211
+ # Set a new value for this field in the response.
212
+ # It may be updated after resolving a {Lazy}.
213
+ # If it is {Execute::PROPAGATE_NULL}, tell the owner to propagate null.
214
+ # If it's {Execute::Execution::SKIP}, remove this field result from its parent
215
+ # @param new_value [Any] The GraphQL-ready value
216
+ # @api private
217
+ def value=(new_value)
218
+ case new_value
219
+ when GraphQL::Execution::Execute::PROPAGATE_NULL, nil
220
+ @invalid_null = true
221
+ @value = nil
222
+ if @type.kind.non_null?
223
+ @parent.received_null_child
224
+ end
225
+ when GraphQL::Execution::Execute::SKIP
226
+ @parent.skipped = true
227
+ @parent.delete(self)
228
+ else
229
+ @value = new_value
230
+ end
231
+ end
232
+
233
+ protected
234
+
235
+ def received_null_child
236
+ case @value
237
+ when Hash
238
+ self.value = GraphQL::Execution::Execute::PROPAGATE_NULL
239
+ when Array
240
+ if list_of_non_null_items?(@type)
241
+ self.value = GraphQL::Execution::Execute::PROPAGATE_NULL
242
+ end
243
+ when nil
244
+ # TODO This is a hack
245
+ # It was already nulled out but it's getting reassigned
246
+ else
247
+ raise "Unexpected value for received_null_child (#{self.value.class}): #{value}"
248
+ end
249
+ end
250
+
251
+ private
252
+
253
+ def list_of_non_null_items?(type)
254
+ case type
255
+ when GraphQL::NonNullType
256
+ # Unwrap [T]!
257
+ list_of_non_null_items?(type.of_type)
258
+ when GraphQL::ListType
259
+ type.of_type.is_a?(GraphQL::NonNullType)
260
+ else
261
+ raise "Unexpected list_of_non_null_items check: #{type}"
262
+ end
160
263
  end
161
264
  end
162
265
  end