graphql 1.9.17 → 1.11.7

Sign up to get free protection for your applications and to get access to all the features.
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