graphql 1.9.17 → 1.11.7

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

Files changed (230) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +18 -2
  3. data/lib/generators/graphql/install_generator.rb +27 -0
  4. data/lib/generators/graphql/object_generator.rb +52 -8
  5. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  6. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  7. data/lib/generators/graphql/templates/base_field.erb +2 -0
  8. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  9. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  10. data/lib/generators/graphql/templates/base_mutation.erb +2 -0
  11. data/lib/generators/graphql/templates/base_object.erb +2 -0
  12. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  13. data/lib/generators/graphql/templates/base_union.erb +2 -0
  14. data/lib/generators/graphql/templates/enum.erb +2 -0
  15. data/lib/generators/graphql/templates/graphql_controller.erb +14 -10
  16. data/lib/generators/graphql/templates/interface.erb +2 -0
  17. data/lib/generators/graphql/templates/loader.erb +2 -0
  18. data/lib/generators/graphql/templates/mutation.erb +2 -0
  19. data/lib/generators/graphql/templates/mutation_type.erb +2 -0
  20. data/lib/generators/graphql/templates/object.erb +2 -0
  21. data/lib/generators/graphql/templates/query_type.erb +2 -0
  22. data/lib/generators/graphql/templates/scalar.erb +2 -0
  23. data/lib/generators/graphql/templates/schema.erb +10 -0
  24. data/lib/generators/graphql/templates/union.erb +3 -1
  25. data/lib/graphql/analysis/ast/field_usage.rb +1 -1
  26. data/lib/graphql/analysis/ast/query_complexity.rb +178 -67
  27. data/lib/graphql/analysis/ast/visitor.rb +3 -3
  28. data/lib/graphql/analysis/ast.rb +12 -11
  29. data/lib/graphql/argument.rb +10 -38
  30. data/lib/graphql/backtrace/table.rb +10 -2
  31. data/lib/graphql/backtrace/tracer.rb +2 -1
  32. data/lib/graphql/base_type.rb +4 -0
  33. data/lib/graphql/compatibility/execution_specification/specification_schema.rb +2 -2
  34. data/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb +5 -9
  35. data/lib/graphql/define/assign_enum_value.rb +1 -1
  36. data/lib/graphql/define/assign_global_id_field.rb +2 -2
  37. data/lib/graphql/define/assign_object_field.rb +3 -3
  38. data/lib/graphql/define/defined_object_proxy.rb +3 -0
  39. data/lib/graphql/define/instance_definable.rb +18 -108
  40. data/lib/graphql/directive/deprecated_directive.rb +1 -12
  41. data/lib/graphql/directive.rb +8 -1
  42. data/lib/graphql/enum_type.rb +5 -71
  43. data/lib/graphql/execution/directive_checks.rb +2 -2
  44. data/lib/graphql/execution/errors.rb +2 -3
  45. data/lib/graphql/execution/execute.rb +1 -1
  46. data/lib/graphql/execution/instrumentation.rb +1 -1
  47. data/lib/graphql/execution/interpreter/argument_value.rb +28 -0
  48. data/lib/graphql/execution/interpreter/arguments.rb +51 -0
  49. data/lib/graphql/execution/interpreter/arguments_cache.rb +79 -0
  50. data/lib/graphql/execution/interpreter/handles_raw_value.rb +25 -0
  51. data/lib/graphql/execution/interpreter/runtime.rb +227 -254
  52. data/lib/graphql/execution/interpreter.rb +34 -11
  53. data/lib/graphql/execution/lazy/lazy_method_map.rb +4 -0
  54. data/lib/graphql/execution/lookahead.rb +39 -114
  55. data/lib/graphql/execution/multiplex.rb +14 -5
  56. data/lib/graphql/field.rb +14 -118
  57. data/lib/graphql/filter.rb +1 -1
  58. data/lib/graphql/function.rb +1 -30
  59. data/lib/graphql/input_object_type.rb +6 -24
  60. data/lib/graphql/integer_decoding_error.rb +17 -0
  61. data/lib/graphql/interface_type.rb +7 -23
  62. data/lib/graphql/internal_representation/scope.rb +2 -2
  63. data/lib/graphql/internal_representation/visit.rb +2 -2
  64. data/lib/graphql/introspection/base_object.rb +2 -5
  65. data/lib/graphql/introspection/directive_type.rb +1 -1
  66. data/lib/graphql/introspection/entry_points.rb +7 -7
  67. data/lib/graphql/introspection/field_type.rb +7 -3
  68. data/lib/graphql/introspection/input_value_type.rb +33 -9
  69. data/lib/graphql/introspection/introspection_query.rb +6 -92
  70. data/lib/graphql/introspection/schema_type.rb +4 -9
  71. data/lib/graphql/introspection/type_type.rb +11 -7
  72. data/lib/graphql/introspection.rb +96 -0
  73. data/lib/graphql/invalid_null_error.rb +18 -0
  74. data/lib/graphql/language/block_string.rb +24 -5
  75. data/lib/graphql/language/definition_slice.rb +21 -10
  76. data/lib/graphql/language/document_from_schema_definition.rb +89 -64
  77. data/lib/graphql/language/lexer.rb +7 -3
  78. data/lib/graphql/language/lexer.rl +7 -3
  79. data/lib/graphql/language/nodes.rb +52 -91
  80. data/lib/graphql/language/parser.rb +719 -717
  81. data/lib/graphql/language/parser.y +104 -98
  82. data/lib/graphql/language/printer.rb +1 -1
  83. data/lib/graphql/language/sanitized_printer.rb +222 -0
  84. data/lib/graphql/language/visitor.rb +2 -2
  85. data/lib/graphql/language.rb +2 -1
  86. data/lib/graphql/name_validator.rb +6 -7
  87. data/lib/graphql/non_null_type.rb +0 -10
  88. data/lib/graphql/object_type.rb +45 -56
  89. data/lib/graphql/pagination/active_record_relation_connection.rb +41 -0
  90. data/lib/graphql/pagination/array_connection.rb +77 -0
  91. data/lib/graphql/pagination/connection.rb +208 -0
  92. data/lib/graphql/pagination/connections.rb +145 -0
  93. data/lib/graphql/pagination/mongoid_relation_connection.rb +25 -0
  94. data/lib/graphql/pagination/relation_connection.rb +185 -0
  95. data/lib/graphql/pagination/sequel_dataset_connection.rb +28 -0
  96. data/lib/graphql/pagination.rb +6 -0
  97. data/lib/graphql/query/arguments.rb +4 -2
  98. data/lib/graphql/query/context.rb +36 -9
  99. data/lib/graphql/query/fingerprint.rb +26 -0
  100. data/lib/graphql/query/input_validation_result.rb +23 -6
  101. data/lib/graphql/query/literal_input.rb +30 -10
  102. data/lib/graphql/query/null_context.rb +5 -1
  103. data/lib/graphql/query/validation_pipeline.rb +4 -1
  104. data/lib/graphql/query/variable_validation_error.rb +1 -1
  105. data/lib/graphql/query/variables.rb +16 -7
  106. data/lib/graphql/query.rb +64 -15
  107. data/lib/graphql/rake_task/validate.rb +3 -0
  108. data/lib/graphql/rake_task.rb +9 -9
  109. data/lib/graphql/relay/array_connection.rb +10 -12
  110. data/lib/graphql/relay/base_connection.rb +23 -13
  111. data/lib/graphql/relay/connection_type.rb +2 -1
  112. data/lib/graphql/relay/edge_type.rb +1 -0
  113. data/lib/graphql/relay/edges_instrumentation.rb +1 -1
  114. data/lib/graphql/relay/mutation.rb +1 -86
  115. data/lib/graphql/relay/node.rb +2 -2
  116. data/lib/graphql/relay/range_add.rb +14 -5
  117. data/lib/graphql/relay/relation_connection.rb +8 -10
  118. data/lib/graphql/scalar_type.rb +15 -59
  119. data/lib/graphql/schema/argument.rb +113 -11
  120. data/lib/graphql/schema/base_64_encoder.rb +2 -0
  121. data/lib/graphql/schema/build_from_definition/resolve_map/default_resolve.rb +1 -1
  122. data/lib/graphql/schema/build_from_definition/resolve_map.rb +13 -5
  123. data/lib/graphql/schema/build_from_definition.rb +212 -190
  124. data/lib/graphql/schema/built_in_types.rb +5 -5
  125. data/lib/graphql/schema/default_type_error.rb +2 -0
  126. data/lib/graphql/schema/directive/deprecated.rb +18 -0
  127. data/lib/graphql/schema/directive/include.rb +1 -1
  128. data/lib/graphql/schema/directive/skip.rb +1 -1
  129. data/lib/graphql/schema/directive.rb +34 -3
  130. data/lib/graphql/schema/enum.rb +52 -4
  131. data/lib/graphql/schema/enum_value.rb +6 -1
  132. data/lib/graphql/schema/field/connection_extension.rb +44 -20
  133. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  134. data/lib/graphql/schema/field.rb +200 -129
  135. data/lib/graphql/schema/find_inherited_value.rb +13 -0
  136. data/lib/graphql/schema/finder.rb +13 -11
  137. data/lib/graphql/schema/input_object.rb +131 -22
  138. data/lib/graphql/schema/interface.rb +26 -8
  139. data/lib/graphql/schema/introspection_system.rb +108 -37
  140. data/lib/graphql/schema/late_bound_type.rb +3 -2
  141. data/lib/graphql/schema/list.rb +47 -0
  142. data/lib/graphql/schema/loader.rb +134 -96
  143. data/lib/graphql/schema/member/base_dsl_methods.rb +29 -12
  144. data/lib/graphql/schema/member/build_type.rb +19 -5
  145. data/lib/graphql/schema/member/cached_graphql_definition.rb +5 -0
  146. data/lib/graphql/schema/member/has_arguments.rb +105 -5
  147. data/lib/graphql/schema/member/has_ast_node.rb +20 -0
  148. data/lib/graphql/schema/member/has_fields.rb +20 -10
  149. data/lib/graphql/schema/member/has_unresolved_type_error.rb +15 -0
  150. data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
  151. data/lib/graphql/schema/member/validates_input.rb +33 -0
  152. data/lib/graphql/schema/member.rb +6 -0
  153. data/lib/graphql/schema/mutation.rb +5 -1
  154. data/lib/graphql/schema/non_null.rb +30 -0
  155. data/lib/graphql/schema/object.rb +65 -12
  156. data/lib/graphql/schema/possible_types.rb +9 -4
  157. data/lib/graphql/schema/printer.rb +0 -15
  158. data/lib/graphql/schema/relay_classic_mutation.rb +5 -3
  159. data/lib/graphql/schema/resolver/has_payload_type.rb +5 -2
  160. data/lib/graphql/schema/resolver.rb +26 -18
  161. data/lib/graphql/schema/scalar.rb +27 -3
  162. data/lib/graphql/schema/subscription.rb +8 -18
  163. data/lib/graphql/schema/timeout.rb +29 -15
  164. data/lib/graphql/schema/traversal.rb +1 -1
  165. data/lib/graphql/schema/type_expression.rb +21 -13
  166. data/lib/graphql/schema/type_membership.rb +2 -2
  167. data/lib/graphql/schema/union.rb +37 -3
  168. data/lib/graphql/schema/unique_within_type.rb +1 -2
  169. data/lib/graphql/schema/validation.rb +10 -2
  170. data/lib/graphql/schema/warden.rb +115 -29
  171. data/lib/graphql/schema.rb +903 -195
  172. data/lib/graphql/static_validation/all_rules.rb +1 -0
  173. data/lib/graphql/static_validation/base_visitor.rb +10 -6
  174. data/lib/graphql/static_validation/literal_validator.rb +52 -27
  175. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +43 -83
  176. data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +17 -5
  177. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +33 -25
  178. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +1 -1
  179. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -4
  180. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +5 -5
  181. data/lib/graphql/static_validation/rules/fields_will_merge.rb +29 -21
  182. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  183. data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
  184. data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
  185. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
  186. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +4 -5
  187. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +12 -13
  188. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +5 -6
  189. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
  190. data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +5 -3
  191. data/lib/graphql/static_validation/type_stack.rb +2 -2
  192. data/lib/graphql/static_validation/validation_context.rb +1 -1
  193. data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
  194. data/lib/graphql/static_validation/validator.rb +30 -8
  195. data/lib/graphql/static_validation.rb +1 -0
  196. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +89 -19
  197. data/lib/graphql/subscriptions/broadcast_analyzer.rb +84 -0
  198. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
  199. data/lib/graphql/subscriptions/event.rb +23 -5
  200. data/lib/graphql/subscriptions/instrumentation.rb +10 -5
  201. data/lib/graphql/subscriptions/serialize.rb +22 -4
  202. data/lib/graphql/subscriptions/subscription_root.rb +15 -5
  203. data/lib/graphql/subscriptions.rb +108 -35
  204. data/lib/graphql/tracing/active_support_notifications_tracing.rb +14 -10
  205. data/lib/graphql/tracing/appoptics_tracing.rb +171 -0
  206. data/lib/graphql/tracing/appsignal_tracing.rb +8 -0
  207. data/lib/graphql/tracing/data_dog_tracing.rb +8 -0
  208. data/lib/graphql/tracing/new_relic_tracing.rb +9 -12
  209. data/lib/graphql/tracing/platform_tracing.rb +53 -9
  210. data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
  211. data/lib/graphql/tracing/prometheus_tracing.rb +8 -0
  212. data/lib/graphql/tracing/scout_tracing.rb +19 -0
  213. data/lib/graphql/tracing/skylight_tracing.rb +8 -0
  214. data/lib/graphql/tracing/statsd_tracing.rb +42 -0
  215. data/lib/graphql/tracing.rb +14 -34
  216. data/lib/graphql/types/big_int.rb +1 -1
  217. data/lib/graphql/types/int.rb +9 -2
  218. data/lib/graphql/types/iso_8601_date.rb +3 -3
  219. data/lib/graphql/types/iso_8601_date_time.rb +25 -10
  220. data/lib/graphql/types/relay/base_connection.rb +11 -7
  221. data/lib/graphql/types/relay/base_edge.rb +2 -1
  222. data/lib/graphql/types/string.rb +7 -1
  223. data/lib/graphql/unauthorized_error.rb +1 -1
  224. data/lib/graphql/union_type.rb +13 -28
  225. data/lib/graphql/unresolved_type_error.rb +2 -2
  226. data/lib/graphql/version.rb +1 -1
  227. data/lib/graphql.rb +31 -6
  228. data/readme.md +1 -1
  229. metadata +34 -9
  230. data/lib/graphql/literal_validation_error.rb +0 -6
