graphql 2.4.5 → 2.5.21

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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  4. data/lib/graphql/analysis/analyzer.rb +2 -1
  5. data/lib/graphql/analysis/query_complexity.rb +87 -7
  6. data/lib/graphql/analysis/visitor.rb +37 -40
  7. data/lib/graphql/analysis.rb +12 -9
  8. data/lib/graphql/autoload.rb +1 -0
  9. data/lib/graphql/backtrace/table.rb +118 -55
  10. data/lib/graphql/backtrace.rb +1 -19
  11. data/lib/graphql/current.rb +6 -1
  12. data/lib/graphql/dashboard/application_controller.rb +41 -0
  13. data/lib/graphql/dashboard/detailed_traces.rb +47 -0
  14. data/lib/graphql/dashboard/installable.rb +22 -0
  15. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  16. data/lib/graphql/dashboard/limiters.rb +93 -0
  17. data/lib/graphql/dashboard/operation_store.rb +199 -0
  18. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
  19. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
  20. data/lib/graphql/dashboard/statics/charts.min.css +1 -0
  21. data/lib/graphql/dashboard/statics/dashboard.css +30 -0
  22. data/lib/graphql/dashboard/statics/dashboard.js +143 -0
  23. data/lib/graphql/dashboard/statics/header-icon.png +0 -0
  24. data/lib/graphql/dashboard/statics/icon.png +0 -0
  25. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  26. data/lib/graphql/dashboard/subscriptions.rb +97 -0
  27. data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
  28. data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
  29. data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
  30. data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
  31. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +24 -0
  32. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
  33. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
  34. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
  35. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
  36. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
  37. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
  38. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
  39. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
  40. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
  41. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
  42. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +108 -0
  43. data/lib/graphql/dashboard.rb +96 -0
  44. data/lib/graphql/dataloader/active_record_association_source.rb +84 -0
  45. data/lib/graphql/dataloader/active_record_source.rb +47 -0
  46. data/lib/graphql/dataloader/async_dataloader.rb +38 -15
  47. data/lib/graphql/dataloader/null_dataloader.rb +55 -10
  48. data/lib/graphql/dataloader/source.rb +18 -6
  49. data/lib/graphql/dataloader.rb +110 -26
  50. data/lib/graphql/date_encoding_error.rb +1 -1
  51. data/lib/graphql/dig.rb +2 -1
  52. data/lib/graphql/execution/interpreter/resolve.rb +10 -16
  53. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +58 -5
  54. data/lib/graphql/execution/interpreter/runtime.rb +229 -93
  55. data/lib/graphql/execution/interpreter.rb +15 -24
  56. data/lib/graphql/execution/multiplex.rb +7 -6
  57. data/lib/graphql/execution/next/field_resolve_step.rb +690 -0
  58. data/lib/graphql/execution/next/load_argument_step.rb +60 -0
  59. data/lib/graphql/execution/next/prepare_object_step.rb +129 -0
  60. data/lib/graphql/execution/next/runner.rb +389 -0
  61. data/lib/graphql/execution/next/selections_step.rb +37 -0
  62. data/lib/graphql/execution/next.rb +69 -0
  63. data/lib/graphql/execution.rb +1 -0
  64. data/lib/graphql/execution_error.rb +13 -10
  65. data/lib/graphql/introspection/directive_location_enum.rb +1 -1
  66. data/lib/graphql/introspection/directive_type.rb +7 -3
  67. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  68. data/lib/graphql/introspection/entry_points.rb +11 -3
  69. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  70. data/lib/graphql/introspection/field_type.rb +13 -5
  71. data/lib/graphql/introspection/input_value_type.rb +21 -13
  72. data/lib/graphql/introspection/type_type.rb +64 -28
  73. data/lib/graphql/invalid_name_error.rb +1 -1
  74. data/lib/graphql/invalid_null_error.rb +25 -16
  75. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  76. data/lib/graphql/language/lexer.rb +16 -5
  77. data/lib/graphql/language/nodes.rb +8 -1
  78. data/lib/graphql/language/parser.rb +16 -8
  79. data/lib/graphql/language/static_visitor.rb +37 -33
  80. data/lib/graphql/language/visitor.rb +59 -55
  81. data/lib/graphql/language.rb +21 -12
  82. data/lib/graphql/pagination/connection.rb +2 -0
  83. data/lib/graphql/pagination/connections.rb +32 -0
  84. data/lib/graphql/query/context.rb +6 -10
  85. data/lib/graphql/query/null_context.rb +9 -3
  86. data/lib/graphql/query/partial.rb +179 -0
  87. data/lib/graphql/query.rb +64 -64
  88. data/lib/graphql/railtie.rb +1 -1
  89. data/lib/graphql/schema/addition.rb +3 -1
  90. data/lib/graphql/schema/always_visible.rb +1 -0
  91. data/lib/graphql/schema/argument.rb +24 -8
  92. data/lib/graphql/schema/build_from_definition.rb +113 -54
  93. data/lib/graphql/schema/directive/flagged.rb +2 -0
  94. data/lib/graphql/schema/directive.rb +52 -2
  95. data/lib/graphql/schema/enum.rb +36 -1
  96. data/lib/graphql/schema/enum_value.rb +1 -1
  97. data/lib/graphql/schema/field/connection_extension.rb +15 -35
  98. data/lib/graphql/schema/field/scope_extension.rb +22 -13
  99. data/lib/graphql/schema/field.rb +101 -51
  100. data/lib/graphql/schema/field_extension.rb +33 -0
  101. data/lib/graphql/schema/input_object.rb +45 -38
  102. data/lib/graphql/schema/interface.rb +2 -1
  103. data/lib/graphql/schema/list.rb +1 -1
  104. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -1
  105. data/lib/graphql/schema/member/has_arguments.rb +56 -19
  106. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  107. data/lib/graphql/schema/member/has_dataloader.rb +79 -0
  108. data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
  109. data/lib/graphql/schema/member/has_directives.rb +1 -1
  110. data/lib/graphql/schema/member/has_fields.rb +81 -5
  111. data/lib/graphql/schema/member/has_interfaces.rb +3 -3
  112. data/lib/graphql/schema/member/scoped.rb +1 -1
  113. data/lib/graphql/schema/member/type_system_helpers.rb +17 -3
  114. data/lib/graphql/schema/member.rb +6 -0
  115. data/lib/graphql/schema/object.rb +18 -8
  116. data/lib/graphql/schema/ractor_shareable.rb +79 -0
  117. data/lib/graphql/schema/resolver.rb +52 -6
  118. data/lib/graphql/schema/scalar.rb +1 -6
  119. data/lib/graphql/schema/subscription.rb +50 -4
  120. data/lib/graphql/schema/timeout.rb +19 -2
  121. data/lib/graphql/schema/validator/required_validator.rb +71 -14
  122. data/lib/graphql/schema/visibility/migration.rb +3 -2
  123. data/lib/graphql/schema/visibility/profile.rb +115 -23
  124. data/lib/graphql/schema/visibility.rb +49 -32
  125. data/lib/graphql/schema/warden.rb +23 -2
  126. data/lib/graphql/schema.rb +333 -68
  127. data/lib/graphql/static_validation/all_rules.rb +2 -2
  128. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
  129. data/lib/graphql/static_validation/rules/fields_will_merge.rb +79 -17
  130. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
  131. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
  132. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
  133. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
  134. data/lib/graphql/static_validation/validator.rb +6 -1
  135. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  136. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
  137. data/lib/graphql/subscriptions/event.rb +12 -1
  138. data/lib/graphql/subscriptions/serialize.rb +1 -1
  139. data/lib/graphql/subscriptions.rb +1 -1
  140. data/lib/graphql/testing/helpers.rb +17 -11
  141. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  142. data/lib/graphql/testing.rb +1 -0
  143. data/lib/graphql/tracing/active_support_notifications_trace.rb +14 -3
  144. data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
  145. data/lib/graphql/tracing/appoptics_trace.rb +9 -1
  146. data/lib/graphql/tracing/appoptics_tracing.rb +7 -0
  147. data/lib/graphql/tracing/appsignal_trace.rb +32 -55
  148. data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
  149. data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
  150. data/lib/graphql/tracing/data_dog_trace.rb +46 -158
  151. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  152. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  153. data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
  154. data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
  155. data/lib/graphql/tracing/detailed_trace.rb +156 -0
  156. data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
  157. data/lib/graphql/tracing/legacy_trace.rb +4 -61
  158. data/lib/graphql/tracing/monitor_trace.rb +283 -0
  159. data/lib/graphql/tracing/new_relic_trace.rb +47 -54
  160. data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
  161. data/lib/graphql/tracing/notifications_trace.rb +184 -34
  162. data/lib/graphql/tracing/notifications_tracing.rb +2 -0
  163. data/lib/graphql/tracing/null_trace.rb +9 -0
  164. data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
  165. data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
  166. data/lib/graphql/tracing/perfetto_trace.rb +864 -0
  167. data/lib/graphql/tracing/platform_trace.rb +5 -0
  168. data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
  169. data/lib/graphql/tracing/prometheus_trace.rb +72 -68
  170. data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
  171. data/lib/graphql/tracing/scout_trace.rb +32 -55
  172. data/lib/graphql/tracing/scout_tracing.rb +2 -0
  173. data/lib/graphql/tracing/sentry_trace.rb +64 -94
  174. data/lib/graphql/tracing/statsd_trace.rb +33 -41
  175. data/lib/graphql/tracing/statsd_tracing.rb +2 -0
  176. data/lib/graphql/tracing/trace.rb +111 -1
  177. data/lib/graphql/tracing.rb +31 -30
  178. data/lib/graphql/type_kinds.rb +1 -0
  179. data/lib/graphql/types/relay/connection_behaviors.rb +9 -7
  180. data/lib/graphql/types/relay/edge_behaviors.rb +5 -4
  181. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  182. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  183. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  184. data/lib/graphql/unauthorized_error.rb +5 -1
  185. data/lib/graphql/version.rb +1 -1
  186. data/lib/graphql.rb +12 -31
  187. metadata +174 -11
  188. data/lib/graphql/backtrace/inspect_result.rb +0 -38
  189. data/lib/graphql/backtrace/trace.rb +0 -93
  190. data/lib/graphql/backtrace/tracer.rb +0 -80
  191. data/lib/graphql/schema/null_mask.rb +0 -11
  192. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
