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