@@ -6,7 +6,6 @@ module GraphQL
6
6
  # - Subscribed to by `subscription { ... }`
7
7
  # - Triggered by `MySchema.subscriber.trigger(name, arguments, obj)`
8
8
  #
9
- # An array of `Event`s are passed to `store.register(query, events)`.
10
9
  class Event
11
10
  # @return [String] Corresponds to the Subscription root field name
12
11
  attr_reader :name
@@ -49,10 +48,26 @@ module GraphQL
49
48
  raise ArgumentError, "Unexpected arguments: #{arguments}, must be Hash or GraphQL::Arguments"
50
49
  end
51
50
 
52
- sorted_h = normalized_args.to_h.sort.to_h
51
+ sorted_h = stringify_args(field, normalized_args.to_h)
53
52
  Serialize.dump_recursive([scope, name, sorted_h])
54
53
  end
55
54
 
55
+ # @return [String] a logical identifier for this event. (Stable when the query is broadcastable.)
56
+ def fingerprint
57
+ @fingerprint ||= begin
58
+ # When this query has been flagged as broadcastable,
59
+ # use a generalized, stable fingerprint so that
60
+ # duplicate subscriptions can be evaluated and distributed in bulk.
61
+ # (`@topic` includes field, args, and subscription scope already.)
62
+ if @context.namespace(:subscriptions)[:subscription_broadcastable]
63
+ "#{@topic}/#{@context.query.fingerprint}"
64
+ else
65
+ # not broadcastable, build a unique ID for this event
66
+ @context.schema.subscriptions.build_id
67
+ end
68
+ end
69
+ end
70
+
56
71
  class << self