@@ -25,22 +25,56 @@ module GraphQL
25
25
  def validate_field_selections(ast_node, resolved_type)
26
26
  msg = if resolved_type.nil?
27
27
  nil
28
- elsif !ast_node.selections.empty? && resolved_type.kind.leaf?
29
- selection_strs = ast_node.selections.map do |n|
30
- case n
31
- when GraphQL::Language::Nodes::InlineFragment
32
- "\"... on #{n.type.name} { ... }\""
33
- when GraphQL::Language::Nodes::Field
34
- "\"#{n.name}\""
35
- when GraphQL::Language::Nodes::FragmentSpread
36
- "\"#{n.name}\""
28
+ elsif resolved_type.kind.leaf?
29
+ if !ast_node.selections.empty?
30
+ selection_strs = ast_node.selections.map do |n|
31
+ case n
32
+ when GraphQL::Language::Nodes::InlineFragment
33
+ "\"... on #{n.type.name} { ... }\""
34
+ when GraphQL::Language::Nodes::Field
35
+ "\"#{n.name}\""
36
+ when GraphQL::Language::Nodes::FragmentSpread
37
+ "\"#{n.name}\""
38
+ else
39
+ raise "Invariant: unexpected selection node: #{n}"
40
+ end
41
+ end
42
+ "Selections can't be made on #{resolved_type.kind.name.sub("_", " ").downcase}s (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
43
+ else
44
+ nil
45
+ end
46
+ elsif ast_node.selections.empty?
47
+ return_validation_error = true
48
+ legacy_invalid_empty_selection_result = nil
49
+ if !resolved_type.kind.fields?
50
+ case @schema.allow_legacy_invalid_empty_selections_on_union
51
+ when true
52
+ legacy_invalid_empty_selection_result = @schema.legacy_invalid_empty_selections_on_union_with_type(@context.query, resolved_type)
53
+ case legacy_invalid_empty_selection_result
54
+ when :return_validation_error
55
+ # keep `return_validation_error = true`
56
+ when String
57
+ return_validation_error = false
58
+ # the string is returned below
59
+ when nil
60
+ # No error:
61
+ return_validation_error = false
62
+ legacy_invalid_empty_selection_result = nil
63
+ else
64
+ raise GraphQL::InvariantError, "Unexpected return value from legacy_invalid_empty_selections_on_union_with_type, must be `:return_validation_error`, String, or nil (got: #{legacy_invalid_empty_selection_result.inspect})"
65
+ end
66
+ when false
67
+ # pass -- error below
37
68
  else
38
- raise "Invariant: unexpected selection node: #{n}"
69
+ return_validation_error = false
70
+ @context.query.logger.warn("Unions require selections but #{ast_node.alias || ast_node.name} (#{resolved_type.graphql_name}) doesn't have any. This will fail with a validation error on a future GraphQL-Ruby version. More info: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_empty_selections_on_union-class_method")
39
71
  end
40
72
  end
41
- "Selections can't be made on #{resolved_type.kind.name.sub("_", " ").downcase}s (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
42
- elsif resolved_type.kind.fields? && ast_node.selections.empty?
43
- "Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)"
73
+ if return_validation_error
74
+ "Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)"
75
+ else
76
+ legacy_invalid_empty_selection_result
77
+ end
44
78
  else
45
79
  nil
46
80
  end
@@ -33,26 +33,19 @@ module GraphQL
33
33
 
34
34
  private
35
35
 
36
- def field_conflicts
37
- @field_conflicts ||= Hash.new do |errors, field|
38
- errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :field, field_name: field)
39
- end
40
- end
41
-
42
- def arg_conflicts
43
- @arg_conflicts ||= Hash.new do |errors, field|
44
- errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :argument, field_name: field)
36
+ def conflicts
37
+ @conflicts ||= Hash.new do |h, error_type|
38
+ h[error_type] = Hash.new do |h2, field_name|
39
+ h2[field_name] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: error_type, field_name: field_name)
40
+ end
45
41
  end