57
72
  private
58
73
  def stringify_args(arg_owner, args)
@@ -71,18 +86,21 @@ module GraphQL
71
86
  arg_defn = get_arg_definition(arg_owner, normalized_arg_name)
72
87
  end
73
88
 
74
- next_args[normalized_arg_name] = stringify_args(arg_defn[1].type, v)
89
+ next_args[normalized_arg_name] = stringify_args(arg_defn.type, v)
75
90
  end
76
- next_args
91
+ # Make sure they're deeply sorted
92
+ next_args.sort.to_h
77
93
  when Array
78
94
  args.map { |a| stringify_args(arg_owner, a) }
95
+ when GraphQL::Schema::InputObject
96
+ stringify_args(arg_owner, args.to_h)
79
97
  else
80
98
  args
81
99
  end
82
100
  end
83
101
 
84
102
  def get_arg_definition(arg_owner, arg_name)
85
- arg_owner.arguments.find { |k, v| k == arg_name || v.keyword.to_s == arg_name }
103
+ arg_owner.arguments[arg_name] || arg_owner.arguments.each_value.find { |v| v.keyword.to_s == arg_name }
86
104
  end
87
105
  end
88
106
  end
@@ -11,7 +11,7 @@ module GraphQL
11
11
  end
12
12
 
13
13
  def instrument(type, field)
14
- if type == @schema.subscription
14
+ if type == @schema.subscription.graphql_definition
15
15
  # This is a root field of `subscription`
16
16
  subscribing_resolve_proc = SubscriptionRegistrationResolve.new(field.resolve_proc)
17
17
  field.redefine(resolve: subscribing_resolve_proc)
@@ -44,7 +44,10 @@ module GraphQL
44
44
 
45
45
  # Wrap the proc with subscription registration logic
46
46
  def call(obj, args, ctx)
47
- @inner_proc.call(obj, args, ctx) if @inner_proc && !@inner_proc.is_a?(GraphQL::Field::Resolve::BuiltInResolve)
47
+ result = nil
48
+ if @inner_proc && !@inner_proc.is_a?(GraphQL::Field::Resolve::BuiltInResolve)
49
+ result = @inner_proc.call(obj, args, ctx)
50
+ end
48
51
 
49
52
  events = ctx.namespace(:subscriptions)[:events]
50
53
 
@@ -56,10 +59,12 @@ module GraphQL
56
59
  arguments: args,
57
60
  context: ctx,
58
61
  )
59
- ctx.skip
62
+ result
60
63
  elsif ctx.irep_node.subscription_topic == ctx.query.subscription_topic
61
- # The root object is _already_ the subscription update:
62
- if obj.is_a?(GraphQL::Schema::Object)
64
+ if !result.nil?
65
+ result
66
+ elsif obj.is_a?(GraphQL::Schema::Object)
67
+ # The root object is _already_ the subscription update:
63
68
  obj.object
64
69
  else
65
70
  obj
@@ -9,6 +9,9 @@ module GraphQL
9
9
  GLOBALID_KEY = "__gid__"
10
10
  SYMBOL_KEY = "__sym__"
11
11
  SYMBOL_KEYS_KEY = "__sym_keys__"
12
+ TIMESTAMP_KEY = "__timestamp__"
13
+ TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%Z" # eg '2020-01-01 23:59:59.123456789+05:00'
14
+ OPEN_STRUCT_KEY = "__ostruct__"
12
15
 
13
16
  module_function
14
17
 
@@ -55,10 +58,20 @@ module GraphQL
55
58
  if value.is_a?(Array)
56
59
  value.map{|item| load_value(item)}
57
60
  elsif value.is_a?(Hash)
58
- if value.size == 1 && value.key?(GLOBALID_KEY)
59
- GlobalID::Locator.locate(value[GLOBALID_KEY])
60
- elsif value.size == 1 && value.key?(SYMBOL_KEY)
61
- value[SYMBOL_KEY].to_sym
61
+ if value.size == 1
62
+ case value.keys.first # there's only 1 key
63
+ when GLOBALID_KEY
64
+ GlobalID::Locator.locate(value[GLOBALID_KEY])
65
+ when SYMBOL_KEY
66
+ value[SYMBOL_KEY].to_sym
67
+ when TIMESTAMP_KEY
68
+ timestamp_class_name, timestamp_s = value[TIMESTAMP_KEY]
69
+ timestamp_class = Object.const_get(timestamp_class_name)
70
+ timestamp_class.strptime(timestamp_s, TIMESTAMP_FORMAT)
71
+ when OPEN_STRUCT_KEY
72
+ ostruct_values = load_value(value[OPEN_STRUCT_KEY])
73
+ OpenStruct.new(ostruct_values)
74
+ end
62
75
  else
63
76
  loaded_h = {}
64
77
  sym_keys = value.fetch(SYMBOL_KEYS_KEY, [])
@@ -101,6 +114,11 @@ module GraphQL
101
114
  { SYMBOL_KEY => obj.to_s }
102
115
  elsif obj.respond_to?(:to_gid_param)
103
116
  {GLOBALID_KEY => obj.to_gid_param}