46
42
  end
47
43
 
48
44
  def setting_errors
49
- @field_conflicts = nil
50
- @arg_conflicts = nil
51
-
45
+ @conflicts = nil
52
46
  yield
53
47
  # don't initialize these if they weren't initialized in the block:
54
- @field_conflicts && @field_conflicts.each_value { |error| add_error(error) }
55
- @arg_conflicts && @arg_conflicts.each_value { |error| add_error(error) }
48
+ @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
56
49
  end
57
50
 
58
51
  def conflicts_within_selection_set(node, parent_type)
@@ -222,7 +215,7 @@ module GraphQL
222
215
 
223
216
  if !are_mutually_exclusive
224
217
  if node1.name != node2.name
225
- conflict = field_conflicts[response_key]
218
+ conflict = conflicts[:field][response_key]
226
219
 
227
220
  conflict.add_conflict(node1, node1.name)
228
221
  conflict.add_conflict(node2, node2.name)
@@ -231,7 +224,7 @@ module GraphQL
231
224
  end
232
225
 
233
226
  if !same_arguments?(node1, node2)
234
- conflict = arg_conflicts[response_key]
227
+ conflict = conflicts[:argument][response_key]
235
228
 
236
229
  conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1)))
237
230
  conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2)))
@@ -240,6 +233,49 @@ module GraphQL
240
233
  end
241
234
  end
242
235
 
236
+ if !conflicts[:field].key?(response_key) &&
237
+ (t1 = field1.definition&.type) &&
238
+ (t2 = field2.definition&.type) &&
239
+ return_types_conflict?(t1, t2)
240
+
241
+ return_error = nil
242
+ message_override = nil
243
+ case @schema.allow_legacy_invalid_return_type_conflicts
244
+ when false
245
+ return_error = true
246
+ when true
247
+ legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2)
248
+ case legacy_handling
249
+ when nil
250
+ return_error = false
251
+ when :return_validation_error
252
+ return_error = true
253
+ when String
254
+ return_error = true
255
+ message_override = legacy_handling
256
+ else
257
+ raise GraphQL::Error, "#{@schema}.legacy_invalid_scalar_conflicts returned unexpected value: #{legacy_handling.inspect}. Expected `nil`, String, or `:return_validation_error`."
258
+ end
259
+ else
260
+ return_error = false
261
+ @context.query.logger.warn <<~WARN
262
+ GraphQL-Ruby encountered mismatched types in this query: `#{t1.to_type_signature}` (at #{node1.line}:#{node1.col}) vs. `#{t2.to_type_signature}` (at #{node2.line}:#{node2.col}).
263
+ This will return an error in future GraphQL-Ruby versions, as per the GraphQL specification
264
+ Learn about migrating here: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_return_type_conflicts-class_method
265
+ WARN
266
+ end
267
+
268
+ if return_error
269
+ conflict = conflicts[:return_type][response_key]
270
+ if message_override
271
+ conflict.message = message_override
272
+ end
273
+ conflict.add_conflict(node1, "`#{t1.to_type_signature}`")
274
+ conflict.add_conflict(node2, "`#{t2.to_type_signature}`")
275
+ @conflict_count += 1
276
+ end
277
+ end
278
+
243
279
  find_conflicts_between_sub_selection_sets(
244
280
  field1,
245
281
  field2,
@@ -247,6 +283,32 @@ module GraphQL
247
283
  )
248
284
  end
249
285
 
286
+ def return_types_conflict?(type1, type2)
287
+ if type1.list?
288
+ if type2.list?
289
+ return_types_conflict?(type1.of_type, type2.of_type)
290
+ else
291
+ true
292
+ end
293
+ elsif type2.list?
294
+ true
295
+ elsif type1.non_null?
296
+ if type2.non_null?
297
+ return_types_conflict?(type1.of_type, type2.of_type)
298
+ else
299
+ true
300
+ end
301
+ elsif type2.non_null?
302
+ true
303
+ elsif type1.kind.leaf? && type2.kind.leaf?
304
+ type1 != type2
305
+ else
306
+ # One or more of these are composite types,
307
+ # their selections will be validated later on.
308
+ false
309
+ end
310
+ end
311
+
250
312
  def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
251
313
  return if field1.definition.nil? ||
252
314
  field2.definition.nil? ||
@@ -345,7 +407,7 @@ module GraphQL
345
407
  fields << Field.new(node, definition, owner_type, parents)
346
408
  when GraphQL::Language::Nodes::InlineFragment
347
409
  fragment_type = node.type ? @types.type(node.type.name) : owner_type
348
- find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: owner_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
410
+ find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
349
411
  when GraphQL::Language::Nodes::FragmentSpread
350
412
  fragment_spreads << FragmentSpread.new(node.name, parents)
351
413
  end
@@ -14,9 +14,11 @@ module GraphQL
14
14
  end
15
15
 
16
16
  def message
17
- "Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?"
17
+ @message || "Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?"
18
18
  end
19
19
 
20
+ attr_writer :message
21
+
20
22
  def path
21
23
  []
22
24
  end
@@ -26,7 +28,13 @@ module GraphQL
26
28
  end
27
29
 
28
30
  def add_conflict(node, conflict_str)
29
- return if nodes.include?(node)
31
+ # Can't use `.include?` here because AST nodes implement `#==`
32
+ # based on string value, not including location. But sometimes,
33
+ # identical nodes conflict because of their differing return types.
34
+ if nodes.any? { |n| n == node && n.line == node.line && n.col == node.col }
35
+ # already have an error for this node
36
+ return
37
+ end
30
38
 
31
39
  @nodes << node
32
40
  @conflicts << conflict_str
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ class NotSingleSubscriptionError < StaticValidation::Error
5
+ def initialize(message, path: nil, nodes: [])
6
+ super(message, path: path, nodes: nodes)
7
+ end
8
+
9
+ # A hash representation of this Message
10
+ def to_h
11
+ extensions = {
12
+ "code" => code,
13
+ }
14
+
15
+ super.merge({
16
+ "extensions" => extensions
17
+ })
18
+ end
19
+
20
+ def code
21
+ "notSingleSubscription"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ module SubscriptionRootExistsAndSingleSubscriptionSelection
5
+ def on_operation_definition(node, parent)
6
+ if node.operation_type == "subscription"
7
+ if context.types.subscription_root.nil?
8
+ add_error(GraphQL::StaticValidation::SubscriptionRootExistsError.new(
9
+ 'Schema is not configured for subscriptions',
10
+ nodes: node
11
+ ))
12
+ elsif node.selections.size != 1
13
+ add_error(GraphQL::StaticValidation::NotSingleSubscriptionError.new(
14
+ 'A subscription operation may only have one selection',
15
+ nodes: node,
16
+ ))
17
+ else
18
+ super
19
+ end
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -19,13 +19,17 @@ module GraphQL
19
19
  :on_field,
20
20
  ]
21
21
 
22
- DIRECTIVE_NODE_HOOKS.each do |method_name|
23
- define_method(method_name) do |node, parent|
22
+ VALIDATE_DIRECTIVE_LOCATION_ON_NODE = <<~RUBY
23
+ def %{method_name}(node, parent)
24
24
  if !node.directives.empty?
25
25
  validate_directive_location(node)
26
26
  end
27
27
  super(node, parent)
28
28
  end
29
+ RUBY
30
+ DIRECTIVE_NODE_HOOKS.each do |method_name|
31
+ # Can't use `define_method {...}` here because the proc can't be isolated for use in non-main Ractors
32
+ module_eval(VALIDATE_DIRECTIVE_LOCATION_ON_NODE % { method_name: method_name }) # rubocop:disable Development/NoEvalCop
29
33
  end
30
34
 
31
35
  private