117
+ elsif obj.is_a?(Date) || obj.is_a?(Time)
118
+ # DateTime extends Date; for TimeWithZone, call `.utc` first.
119
+ { TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
120
+ elsif obj.is_a?(OpenStruct)
121
+ { OPEN_STRUCT_KEY => dump_value(obj.to_h) }
104
122
  else
105
123
  obj
106
124
  end
@@ -2,9 +2,11 @@
2
2
 
3
3
  module GraphQL
4
4
  class Subscriptions
5
- # Extend this module in your subscription root when using {GraphQL::Execution::Interpreter}.
5
+ # @api private
6
+ # @deprecated This module is no longer needed.
6
7
  module SubscriptionRoot
7
8
  def self.extended(child_cls)
9
+ warn "`extend GraphQL::Subscriptions::SubscriptionRoot` is no longer required; you can remove it from your Subscription type (#{child_cls})"
8
10
  child_cls.include(InstanceMethods)
9
11
  end
10
12
 
@@ -38,17 +40,17 @@ module GraphQL
38
40
  elsif (events = context.namespace(:subscriptions)[:events])
39
41
  # This is the first execution, so gather an Event
40
42
  # for the backend to register:
41
- events << Subscriptions::Event.new(
43
+ event = Subscriptions::Event.new(
42
44
  name: field.name,
43
- arguments: arguments,
45
+ arguments: arguments_without_field_extras(arguments: arguments),
44
46
  context: context,
45
47
  field: field,
46
48
  )
47
- # TODO compat with non-class-based subscriptions?
49
+ events << event
48
50
  value
49
51
  elsif context.query.subscription_topic == Subscriptions::Event.serialize(
50
52
  field.name,
51
- arguments,
53
+ arguments_without_field_extras(arguments: arguments),
52
54
  field,
53
55
  scope: (field.subscription_scope ? context[field.subscription_scope] : nil),
54
56
  )
@@ -60,6 +62,14 @@ module GraphQL
60
62
  context.skip
61
63
  end
62
64
  end
65
+
66
+ private
67
+
68
+ def arguments_without_field_extras(arguments:)
69
+ arguments.dup.tap do |event_args|
70
+ field.extras.each { |k| event_args.delete(k) }
71
+ end
72
+ end
63
73
  end
64
74
  end
65
75
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  require "securerandom"
3
+ require "graphql/subscriptions/broadcast_analyzer"
3
4
  require "graphql/subscriptions/event"
4
5
  require "graphql/subscriptions/instrumentation"
5
6
  require "graphql/subscriptions/serialize"
6
- if defined?(ActionCable)
7
- require "graphql/subscriptions/action_cable_subscriptions"
8
- end
7
+ require "graphql/subscriptions/action_cable_subscriptions"
9
8
  require "graphql/subscriptions/subscription_root"
9
+ require "graphql/subscriptions/default_subscription_resolve_extension"
10
10
 
11
11
  module GraphQL
12
12
  class Subscriptions
@@ -18,20 +18,36 @@ module GraphQL
18
18
 
19
19
  # @see {Subscriptions#initialize} for options, concrete implementations may add options.
20
20
  def self.use(defn, options = {})
21
- schema = defn.target
22
- options[:schema] = schema
23
- schema.subscriptions = self.new(options)
21
+ schema = defn.is_a?(Class) ? defn : defn.target
22
+
23
+ if schema.subscriptions
24
+ raise ArgumentError, "Can't reinstall subscriptions. #{schema} is using #{schema.subscriptions}, can't also add #{self}"
25
+ end
26
+
24
27
  instrumentation = Subscriptions::Instrumentation.new(schema: schema)
25
- defn.instrument(:field, instrumentation)
26
28
  defn.instrument(:query, instrumentation)
29
+ defn.instrument(:field, instrumentation)
30
+ options[:schema] = schema
31
+ schema.subscriptions = self.new(**options)
32
+ schema.add_subscription_extension_if_necessary
27
33
  nil
28
34
  end
29
35
 
30
36
  # @param schema [Class] the GraphQL schema this manager belongs to
31
- def initialize(schema:, **rest)
37
+ def initialize(schema:, broadcast: false, default_broadcastable: false, **rest)
38
+ if broadcast
39
+ if !schema.using_ast_analysis?
40
+ raise ArgumentError, "`broadcast: true` requires AST analysis, add `using GraphQL::Analysis::AST` to your schema or see https://graphql-ruby.org/queries/ast_analysis.html."
41
+ end
42
+ schema.query_analyzer(Subscriptions::BroadcastAnalyzer)
43
+ end
44
+ @default_broadcastable = default_broadcastable
32
45
  @schema = schema
33
46
  end
34
47
 
48
+ # @return [Boolean] Used when fields don't have `broadcastable:` explicitly set
49
+ attr_reader :default_broadcastable
50
+
35
51
  # Fetch subscriptions matching this field + arguments pair
36
52
  # And pass them off to the queue.
37
53
  # @param event_name [String]
@@ -72,40 +88,64 @@ module GraphQL
72
88
  # `event` was triggered on `object`, and `subscription_id` was subscribed,
73
89
  # so it should be updated.
74
90
  #
75
- # Load `subscription_id`'s GraphQL data, re-evaluate the query, and deliver the result.
76
- #
77
- # This is where a queue may be inserted to push updates in the background.
91
+ # Load `subscription_id`'s GraphQL data, re-evaluate the query and return the result.
78
92
  #
79
93
  # @param subscription_id [String]
80
94
  # @param event [GraphQL::Subscriptions::Event] The event which was triggered
81
95
  # @param object [Object] The value for the subscription field
82
- # @return [void]
83
- def execute(subscription_id, event, object)
96
+ # @return [GraphQL::Query::Result]
97
+ def execute_update(subscription_id, event, object)
84
98
  # Lookup the saved data for this subscription
85
99
  query_data = read_subscription(subscription_id)
100
+ if query_data.nil?
101
+ delete_subscription(subscription_id)
102
+ return nil
103
+ end
104
+
86
105
  # Fetch the required keys from the saved data
87
106
  query_string = query_data.fetch(:query_string)
88
107
  variables = query_data.fetch(:variables)
89
108
  context = query_data.fetch(:context)
90
109
  operation_name = query_data.fetch(:operation_name)
91
- # Re-evaluate the saved query
92
- result = @schema.execute(
93
- {
94
- query: query_string,
95
- context: context,
96
- subscription_topic: event.topic,
97
- operation_name: operation_name,
98
- variables: variables,
99
- root_value: object,
100
- }
101
- )
102
- deliver(subscription_id, result)
103
- rescue GraphQL::Schema::Subscription::NoUpdateError
104
- # This update was skipped in user code; do nothing.
105
- rescue GraphQL::Schema::Subscription::UnsubscribedError
106
- # `unsubscribe` was called, clean up on our side
107
- # TODO also send `{more: false}` to client?
108
- delete_subscription(subscription_id)
110
+ result = nil
111
+ # this will be set to `false` unless `.execute` is terminated
112
+ # with a `throw :graphql_subscription_unsubscribed`
113
+ unsubscribed = true
114
+ catch(:graphql_subscription_unsubscribed) do
115
+ catch(:graphql_no_subscription_update) do
116
+ # Re-evaluate the saved query,
117
+ # but if it terminates early with a `throw`,
118
+ # it will stay `nil`
119
+ result = @schema.execute(
120
+ query: query_string,
121
+ context: context,
122
+ subscription_topic: event.topic,
123
+ operation_name: operation_name,
124
+ variables: variables,
125
+ root_value: object,
126
+ )
127
+ end
128
+ unsubscribed = false
129
+ end
130
+
131
+ if unsubscribed
132
+ # `unsubscribe` was called, clean up on our side
133
+ # TODO also send `{more: false}` to client?
134
+ delete_subscription(subscription_id)
135
+ end
136
+
137
+ result
138
+ end
139
+
140
+ # Run the update query for this subscription and deliver it
141
+ # @see {#execute_update}
142
+ # @see {#deliver}
143
+ # @return [void]
144
+ def execute(subscription_id, event, object)
145
+ res = execute_update(subscription_id, event, object)
146
+ if !res.nil?
147
+ deliver(subscription_id, res)
148
+ end
109
149
  end
110
150
 
111
151
  # Event `event` occurred on `object`,
@@ -178,6 +218,16 @@ module GraphQL
178
218
  Schema::Member::BuildType.camelize(event_or_arg_name.to_s)
179
219
  end
180
220
 
221
+ # @return [Boolean] if true, then a query like this one would be broadcasted
222
+ def broadcastable?(query_str, **query_options)
223
+ query = GraphQL::Query.new(@schema, query_str, **query_options)
224
+ if !query.valid?
225
+ raise "Invalid query: #{query.validation_errors.map(&:to_h).inspect}"
226
+ end
227
+ GraphQL::Analysis::AST.analyze_query(query, @schema.query_analyzers)
228
+ query.context.namespace(:subscriptions)[:subscription_broadcastable]
229
+ end
230
+
181
231
  private
182
232
 
183
233
  # Recursively normalize `args` as belonging to `arg_owner`:
@@ -188,7 +238,11 @@ module GraphQL
188
238
  # @return [Any] normalized arguments value
189
239
  def normalize_arguments(event_name, arg_owner, args)
190
240
  case arg_owner
191
- when GraphQL::Field, GraphQL::InputObjectType
241
+ when GraphQL::Field, GraphQL::InputObjectType, GraphQL::Schema::Field, Class
242
+ if arg_owner.is_a?(Class) && !arg_owner.kind.input_object?
243
+ # it's a type, but not an input object
244
+ return args
245
+ end
192
246
  normalized_args = {}
193
247
  missing_arg_names = []
194
248
  args.each do |k, v|
@@ -202,16 +256,35 @@ module GraphQL
202
256
  end
203
257
 
204
258
  if arg_defn
205
- normalized_args[normalized_arg_name] = normalize_arguments(event_name, arg_defn.type, v)
259
+ if arg_defn.loads
260
+ normalized_arg_name = arg_defn.keyword.to_s
261
+ end
262
+ normalized = normalize_arguments(event_name, arg_defn.type, v)
263
+ normalized_args[normalized_arg_name] = normalized
206
264
  else
207
265
  # Couldn't find a matching argument definition
208
266
  missing_arg_names << arg_name
209
267
  end
210
268
  end
211
269
 
270
+ # Backfill default values so that trigger arguments
271
+ # match query arguments.
272
+ arg_owner.arguments.each do |name, arg_defn|
273
+ if arg_defn.default_value? && !normalized_args.key?(arg_defn.name)
274
+ default_value = arg_defn.default_value
275
+ # We don't have an underlying "object" here, so it can't call methods.
276
+ # This is broken.
277
+ normalized_args[arg_defn.name] = arg_defn.prepare_value(nil, default_value, context: GraphQL::Query::NullContext)
278
+ end
279
+ end
280
+
212
281
  if missing_arg_names.any?
213
282
  arg_owner_name = if arg_owner.is_a?(GraphQL::Field)
214
283
  "Subscription.#{arg_owner.name}"
284
+ elsif arg_owner.is_a?(GraphQL::Schema::Field)
285
+ arg_owner.path
286
+ elsif arg_owner.is_a?(Class)
287
+ arg_owner.graphql_name
215
288
  else
216
289
  arg_owner.to_s
217
290
  end
@@ -219,9 +292,9 @@ module GraphQL
219
292
  end
220
293
 
221
294
  normalized_args
222
- when GraphQL::ListType
295
+ when GraphQL::ListType, GraphQL::Schema::List
223
296
  args.map { |a| normalize_arguments(event_name, arg_owner.of_type, a) }
224
- when GraphQL::NonNullType
297
+ when GraphQL::NonNullType, GraphQL::Schema::NonNull
225
298
  normalize_arguments(event_name, arg_owner.of_type, args)
226
299
  else
227
300
  args
@@ -8,19 +8,23 @@ module GraphQL
8
8
  module ActiveSupportNotificationsTracing
9
9
  # A cache of frequently-used keys to avoid needless string allocations
10
10
  KEYS = {
11
- "lex" => "graphql.lex",
12
- "parse" => "graphql.parse",
13
- "validate" => "graphql.validate",
14
- "analyze_multiplex" => "graphql.analyze_multiplex",
15
- "analyze_query" => "graphql.analyze_query",
16
- "execute_query" => "graphql.execute_query",
17
- "execute_query_lazy" => "graphql.execute_query_lazy",
18
- "execute_field" => "graphql.execute_field",
19
- "execute_field_lazy" => "graphql.execute_field_lazy",
11
+ "lex" => "lex.graphql",
12
+ "parse" => "parse.graphql",
13
+ "validate" => "validate.graphql",
14
+ "analyze_multiplex" => "analyze_multiplex.graphql",
15
+ "analyze_query" => "analyze_query.graphql",
16
+ "execute_query" => "execute_query.graphql",
17
+ "execute_query_lazy" => "execute_query_lazy.graphql",
18
+ "execute_field" => "execute_field.graphql",
19
+ "execute_field_lazy" => "execute_field_lazy.graphql",
20
+ "authorized" => "authorized.graphql",
21
+ "authorized_lazy" => "authorized_lazy.graphql",
22
+ "resolve_type" => "resolve_type.graphql",
23
+ "resolve_type_lazy" => "resolve_type.graphql",
20
24
  }
21
25
 
22
26
  def self.trace(key, metadata)
23
- prefixed_key = KEYS[key] || "graphql.#{key}"
27
+ prefixed_key = KEYS[key] || "#{key}.graphql"
24
28
  ActiveSupport::Notifications.instrument(prefixed_key, metadata) do
25
29
  yield
26
30
  end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+
6
+ # This class uses the AppopticsAPM SDK from the appoptics_apm gem to create
7
+ # traces for GraphQL.
8
+ #
9
+ # There are 4 configurations available. They can be set in the
10
+ # appoptics_apm config file or in code. Please see:
11
+ # {https://docs.appoptics.com/kb/apm_tracing/ruby/configure}
12
+ #
13
+ # AppOpticsAPM::Config[:graphql][:enabled] = true|false
14
+ # AppOpticsAPM::Config[:graphql][:transaction_name] = true|false
15
+ # AppOpticsAPM::Config[:graphql][:sanitize_query] = true|false
16
+ # AppOpticsAPM::Config[:graphql][:remove_comments] = true|false
17
+ class AppOpticsTracing < GraphQL::Tracing::PlatformTracing
18
+ # These GraphQL events will show up as 'graphql.prep' spans
19
+ PREP_KEYS = ['lex', 'parse', 'validate', 'analyze_query', 'analyze_multiplex'].freeze
20
+ # These GraphQL events will show up as 'graphql.execute' spans
21
+ EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze
22
+
23
+ # During auto-instrumentation this version of AppOpticsTracing is compared
24
+ # with the version provided in the appoptics_apm gem, so that the newer
25
+ # version of the class can be used
26
+
27
+ def self.version
28
+ Gem::Version.new('1.0.0')
29
+ end
30
+
31
+ self.platform_keys = {
32
+ 'lex' => 'lex',
33
+ 'parse' => 'parse',
34
+ 'validate' => 'validate',
35
+ 'analyze_query' => 'analyze_query',
36
+ 'analyze_multiplex' => 'analyze_multiplex',
37
+ 'execute_multiplex' => 'execute_multiplex',
38
+ 'execute_query' => 'execute_query',
39
+ 'execute_query_lazy' => 'execute_query_lazy'
40
+ }
41
+
42
+ def platform_trace(platform_key, _key, data)
43
+ return yield if !defined?(AppOpticsAPM) || gql_config[:enabled] == false
44
+
45
+ layer = span_name(platform_key)
46
+ kvs = metadata(data, layer)
47
+ kvs[:Key] = platform_key if (PREP_KEYS + EXEC_KEYS).include?(platform_key)
48
+
49
+ transaction_name(kvs[:InboundQuery]) if kvs[:InboundQuery] && layer == 'graphql.execute'
50
+
51
+ ::AppOpticsAPM::SDK.trace(layer, kvs) do
52
+ kvs.clear # we don't have to send them twice
53
+ yield
54
+ end
55
+ end
56
+
57
+ def platform_field_key(type, field)
58
+ "graphql.#{type.graphql_name}.#{field.graphql_name}"
59
+ end
60
+
61
+ def platform_authorized_key(type)
62
+ "graphql.authorized.#{type.graphql_name}"
63
+ end
64
+
65
+ def platform_resolve_type_key(type)
66
+ "graphql.resolve_type.#{type.graphql_name}"
67
+ end
68
+
69
+ private
70
+
71
+ def gql_config
72
+ ::AppOpticsAPM::Config[:graphql] ||= {}
73
+ end
74
+
75
+ def transaction_name(query)
76
+ return if gql_config[:transaction_name] == false ||
77
+ ::AppOpticsAPM::SDK.get_transaction_name
78
+
79
+ split_query = query.strip.split(/\W+/, 3)
80
+ split_query[0] = 'query' if split_query[0].empty?
81
+ name = "graphql.#{split_query[0..1].join('.')}"
82
+
83
+ ::AppOpticsAPM::SDK.set_transaction_name(name)
84
+ end
85
+
86
+ def multiplex_transaction_name(names)
87
+ return if gql_config[:transaction_name] == false ||
88
+ ::AppOpticsAPM::SDK.get_transaction_name
89
+
90
+ name = "graphql.multiplex.#{names.join('.')}"
91
+ name = "#{name[0..251]}..." if name.length > 254
92
+
93
+ ::AppOpticsAPM::SDK.set_transaction_name(name)
94
+ end
95
+
96
+ def span_name(key)
97
+ return 'graphql.prep' if PREP_KEYS.include?(key)
98
+ return 'graphql.execute' if EXEC_KEYS.include?(key)
99
+
100
+ key[/^graphql\./] ? key : "graphql.#{key}"
101
+ end
102
+
103
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
104
+ def metadata(data, layer)
105
+ data.keys.map do |key|
106
+ case key
107
+ when :context
108
+ graphql_context(data[key], layer)
109
+ when :query
110
+ graphql_query(data[key])
111
+ when :query_string
112
+ graphql_query_string(data[key])
113
+ when :multiplex
114
+ graphql_multiplex(data[key])
115
+ else
116
+ [key, data[key]]
117
+ end
118
+ end.flatten(2).each_slice(2).to_h.merge(Spec: 'graphql')
119
+ end
120
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
121
+
122
+ def graphql_context(context, layer)
123
+ context.errors && context.errors.each do |err|
124
+ AppOpticsAPM::API.log_exception(layer, err)
125
+ end
126
+
127
+ [[:Path, context.path.join('.')]]
128
+ end
129
+
130
+ def graphql_query(query)
131
+ return [] unless query
132
+
133
+ query_string = query.query_string
134
+ query_string = remove_comments(query_string) if gql_config[:remove_comments] != false
135
+ query_string = sanitize(query_string) if gql_config[:sanitize_query] != false
136
+
137
+ [[:InboundQuery, query_string],
138
+ [:Operation, query.selected_operation_name]]
139
+ end
140
+
141
+ def graphql_query_string(query_string)
142
+ query_string = remove_comments(query_string) if gql_config[:remove_comments] != false
143
+ query_string = sanitize(query_string) if gql_config[:sanitize_query] != false
144
+
145
+ [:InboundQuery, query_string]
146
+ end
147
+
148
+ def graphql_multiplex(data)
149
+ names = data.queries.map(&:operations).map(&:keys).flatten.compact
150
+ multiplex_transaction_name(names) if names.size > 1
151
+
152
+ [:Operations, names.join(', ')]
153
+ end
154
+
155
+ def sanitize(query)
156
+ return unless query
157
+
158
+ # remove arguments
159
+ query.gsub(/"[^"]*"/, '"?"') # strings
160
+ .gsub(/-?[0-9]*\.?[0-9]+e?[0-9]*/, '?') # ints + floats
161
+ .gsub(/\[[^\]]*\]/, '[?]') # arrays
162
+ end
163
+
164
+ def remove_comments(query)
165
+ return unless query
166
+
167
+ query.gsub(/#[^\n\r]*/, '')
168
+ end
169
+ end
170
+ end
171
+ end
@@ -23,6 +23,14 @@ module GraphQL
23
23
  def platform_field_key(type, field)
24
24
  "#{type.graphql_name}.#{field.graphql_name}.graphql"
25
25
  end
26
+
27
+ def platform_authorized_key(type)
28
+ "#{type.graphql_name}.authorized.graphql"
29
+ end
30
+
31
+ def platform_resolve_type_key(type)
32
+ "#{type.graphql_name}.resolve_type.graphql"
33
+ end
26
34
  end
27
35
  end
28
36
  end
@@ -63,6 +63,14 @@ module GraphQL
63
63
  def platform_field_key(type, field)
64
64
  "#{type.graphql_name}.#{field.graphql_name}"
65
65
  end
66
+
67
+ def platform_authorized_key(type)
68
+ "#{type.graphql_name}.authorized"
69
+ end
70
+
71
+ def platform_resolve_type_key(type)
72
+ "#{type.graphql_name}.resolve_type"
73
+ end
66
74
  end
67
75
  end
68
76
  end