@@ -27,6 +27,8 @@ module GraphQL
27
27
  # @param max_errors [Integer] Maximum number of errors before aborting validation. Any positive number will limit the number of errors. Defaults to nil for no limit.
28
28
  # @return [Array<Hash>]
29
29
  def validate(query, validate: true, timeout: nil, max_errors: nil)
30
+ errors = nil
31
+ query.current_trace.begin_validate(query, validate)
30
32
  query.current_trace.validate(validate: validate, query: query) do
31
33
  begin_t = Time.now
32
34
  errors = if validate == false
@@ -58,10 +60,13 @@ module GraphQL
58
60
  }
59
61
  end
60
62
  rescue GraphQL::ExecutionError => e
63
+ errors = [e]
61
64
  {
62
65
  remaining_timeout: nil,
63
- errors: [e],
66
+ errors: errors,
64
67
  }
68
+ ensure
69
+ query.current_trace.end_validate(query, validate, errors)
65
70
  end
66
71
 
67
72
  # Invoked when static validation times out.
@@ -81,6 +81,7 @@ module GraphQL
81
81
  # end
82
82
  # end
83
83
  #
84
+ # @see GraphQL::Testing::MockActionCable for test helpers
84
85
  class ActionCableSubscriptions < GraphQL::Subscriptions
85
86
  SUBSCRIPTION_PREFIX = "graphql-subscription:"
86
87
  EVENT_PREFIX = "graphql-event:"
@@ -20,12 +20,22 @@ module GraphQL
20
20
  def after_resolve(value:, context:, object:, arguments:, **rest)
21
21
  if value.is_a?(GraphQL::ExecutionError)
22
22
  value
23
+ elsif @field.resolver&.method_defined?(:subscription_written?) &&
24
+ (subscription_namespace = context.namespace(:subscriptions)) &&
25
+ (subscriptions_by_path = subscription_namespace[:subscriptions])
26
+ (subscription_instance = subscriptions_by_path[context.current_path])
27
+ # If it was already written, don't append this event to be written later
28
+ if !subscription_instance.subscription_written?
29
+ events = context.namespace(:subscriptions)[:events]
30
+ events << subscription_instance.event
31
+ end
32
+ value
23
33
  elsif (events = context.namespace(:subscriptions)[:events])
24
34
  # This is the first execution, so gather an Event
25
35
  # for the backend to register:
26
36
  event = Subscriptions::Event.new(
27
37
  name: field.name,
28
- arguments: arguments_without_field_extras(arguments: arguments),
38
+ arguments: arguments,
29
39
  context: context,
30
40
  field: field,
31
41
  )
@@ -33,7 +43,7 @@ module GraphQL
33
43
  value
34
44
  elsif context.query.subscription_topic == Subscriptions::Event.serialize(
35
45
  field.name,
36
- arguments_without_field_extras(arguments: arguments),
46
+ arguments,
37
47
  field,
38
48
  scope: (field.subscription_scope ? context[field.subscription_scope] : nil),
39
49
  )
@@ -45,14 +55,6 @@ module GraphQL
45
55
  context.skip
46
56
  end
47
57
  end
48
-
49
- private
50
-
51
- def arguments_without_field_extras(arguments:)
52
- arguments.dup.tap do |event_args|
53
- field.extras.each { |k| event_args.delete(k) }
54
- end
55
- end
56
58
  end
57
59
  end
58
60
  end
@@ -20,7 +20,7 @@ module GraphQL
20
20
 
21
21
  def initialize(name:, arguments:, field: nil, context: nil, scope: nil)
22
22
  @name = name
23
- @arguments = arguments
23
+ @arguments = self.class.arguments_without_field_extras(arguments: arguments, field: field)
24
24
  @context = context
25
25
  field ||= context.field
26
26
  scope_key = field.subscription_scope
@@ -39,6 +39,7 @@ module GraphQL
39
39
  # @return [String] an identifier for this unit of subscription
40
40
  def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext.instance)
41
41
  subscription = field.resolver || GraphQL::Schema::Subscription
42
+ arguments = arguments_without_field_extras(field: field, arguments: arguments)
42
43
  normalized_args = stringify_args(field, arguments.to_h, context)
43
44
  subscription.topic_for(arguments: normalized_args, field: field, scope: scope)
44
45
  end
@@ -60,6 +61,16 @@ module GraphQL
60
61
  end
61
62
 
62
63
  class << self
64
+ def arguments_without_field_extras(arguments:, field:)
65
+ if !field.extras.empty?
66
+ arguments = arguments.dup
67
+ field.extras.each do |extra_key|
68
+ arguments.delete(extra_key)
69
+ end
70
+ end
71
+ arguments
72
+ end
73
+
63
74
  private
64
75
 
65
76
  # This method does not support cyclic references in the Hash,
@@ -146,7 +146,7 @@ module GraphQL
146
146
  elsif obj.is_a?(Date) || obj.is_a?(Time)
147
147
  # DateTime extends Date; for TimeWithZone, call `.utc` first.
148
148
  { TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
149
- elsif obj.is_a?(OpenStruct)
149
+ elsif defined?(OpenStruct) && obj.is_a?(OpenStruct)
150
150
  { OPEN_STRUCT_KEY => dump_value(obj.to_h) }
151
151
  elsif defined?(ActiveRecord::Relation) && obj.is_a?(ActiveRecord::Relation)
152
152
  dump_value(obj.to_a)
@@ -80,7 +80,7 @@ module GraphQL
80
80
 
81
81
  # Normalize symbol-keyed args to strings, try camelizing them
82
82
  # Should this accept a real context somehow?
83
- normalized_args = normalize_arguments(normalized_event_name, field, args, GraphQL::Query::NullContext.instance)
83
+ normalized_args = normalize_arguments(normalized_event_name, field, args, @schema.null_context)
84
84
 
85
85
  event = Subscriptions::Event.new(
86
86
  name: normalized_event_name,
@@ -39,22 +39,25 @@ module GraphQL
39
39
  end
40
40
  end
41
41
 
42
- def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, ast_node: nil, lookahead: nil)
42
+ def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, ast_node: nil, lookahead: nil, visibility_profile: nil)
43
43
  type_name, *field_names = field_path.split(".")
44
- dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context)
44
+ dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context, visibility_profile: visibility_profile)
45
45
  query_context = dummy_query.context
46
+ dataloader = query_context.dataloader
46
47
  object_type = dummy_query.types.type(type_name) # rubocop:disable Development/ContextIsPassedCop
47
48
  if object_type
48
49
  graphql_result = object
49
50
  field_names.each do |field_name|
50
51
  inner_object = graphql_result
51
- graphql_result = object_type.wrap(inner_object, query_context)
52
+ dataloader.run_isolated {
53
+ graphql_result = object_type.wrap(inner_object, query_context)
54
+ }
52
55
  if graphql_result.nil?
53
56
  return nil
54
57
  end
55
58
  visible_field = dummy_query.types.field(object_type, field_name) # rubocop:disable Development/ContextIsPassedCop
56
59
  if visible_field
57
- dummy_query.context.dataloader.run_isolated {
60
+ dataloader.run_isolated {
58
61
  query_context[:current_field] = visible_field
59
62
  field_args = visible_field.coerce_arguments(graphql_result, arguments, query_context)
60
63
  field_args = schema.sync_lazy(field_args)
@@ -101,32 +104,35 @@ module GraphQL
101
104
  end
102
105
  end
103
106
 
104
- def with_resolution_context(schema, type:, object:, context:{})
107
+ def with_resolution_context(schema, type:, object:, context:{}, visibility_profile: nil)
105
108
  resolution_context = ResolutionAssertionContext.new(
106
109
  self,
107
110
  schema: schema,
108
111
  type_name: type,
109
112
  object: object,
110
- context: context
113
+ context: context,
114
+ visibility_profile: visibility_profile,
111
115
  )
112
116
  yield(resolution_context)
113
117
  end
114
118
 
115
119
  class ResolutionAssertionContext
116
- def initialize(test, type_name:, object:, schema:, context:)
120
+ def initialize(test, type_name:, object:, schema:, context:, visibility_profile:)
117
121
  @test = test
118
122
  @type_name = type_name
119
123
  @object = object
120
124
  @schema = schema
121
125
  @context = context
126
+ @visibility_profile = visibility_profile
122
127
  end
123
128
 
129
+ attr_reader :visibility_profile
124
130
 
125
131
  def run_graphql_field(field_name, arguments: {})
126
132
  if @schema
127
- @test.run_graphql_field(@schema, "#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context)
133
+ @test.run_graphql_field(@schema, "#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context, visibility_profile: @visibility_profile)
128
134
  else
129
- @test.run_graphql_field("#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context)
135
+ @test.run_graphql_field("#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context, visibility_profile: @visibility_profile)
130
136
  end
131
137
  end
132
138
  end
@@ -134,8 +140,8 @@ module GraphQL
134
140
  module SchemaHelpers
135
141
  include Helpers
136
142
 
137
- def run_graphql_field(field_path, object, arguments: {}, context: {})
138
- super(@@schema_class_for_helpers, field_path, object, arguments: arguments, context: context)
143
+ def run_graphql_field(field_path, object, arguments: {}, context: {}, visibility_profile: nil)
144
+ super(@@schema_class_for_helpers, field_path, object, arguments: arguments, context: context, visibility_profile: visibility_profile)
139
145
  end
140
146
 
141
147
  def with_resolution_context(*args, **kwargs, &block)
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Testing
4
+ # A stub implementation of ActionCable.
5
+ # Any methods to support the mock backend have `mock` in the name.
6
+ #
7
+ # @example Configuring your schema to use MockActionCable in the test environment
8
+ # class MySchema < GraphQL::Schema
9
+ # # Use MockActionCable in test:
10
+ # use GraphQL::Subscriptions::ActionCableSubscriptions,
11
+ # action_cable: Rails.env.test? ? GraphQL::Testing::MockActionCable : ActionCable
12
+ # end
13
+ #
14
+ # @example Clearing old data before each test
15
+ # setup do
16
+ # GraphQL::Testing::MockActionCable.clear_mocks
17
+ # end
18
+ #
19
+ # @example Using MockActionCable in a test case
20
+ # # Create a channel to use in the test, pass it to GraphQL
21
+ # mock_channel = GraphQL::Testing::MockActionCable.get_mock_channel
22
+ # ActionCableTestSchema.execute("subscription { newsFlash { text } }", context: { channel: mock_channel })
23
+ #
24
+ # # Trigger a subscription update
25
+ # ActionCableTestSchema.subscriptions.trigger(:news_flash, {}, {text: "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"})
26
+ #
27
+ # # Check messages on the channel
28
+ # expected_msg = {
29
+ # result: {
30
+ # "data" => {
31
+ # "newsFlash" => {
32
+ # "text" => "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"
33
+ # }
34
+ # }
35
+ # },
36
+ # more: true,
37
+ # }
38
+ # assert_equal [expected_msg], mock_channel.mock_broadcasted_messages
39
+ #
40
+ class MockActionCable
41
+ class MockChannel
42
+ def initialize
43
+ @mock_broadcasted_messages = []
44
+ end
45
+
46
+ # @return [Array<Hash>] Payloads "sent" to this channel by GraphQL-Ruby
47
+ attr_reader :mock_broadcasted_messages
48
+
49
+ # Called by ActionCableSubscriptions. Implements a Rails API.
50
+ def stream_from(stream_name, coder: nil, &block)
51
+ # Rails uses `coder`, we don't
52
+ block ||= ->(msg) { @mock_broadcasted_messages << msg }
53
+ MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block)
54
+ end
55
+ end
56
+
57
+ # Used by mock code
58
+ # @api private
59
+ class MockStream
60
+ def initialize
61
+ @mock_channels = {}
62
+ end
63
+
64
+ def add_mock_channel(channel, handler)
65
+ @mock_channels[channel] = handler
66
+ end
67
+
68
+ def mock_broadcast(message)
69
+ @mock_channels.each do |channel, handler|
70
+ handler && handler.call(message)
71
+ end
72
+ end
73
+ end
74
+
75
+ class << self
76
+ # Call this before each test run to make sure that MockActionCable's data is empty
77
+ def clear_mocks
78
+ @mock_streams = {}
79
+ end
80
+
81
+ # Implements Rails API
82
+ def server
83
+ self
84
+ end
85
+
86
+ # Implements Rails API
87
+ def broadcast(stream_name, message)
88
+ stream = @mock_streams[stream_name]
89
+ stream && stream.mock_broadcast(message)
90
+ end
91
+
92
+ # Used by mock code
93
+ def mock_stream_for(stream_name)
94
+ @mock_streams[stream_name] ||= MockStream.new
95
+ end
96
+
97
+ # Use this as `context[:channel]` to simulate an ActionCable channel
98
+ #
99
+ # @return [GraphQL::Testing::MockActionCable::MockChannel]
100
+ def get_mock_channel
101
+ MockChannel.new
102
+ end
103
+
104
+ # @return [Array<String>] Streams that currently have subscribers
105
+ def mock_stream_names
106
+ @mock_streams.keys
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -1,2 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/testing/helpers"
3
+ require "graphql/testing/mock_action_cable"