rails-graphql 0.2.1 → 1.0.0.rc1

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 (315) hide show
  1. checksums.yaml +4 -4
  2. data/ext/console.rb +18 -0
  3. data/ext/extconf.h +3 -0
  4. data/ext/extconf.rb +1 -54
  5. data/ext/gql_parser.c +631 -0
  6. data/ext/gql_parser.h +21 -0
  7. data/ext/shared.c +477 -0
  8. data/ext/shared.h +177 -0
  9. data/lib/generators/graphql/channel_generator.rb +27 -0
  10. data/lib/generators/graphql/controller_generator.rb +9 -4
  11. data/lib/generators/graphql/install_generator.rb +49 -0
  12. data/lib/generators/graphql/schema_generator.rb +9 -4
  13. data/lib/generators/graphql/templates/channel.erb +7 -0
  14. data/lib/generators/graphql/templates/config.rb +97 -0
  15. data/lib/generators/graphql/templates/controller.erb +2 -0
  16. data/lib/generators/graphql/templates/schema.erb +5 -3
  17. data/lib/gql_parser.so +0 -0
  18. data/lib/rails/graphql/adapters/mysql_adapter.rb +59 -0
  19. data/lib/rails/graphql/adapters/pg_adapter.rb +25 -22
  20. data/lib/rails/graphql/adapters/sqlite_adapter.rb +17 -14
  21. data/lib/rails/graphql/alternative/field_set.rb +48 -0
  22. data/lib/rails/graphql/alternative/mutation.rb +17 -0
  23. data/lib/rails/graphql/alternative/query.rb +98 -0
  24. data/lib/rails/graphql/alternative/subscription.rb +18 -0
  25. data/lib/rails/graphql/alternative.rb +20 -0
  26. data/lib/rails/graphql/argument.rb +25 -26
  27. data/lib/rails/graphql/callback.rb +30 -14
  28. data/lib/rails/graphql/collectors/hash_collector.rb +26 -7
  29. data/lib/rails/graphql/collectors/idented_collector.rb +10 -7
  30. data/lib/rails/graphql/collectors/json_collector.rb +43 -17
  31. data/lib/rails/graphql/collectors.rb +4 -4
  32. data/lib/rails/graphql/config.rb +154 -23
  33. data/lib/rails/graphql/directive/cached_directive.rb +33 -0
  34. data/lib/rails/graphql/directive/deprecated_directive.rb +10 -10
  35. data/lib/rails/graphql/directive/include_directive.rb +4 -4
  36. data/lib/rails/graphql/directive/skip_directive.rb +4 -4
  37. data/lib/rails/graphql/directive/specified_by_directive.rb +24 -0
  38. data/lib/rails/graphql/directive.rb +134 -73
  39. data/lib/rails/graphql/errors.rb +33 -4
  40. data/lib/rails/graphql/event.rb +21 -9
  41. data/lib/rails/graphql/field/authorized_field.rb +17 -6
  42. data/lib/rails/graphql/field/input_field.rb +8 -12
  43. data/lib/rails/graphql/field/mutation_field.rb +43 -9
  44. data/lib/rails/graphql/field/output_field.rb +112 -12
  45. data/lib/rails/graphql/field/proxied_field.rb +35 -26
  46. data/lib/rails/graphql/field/resolved_field.rb +27 -25
  47. data/lib/rails/graphql/field/scoped_config.rb +10 -4
  48. data/lib/rails/graphql/field/subscription_field.rb +123 -0
  49. data/lib/rails/graphql/field/typed_field.rb +69 -24
  50. data/lib/rails/graphql/field.rb +89 -74
  51. data/lib/rails/graphql/global_id.rb +89 -0
  52. data/lib/rails/graphql/helpers/attribute_delegator.rb +5 -5
  53. data/lib/rails/graphql/helpers/inherited_collection/array.rb +51 -0
  54. data/lib/rails/graphql/helpers/inherited_collection/base.rb +45 -0
  55. data/lib/rails/graphql/helpers/inherited_collection/hash.rb +88 -0
  56. data/lib/rails/graphql/helpers/inherited_collection.rb +25 -76
  57. data/lib/rails/graphql/helpers/instantiable.rb +15 -0
  58. data/lib/rails/graphql/helpers/leaf_from_ar.rb +7 -7
  59. data/lib/rails/graphql/helpers/registerable.rb +44 -62
  60. data/lib/rails/graphql/helpers/unregisterable.rb +16 -0
  61. data/lib/rails/graphql/helpers/with_arguments.rb +33 -28
  62. data/lib/rails/graphql/helpers/with_assignment.rb +6 -6
  63. data/lib/rails/graphql/helpers/with_callbacks.rb +28 -11
  64. data/lib/rails/graphql/helpers/with_description.rb +73 -0
  65. data/lib/rails/graphql/helpers/with_directives.rb +58 -30
  66. data/lib/rails/graphql/helpers/with_events.rb +22 -23
  67. data/lib/rails/graphql/helpers/with_fields.rb +86 -26
  68. data/lib/rails/graphql/helpers/with_global_id.rb +22 -0
  69. data/lib/rails/graphql/helpers/with_name.rb +44 -0
  70. data/lib/rails/graphql/helpers/with_namespace.rb +7 -4
  71. data/lib/rails/graphql/helpers/with_owner.rb +8 -7
  72. data/lib/rails/graphql/helpers/with_schema_fields.rb +162 -56
  73. data/lib/rails/graphql/helpers/with_validator.rb +9 -9
  74. data/lib/rails/graphql/helpers.rb +10 -3
  75. data/lib/rails/graphql/introspection.rb +43 -36
  76. data/lib/rails/graphql/railtie.rb +89 -33
  77. data/lib/rails/graphql/railties/app/base_channel.rb +10 -0
  78. data/lib/rails/graphql/railties/app/base_controller.rb +12 -0
  79. data/lib/rails/graphql/railties/app/views/_cable.js.erb +56 -0
  80. data/lib/rails/graphql/railties/app/views/_fetch.js.erb +20 -0
  81. data/lib/rails/graphql/railties/app/views/graphiql.html.erb +101 -0
  82. data/lib/rails/graphql/railties/base_generator.rb +5 -17
  83. data/lib/rails/graphql/railties/channel.rb +157 -0
  84. data/lib/rails/graphql/railties/controller.rb +91 -25
  85. data/lib/rails/graphql/railties/controller_runtime.rb +5 -5
  86. data/lib/rails/graphql/railties/log_subscriber.rb +81 -14
  87. data/lib/rails/graphql/request/arguments.rb +26 -50
  88. data/lib/rails/graphql/request/backtrace.rb +212 -0
  89. data/lib/rails/graphql/request/component/field.rb +98 -70
  90. data/lib/rails/graphql/request/component/fragment.rb +80 -26
  91. data/lib/rails/graphql/request/component/operation/subscription.rb +162 -4
  92. data/lib/rails/graphql/request/component/operation.rb +73 -34
  93. data/lib/rails/graphql/request/component/spread.rb +79 -27
  94. data/lib/rails/graphql/request/component/typename.rb +28 -13
  95. data/lib/rails/graphql/request/component.rb +77 -36
  96. data/lib/rails/graphql/request/context.rb +19 -9
  97. data/lib/rails/graphql/request/errors.rb +16 -6
  98. data/lib/rails/graphql/request/event.rb +23 -8
  99. data/lib/rails/graphql/request/helpers/directives.rb +69 -27
  100. data/lib/rails/graphql/request/helpers/selection_set.rb +57 -25
  101. data/lib/rails/graphql/request/helpers/value_writers.rb +24 -19
  102. data/lib/rails/graphql/request/prepared_data.rb +100 -0
  103. data/lib/rails/graphql/request/steps/authorizable.rb +24 -14
  104. data/lib/rails/graphql/request/steps/organizable.rb +111 -49
  105. data/lib/rails/graphql/request/steps/{prepareable.rb → preparable.rb} +21 -8
  106. data/lib/rails/graphql/request/steps/{resolveable.rb → resolvable.rb} +16 -7
  107. data/lib/rails/graphql/request/strategy/cached_strategy.rb +64 -0
  108. data/lib/rails/graphql/request/strategy/dynamic_instance.rb +6 -6
  109. data/lib/rails/graphql/request/strategy/multi_query_strategy.rb +6 -13
  110. data/lib/rails/graphql/request/strategy/sequenced_strategy.rb +9 -9
  111. data/lib/rails/graphql/request/strategy.rb +147 -77
  112. data/lib/rails/graphql/request/subscription.rb +82 -0
  113. data/lib/rails/graphql/request.rb +353 -104
  114. data/lib/rails/graphql/schema.rb +251 -106
  115. data/lib/rails/graphql/shortcuts.rb +33 -8
  116. data/lib/rails/graphql/source/active_record/builders.rb +64 -51
  117. data/lib/rails/graphql/source/active_record_source.rb +158 -82
  118. data/lib/rails/graphql/source/base.rb +83 -0
  119. data/lib/rails/graphql/source/builder.rb +115 -0
  120. data/lib/rails/graphql/source/scoped_arguments.rb +39 -21
  121. data/lib/rails/graphql/source.rb +90 -228
  122. data/lib/rails/graphql/subscription/provider/action_cable.rb +113 -0
  123. data/lib/rails/graphql/subscription/provider/base.rb +192 -0
  124. data/lib/rails/graphql/subscription/provider.rb +18 -0
  125. data/lib/rails/graphql/subscription/store/base.rb +141 -0
  126. data/lib/rails/graphql/subscription/store/memory.rb +136 -0
  127. data/lib/rails/graphql/subscription/store.rb +19 -0
  128. data/lib/rails/graphql/subscription.rb +17 -0
  129. data/lib/rails/graphql/to_gql.rb +29 -32
  130. data/lib/rails/graphql/type/creator.rb +196 -0
  131. data/lib/rails/graphql/type/enum/directive_location_enum.rb +11 -11
  132. data/lib/rails/graphql/type/enum/type_kind_enum.rb +3 -3
  133. data/lib/rails/graphql/type/enum.rb +44 -50
  134. data/lib/rails/graphql/type/input.rb +92 -25
  135. data/lib/rails/graphql/type/interface.rb +29 -28
  136. data/lib/rails/graphql/type/object/directive_object.rb +10 -9
  137. data/lib/rails/graphql/type/object/enum_value_object.rb +3 -3
  138. data/lib/rails/graphql/type/object/field_object.rb +24 -6
  139. data/lib/rails/graphql/type/object/input_value_object.rb +6 -7
  140. data/lib/rails/graphql/type/object/schema_object.rb +5 -8
  141. data/lib/rails/graphql/type/object/type_object.rb +62 -25
  142. data/lib/rails/graphql/type/object.rb +34 -26
  143. data/lib/rails/graphql/type/scalar/any_scalar.rb +30 -0
  144. data/lib/rails/graphql/type/scalar/bigint_scalar.rb +5 -5
  145. data/lib/rails/graphql/type/scalar/binary_scalar.rb +5 -3
  146. data/lib/rails/graphql/type/scalar/boolean_scalar.rb +8 -8
  147. data/lib/rails/graphql/type/scalar/date_scalar.rb +5 -3
  148. data/lib/rails/graphql/type/scalar/date_time_scalar.rb +5 -3
  149. data/lib/rails/graphql/type/scalar/decimal_scalar.rb +5 -3
  150. data/lib/rails/graphql/type/scalar/float_scalar.rb +5 -5
  151. data/lib/rails/graphql/type/scalar/id_scalar.rb +6 -5
  152. data/lib/rails/graphql/type/scalar/int_scalar.rb +6 -5
  153. data/lib/rails/graphql/type/scalar/json_scalar.rb +41 -0
  154. data/lib/rails/graphql/type/scalar/string_scalar.rb +18 -4
  155. data/lib/rails/graphql/type/scalar/time_scalar.rb +8 -6
  156. data/lib/rails/graphql/type/scalar.rb +26 -23
  157. data/lib/rails/graphql/type/union.rb +21 -18
  158. data/lib/rails/graphql/type.rb +43 -26
  159. data/lib/rails/graphql/type_map.rb +268 -165
  160. data/lib/rails/graphql/uri.rb +167 -0
  161. data/lib/rails/graphql/version.rb +19 -3
  162. data/lib/rails/graphql.rake +3 -0
  163. data/lib/rails/graphql.rb +91 -56
  164. data/lib/rails-graphql.rb +1 -1
  165. data/test/assets/en.yml +29 -0
  166. data/test/assets/introspection-mem.txt +1 -1
  167. data/test/assets/introspection.gql +2 -0
  168. data/test/assets/mem.gql +86 -99
  169. data/test/assets/mysql.gql +406 -0
  170. data/test/assets/sqlite.gql +96 -73
  171. data/test/assets/translate.gql +346 -0
  172. data/test/config.rb +19 -8
  173. data/test/graphql/schema_test.rb +14 -50
  174. data/test/graphql/source_test.rb +8 -85
  175. data/test/graphql/type/enum_test.rb +207 -203
  176. data/test/graphql/type/input_test.rb +14 -9
  177. data/test/graphql/type/interface_test.rb +12 -9
  178. data/test/graphql/type/object_test.rb +8 -2
  179. data/test/graphql/type/scalar/any_scalar_test.rb +38 -0
  180. data/test/graphql/type/scalar/boolean_scalar_test.rb +6 -3
  181. data/test/graphql/type/scalar/json_scalar_test.rb +23 -0
  182. data/test/graphql/type_map_test.rb +63 -81
  183. data/test/graphql/type_test.rb +0 -19
  184. data/test/graphql_test.rb +1 -1
  185. data/test/integration/{authorization/authorization_test.rb → authorization_test.rb} +40 -14
  186. data/test/integration/config.rb +36 -3
  187. data/test/integration/customization_test.rb +39 -0
  188. data/test/integration/global_id_test.rb +99 -0
  189. data/test/integration/memory/star_wars_introspection_test.rb +24 -16
  190. data/test/integration/memory/star_wars_query_test.rb +54 -3
  191. data/test/integration/memory/star_wars_validation_test.rb +3 -3
  192. data/test/integration/mysql/star_wars_introspection_test.rb +25 -0
  193. data/test/integration/persisted_query_test.rb +87 -0
  194. data/test/integration/resolver_precedence_test.rb +154 -0
  195. data/test/integration/schemas/memory.rb +24 -10
  196. data/test/integration/schemas/mysql.rb +62 -0
  197. data/test/integration/schemas/sqlite.rb +21 -12
  198. data/test/integration/sqlite/star_wars_global_id_test.rb +89 -0
  199. data/test/integration/sqlite/star_wars_introspection_test.rb +10 -0
  200. data/test/integration/sqlite/star_wars_query_test.rb +14 -1
  201. data/test/integration/translate_test.rb +73 -0
  202. data/test/test_ext.rb +16 -13
  203. metadata +125 -161
  204. data/ext/depend +0 -3
  205. data/ext/graphqlparser/Ast.cpp +0 -346
  206. data/ext/graphqlparser/Ast.h +0 -1214
  207. data/ext/graphqlparser/AstNode.h +0 -36
  208. data/ext/graphqlparser/AstVisitor.h +0 -137
  209. data/ext/graphqlparser/GraphQLParser.cpp +0 -76
  210. data/ext/graphqlparser/GraphQLParser.h +0 -55
  211. data/ext/graphqlparser/JsonVisitor.cpp +0 -161
  212. data/ext/graphqlparser/JsonVisitor.cpp.inc +0 -456
  213. data/ext/graphqlparser/JsonVisitor.h +0 -121
  214. data/ext/graphqlparser/JsonVisitor.h.inc +0 -110
  215. data/ext/graphqlparser/VERSION +0 -1
  216. data/ext/graphqlparser/c/GraphQLAst.cpp +0 -324
  217. data/ext/graphqlparser/c/GraphQLAst.h +0 -180
  218. data/ext/graphqlparser/c/GraphQLAstForEachConcreteType.h +0 -44
  219. data/ext/graphqlparser/c/GraphQLAstNode.cpp +0 -25
  220. data/ext/graphqlparser/c/GraphQLAstNode.h +0 -33
  221. data/ext/graphqlparser/c/GraphQLAstToJSON.cpp +0 -21
  222. data/ext/graphqlparser/c/GraphQLAstToJSON.h +0 -24
  223. data/ext/graphqlparser/c/GraphQLAstVisitor.cpp +0 -55
  224. data/ext/graphqlparser/c/GraphQLAstVisitor.h +0 -53
  225. data/ext/graphqlparser/c/GraphQLParser.cpp +0 -35
  226. data/ext/graphqlparser/c/GraphQLParser.h +0 -54
  227. data/ext/graphqlparser/dump_json_ast.cpp +0 -48
  228. data/ext/graphqlparser/lexer.lpp +0 -324
  229. data/ext/graphqlparser/parser.ypp +0 -693
  230. data/ext/graphqlparser/parsergen/lexer.cpp +0 -2633
  231. data/ext/graphqlparser/parsergen/lexer.h +0 -528
  232. data/ext/graphqlparser/parsergen/location.hh +0 -189
  233. data/ext/graphqlparser/parsergen/parser.tab.cpp +0 -3300
  234. data/ext/graphqlparser/parsergen/parser.tab.hpp +0 -646
  235. data/ext/graphqlparser/parsergen/position.hh +0 -179
  236. data/ext/graphqlparser/parsergen/stack.hh +0 -156
  237. data/ext/graphqlparser/syntaxdefs.h +0 -19
  238. data/ext/libgraphqlparser/AstNode.h +0 -36
  239. data/ext/libgraphqlparser/CMakeLists.txt +0 -148
  240. data/ext/libgraphqlparser/CONTRIBUTING.md +0 -23
  241. data/ext/libgraphqlparser/GraphQLParser.cpp +0 -76
  242. data/ext/libgraphqlparser/GraphQLParser.h +0 -55
  243. data/ext/libgraphqlparser/JsonVisitor.cpp +0 -161
  244. data/ext/libgraphqlparser/JsonVisitor.h +0 -121
  245. data/ext/libgraphqlparser/LICENSE +0 -22
  246. data/ext/libgraphqlparser/README.clang-tidy +0 -7
  247. data/ext/libgraphqlparser/README.md +0 -84
  248. data/ext/libgraphqlparser/ast/ast.ast +0 -203
  249. data/ext/libgraphqlparser/ast/ast.py +0 -61
  250. data/ext/libgraphqlparser/ast/c.py +0 -100
  251. data/ext/libgraphqlparser/ast/c.pyc +0 -0
  252. data/ext/libgraphqlparser/ast/c_impl.py +0 -61
  253. data/ext/libgraphqlparser/ast/c_impl.pyc +0 -0
  254. data/ext/libgraphqlparser/ast/c_visitor_impl.py +0 -39
  255. data/ext/libgraphqlparser/ast/c_visitor_impl.pyc +0 -0
  256. data/ext/libgraphqlparser/ast/casing.py +0 -26
  257. data/ext/libgraphqlparser/ast/casing.pyc +0 -0
  258. data/ext/libgraphqlparser/ast/cxx.py +0 -197
  259. data/ext/libgraphqlparser/ast/cxx.pyc +0 -0
  260. data/ext/libgraphqlparser/ast/cxx_impl.py +0 -61
  261. data/ext/libgraphqlparser/ast/cxx_impl.pyc +0 -0
  262. data/ext/libgraphqlparser/ast/cxx_json_visitor_header.py +0 -42
  263. data/ext/libgraphqlparser/ast/cxx_json_visitor_header.pyc +0 -0
  264. data/ext/libgraphqlparser/ast/cxx_json_visitor_impl.py +0 -80
  265. data/ext/libgraphqlparser/ast/cxx_json_visitor_impl.pyc +0 -0
  266. data/ext/libgraphqlparser/ast/cxx_visitor.py +0 -64
  267. data/ext/libgraphqlparser/ast/cxx_visitor.pyc +0 -0
  268. data/ext/libgraphqlparser/ast/js.py +0 -65
  269. data/ext/libgraphqlparser/ast/license.py +0 -10
  270. data/ext/libgraphqlparser/ast/license.pyc +0 -0
  271. data/ext/libgraphqlparser/c/GraphQLAstNode.cpp +0 -25
  272. data/ext/libgraphqlparser/c/GraphQLAstNode.h +0 -33
  273. data/ext/libgraphqlparser/c/GraphQLAstToJSON.cpp +0 -21
  274. data/ext/libgraphqlparser/c/GraphQLAstToJSON.h +0 -24
  275. data/ext/libgraphqlparser/c/GraphQLAstVisitor.cpp +0 -55
  276. data/ext/libgraphqlparser/c/GraphQLAstVisitor.h +0 -53
  277. data/ext/libgraphqlparser/c/GraphQLParser.cpp +0 -35
  278. data/ext/libgraphqlparser/c/GraphQLParser.h +0 -54
  279. data/ext/libgraphqlparser/clang-tidy-all.sh +0 -3
  280. data/ext/libgraphqlparser/cmake/version.cmake +0 -16
  281. data/ext/libgraphqlparser/dump_json_ast.cpp +0 -48
  282. data/ext/libgraphqlparser/go/README.md +0 -20
  283. data/ext/libgraphqlparser/go/callbacks.go +0 -18
  284. data/ext/libgraphqlparser/go/gotest.go +0 -64
  285. data/ext/libgraphqlparser/lexer.lpp +0 -324
  286. data/ext/libgraphqlparser/libgraphqlparser.pc.in +0 -11
  287. data/ext/libgraphqlparser/parser.ypp +0 -693
  288. data/ext/libgraphqlparser/parsergen/lexer.cpp +0 -2633
  289. data/ext/libgraphqlparser/parsergen/lexer.h +0 -528
  290. data/ext/libgraphqlparser/parsergen/location.hh +0 -189
  291. data/ext/libgraphqlparser/parsergen/parser.tab.cpp +0 -3300
  292. data/ext/libgraphqlparser/parsergen/parser.tab.hpp +0 -646
  293. data/ext/libgraphqlparser/parsergen/position.hh +0 -179
  294. data/ext/libgraphqlparser/parsergen/stack.hh +0 -156
  295. data/ext/libgraphqlparser/python/CMakeLists.txt +0 -14
  296. data/ext/libgraphqlparser/python/README.md +0 -5
  297. data/ext/libgraphqlparser/python/example.py +0 -31
  298. data/ext/libgraphqlparser/syntaxdefs.h +0 -19
  299. data/ext/libgraphqlparser/test/BuildCAPI.c +0 -5
  300. data/ext/libgraphqlparser/test/CMakeLists.txt +0 -25
  301. data/ext/libgraphqlparser/test/JsonVisitorTests.cpp +0 -28
  302. data/ext/libgraphqlparser/test/ParserTests.cpp +0 -352
  303. data/ext/libgraphqlparser/test/kitchen-sink.graphql +0 -59
  304. data/ext/libgraphqlparser/test/kitchen-sink.json +0 -1
  305. data/ext/libgraphqlparser/test/schema-kitchen-sink.graphql +0 -78
  306. data/ext/libgraphqlparser/test/schema-kitchen-sink.json +0 -1
  307. data/ext/libgraphqlparser/test/valgrind.supp +0 -33
  308. data/ext/version.cpp +0 -21
  309. data/lib/graphqlparser.so +0 -0
  310. data/lib/rails/graphql/native/functions.rb +0 -38
  311. data/lib/rails/graphql/native/location.rb +0 -41
  312. data/lib/rails/graphql/native/pointers.rb +0 -23
  313. data/lib/rails/graphql/native/visitor.rb +0 -349
  314. data/lib/rails/graphql/native.rb +0 -56
  315. data/test/integration/schemas/authorization.rb +0 -12
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_cable'
4
+
5
+ module Rails
6
+ module GraphQL
7
+ module Subscription
8
+ module Provider
9
+ # = GraphQL Action Cable Subscription Provider
10
+ #
11
+ # The subscription provider associated with Rails Action Cable, that
12
+ # delivers subscription notifications through an Action Cable Channel
13
+ # TODO: Try to serialize and deserialize the origin
14
+ class ActionCable < Base
15
+ INTERNAL_CHANNEL = 'rails-graphql:events'
16
+
17
+ attr_reader :cable, :prefix
18
+
19
+ def initialize(*args, **options)
20
+ @cable = options.fetch(:cable, ::ActionCable)
21
+ @prefix = options.fetch(:prefix, 'rails-graphql')
22
+
23
+ @event_callback = ->(message) do
24
+ method_name, args, xargs = Marshal.load(message)
25
+ @mutex.synchronize { send(method_name, *args, **xargs) }
26
+ end
27
+
28
+ super
29
+ end
30
+
31
+ def shutdown
32
+ @pubsub = nil
33
+ end
34
+
35
+ def accepts?(operation)
36
+ operation.request.origin.is_a?(::ActionCable::Channel::Base)
37
+ end
38
+
39
+ def add(*subscriptions)
40
+ with_pubsub do
41
+ subscriptions.each do |item|
42
+ log(:added, item)
43
+ store.add(item)
44
+ stream_from(item)
45
+ end
46
+ end
47
+ end
48
+
49
+ def async_remove(item)
50
+ return if (item = store.fetch(item)).nil?
51
+ cable.server.broadcast(stream_name(item), unsubscribed_payload)
52
+ store.remove(item)
53
+
54
+ log(:removed, item)
55
+ end
56
+
57
+ def async_update(item, data = nil, **xargs)
58
+ return if (item = store.fetch(item)).nil?
59
+ removing = false
60
+
61
+ log(:updated, item) do
62
+ data = execute(item, **xargs) if data.nil?
63
+ store.update!(item)
64
+
65
+ unless (removing = unsubscribing?(data))
66
+ data = { 'result' => data, 'more' => true }
67
+ cable.server.broadcast(stream_name(item), data)
68
+ end
69
+ end
70
+
71
+ async_remove(item) if removing
72
+ end
73
+
74
+ def stream_name(item)
75
+ "#{prefix}:#{item.sid}"
76
+ end
77
+
78
+ protected
79
+
80
+ def stream_from(item)
81
+ item.origin.stream_from(stream_name(item))
82
+ end
83
+
84
+ def execute(item, **xargs)
85
+ super(item, origin: item.origin, **xargs, as: :hash)
86
+ end
87
+
88
+ def async_exec(method_name, *args, **xargs)
89
+ payload = [method_name, args, store.serialize(**xargs)]
90
+ with_pubsub { @pubsub.broadcast(INTERNAL_CHANNEL, Marshal.dump(payload)) }
91
+ nil
92
+ end
93
+
94
+ def with_pubsub(&callback)
95
+ success = -> { cable.server.event_loop.post(&callback) }
96
+ return success.call if defined?(@pubsub) && !@pubsub.nil?
97
+
98
+ cable.server.pubsub.subscribe(INTERNAL_CHANNEL, @event_callback, success)
99
+ @pubsub = cable.server.pubsub
100
+ end
101
+
102
+ def validate!
103
+ super
104
+
105
+ raise ValidationError, (+<<~MSG).squish if @prefix.blank?
106
+ Unable to setup #{self.class.name} because a proper prefix was not provided.
107
+ MSG
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module GraphQL
5
+ module Subscription
6
+ module Provider
7
+ # = GraphQL Base Subscription Provider
8
+ #
9
+ # The base class for all the other subscription providers, which defines
10
+ # the necessary interfaces to install and stream the subscription to
11
+ # their right places
12
+ #
13
+ # As a way to properly support ActiveRecord objects as part of the scope
14
+ # in a way that does not require queries and instance, a hash scope,
15
+ # where we have the class as the key and one or more ids as values,
16
+ # must be supported. In general, a implementation using +.hash+ is
17
+ # recommended because +User.find(1).hash == User.class.hash ^ 1.hash+
18
+ class Base
19
+ # An abstract type won't appear in the introspection and will not be
20
+ # instantiated by requests
21
+ class_attribute :abstract, instance_accessor: false, default: false
22
+
23
+ delegate :fetch, :search, :find_each, to: :store
24
+
25
+ class << self
26
+
27
+ # Make sure that abstract classes cannot be instantiated
28
+ def new(*, **)
29
+ return super unless self.abstract
30
+
31
+ raise StandardError, (+<<~MSG).squish
32
+ #{name} is abstract and cannot be used as a subscription provider.
33
+ MSG
34
+ end
35
+
36
+ # Make sure to run the provided +methods+ in async mode. Use the
37
+ # lock to identify if it's already running in async or not
38
+ def async_exec(*method_names)
39
+ method_names.each do |method_name|
40
+ async_method_name = :"async_#{method_name}"
41
+
42
+ class_eval do
43
+ return warn((+<<~MSG).squish) if method_defined?(async_method_name)
44
+ Already async #{method_name}
45
+ MSG
46
+
47
+ alias_method async_method_name, method_name
48
+
49
+ define_method(method_name) do |*args, **xargs|
50
+ if @mutex.owned?
51
+ send(async_method_name, *args, **xargs)
52
+ else
53
+ async_exec(async_method_name, *args, **xargs)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ def initialize(**options)
63
+ @store = options.fetch(:store, Store::Memory.new)
64
+ @logger = options.fetch(:logger, GraphQL.logger)
65
+ @mutex = Mutex.new
66
+
67
+ validate!
68
+ end
69
+
70
+ # Use this method to remove variables that needs to be restarted when
71
+ # the provider is doing a refresh. Remember to keep the data in the
72
+ # store so that it can still recover and keep posting updates
73
+ def shutdown
74
+ end
75
+
76
+ # Before even generating the item, check if the operation can be
77
+ # subscribed
78
+ def accepts?(operation)
79
+ raise NotImplementedError, +"#{self.class.name} does not implement accepts?"
80
+ end
81
+
82
+ # Add one or more subscriptions to the provider
83
+ def add(*subscriptions)
84
+ raise NotImplementedError, +"#{self.class.name} does not implement add"
85
+ end
86
+
87
+ # Remove one subscription from the provider, assuming that they will
88
+ # be properly notified about the removal. For a group operation, use
89
+ # +search_and_remove+
90
+ async_exec def remove(item)
91
+ raise NotImplementedError, +"#{self.class.name} does not implement remove"
92
+ end
93
+
94
+ # Update one single subscription, for broadcasting, use +update_all+
95
+ # or +search_and_update+. You can provide the +data+ that will be sent
96
+ # to upstream, skipping it from being collected from a request
97
+ async_exec def update(item, data = nil, **xargs)
98
+ raise NotImplementedError, +"#{self.class.name} does not implement update"
99
+ end
100
+
101
+ # A simple shortcut for calling remove on each individual sid
102
+ async_exec def remove_all(*items)
103
+ items.each(&method(:remove))
104
+ end
105
+
106
+ # A simple shortcut for calling update on each individual sid
107
+ async_exec def update_all(*sids, **xargs)
108
+ return if sids.blank?
109
+
110
+ enum = GraphQL.enumerate(store.fetch(*sids))
111
+ enum.group_by(&:operation_id).each_value do |subscriptions|
112
+ data = execute(subscriptions.first, **xargs, broadcasting: true) \
113
+ unless subscriptions.one? || first.broadcastable?
114
+
115
+ subscriptions.each { |item| update(item, data, **xargs) }
116
+ end
117
+ end
118
+
119
+ # A shortcut for finding the subscriptions and then remove them
120
+ async_exec def search_and_remove(**options)
121
+ find_each(**options, &method(:remove))
122
+ end
123
+
124
+ # A shortcut for finding the subscriptions and then updating them
125
+ async_exec def search_and_update(**options)
126
+ xargs = options.slice(:data_for)
127
+ update_all(*find_each(**options), **xargs)
128
+ end
129
+
130
+ # Get the payload that should be used when unsubscribing
131
+ def unsubscribed_payload
132
+ Request::Component::Operation::Subscription::UNSUBSCRIBED_PAYLOAD
133
+ end
134
+
135
+ # Check if the given +value+ indicates that it is unsubscribing
136
+ def unsubscribing?(value)
137
+ value == Request::Component::Operation::Subscription::UNSUBSCRIBED_RESULT
138
+ end
139
+
140
+ protected
141
+ attr_reader :store, :logger
142
+
143
+ # Make sure to set sub provider as not abstract
144
+ def inherited(other)
145
+ other.abstract = false
146
+ super
147
+ end
148
+
149
+ # Check if the given +object+ is a subscription instance
150
+ def instance?(object)
151
+ object.is_a?(Request::Subscription)
152
+ end
153
+
154
+ # Logo a given +event+ for the given +item+
155
+ def log(event, item = nil, &block)
156
+ data = { item: item, type: event, provider: self }
157
+ ActiveSupport::Notifications.instrument('subscription.graphql', **data, &block)
158
+ end
159
+
160
+ # Create a new request and execute, but using all the information
161
+ # stored in the provided +subscription+
162
+ def execute(subscription, broadcasting: false, **xargs)
163
+ catch(:skip_subscription_update) do
164
+ context = subscription.context.dup
165
+ context[:broadcasting] = true if broadcasting
166
+ xargs.reverse_merge(context: context, as: :string)
167
+
168
+ namespace = subscription.schema
169
+ Request.execute(nil, **xargs, namespace: namespace, hash: subscription.sid)
170
+ end
171
+ end
172
+
173
+ # Make sure to rewrite this method so that you can properly execute
174
+ # methods asynchronously. Remember to
175
+ def async_exec(method_name, *args, **xargs)
176
+ @mutex.synchronize { send(method_name, *args, **xargs) }
177
+ end
178
+
179
+ # Make sure that the settings provided are enough to operate
180
+ def validate!
181
+ valid = defined?(@store) && @store.is_a?(Subscription::Store::Base)
182
+ raise ValidationError, (+<<~MSG).squish unless valid
183
+ Unable to setup #{self.class.name} because a proper store was not
184
+ defined, "#{@store.inspect}" was provided.
185
+ MSG
186
+ end
187
+
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module GraphQL
5
+ module Subscription
6
+ # = GraphQL Subscription Provider
7
+ #
8
+ # Subscription provider holds all the possible options for handlers of
9
+ # subscriptions, which all should inherit from Provider::Base
10
+ module Provider
11
+ extend ActiveSupport::Autoload
12
+
13
+ autoload :Base
14
+ autoload :ActionCable
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module GraphQL
5
+ module Subscription
6
+ module Store
7
+ # = GraphQL Base Subscription Store
8
+ #
9
+ # The base class for all the other subscription stores, which defines
10
+ # the necessary interfaces to keep track of all the subscriptions
11
+ #
12
+ # Be careful with each subscription context. Although there are ways to
13
+ # clean it up (by implementing a +subscription_context+ callback into
14
+ # the field), it is still the most dangerous and heavy object that can
15
+ # be placed into the store. The problem with in memory store is that it
16
+ # does not work with a Rails application running cross-processes. On the
17
+ # other hand, file, Redis, or Database-based stores can find it
18
+ # difficult to save the context and bring it back to Rails again
19
+ #
20
+ # The closest best way to be safe about the context is relying on
21
+ # +ActiveJob::Arguments+ to serialize and deserialize it (which aligns
22
+ # with all possible arguments that jobs and receive and how they are
23
+ # usually properly stored in several different providers for ActiveJob)
24
+ class Base
25
+ # An abstract type won't appear in the introspection and will not be
26
+ # instantiated by requests
27
+ class_attribute :abstract, instance_accessor: false, default: false
28
+
29
+ class << self
30
+
31
+ # Make sure that abstract classes cannot be instantiated
32
+ def new(*)
33
+ return super unless self.abstract
34
+
35
+ raise StandardError, (+<<~MSG).squish
36
+ #{name} is abstract and cannot be used as a subscription store.
37
+ MSG
38
+ end
39
+
40
+ end
41
+
42
+ # Get the list of provided +xargs+ for search and serialize them
43
+ def serialize(**xargs)
44
+ xargs
45
+ end
46
+
47
+ # Return all the sids stored
48
+ def all
49
+ raise NotImplementedError, +"#{self.class.name} does not implement all"
50
+ end
51
+
52
+ # Add a new subscription to the store, saving in a way it can be easily
53
+ # searched at any point
54
+ def add(subscription)
55
+ raise NotImplementedError, +"#{self.class.name} does not implement add"
56
+ end
57
+
58
+ # Fetch one or more subscriptions by their ids
59
+ def fetch(*sids)
60
+ raise NotImplementedError, +"#{self.class.name} does not implement fetch"
61
+ end
62
+
63
+ # Remove a given subscription from the store by its id or instance
64
+ def remove(item)
65
+ raise NotImplementedError, +"#{self.class.name} does not implement remove"
66
+ end
67
+
68
+ # Marks that a subscription has received an update
69
+ def update!(item)
70
+ raise NotImplementedError, +"#{self.class.name} does not implement update!"
71
+ end
72
+
73
+ # Check if a given sid or instance is stored
74
+ def has?(item)
75
+ raise NotImplementedError, +"#{self.class.name} does not implement has?"
76
+ end
77
+
78
+ # Search one or more subscriptions by the list of provided options and
79
+ # return the list of sids that matched. A block can be provided to go
80
+ # through each of the found results, yield the object itself instead
81
+ # of the sid
82
+ def search(**options, &block)
83
+ raise NotImplementedError, +"#{self.class.name} does not implement search"
84
+ end
85
+
86
+ alias find_each search
87
+
88
+ protected
89
+
90
+ # Check if the given +object+ is a subscription instance
91
+ def instance?(object)
92
+ object.is_a?(Request::Subscription)
93
+ end
94
+
95
+ # Transform a scope in several possible scopes, as in:
96
+ # nil => nil
97
+ # :user => [[:user]]
98
+ # User.find(1) => [[NNN1]] # .hash
99
+ # [User.find(1), :sample] => [[NNN1, :sample]]
100
+ # { User => 1, other: :profile } => [[NNN1, :profile]]
101
+ # { User => [1, 2], other: :profile } => [[NNN1, :profile], [NNN2, :profile]]
102
+ def possible_scopes(scope)
103
+ return if scope.nil? || scope === EMPTY_ARRAY
104
+
105
+ list = Array.wrap(scope).each_with_object([]) do |value, result|
106
+ result << options = []
107
+
108
+ next GraphQL.enumerate(value).each do |val|
109
+ options << hash_for(val)
110
+ end unless value.is_a?(Hash)
111
+
112
+ value.each.with_index do |(key, sub_value), idx|
113
+ result << options = [] if idx > 0
114
+
115
+ klass_arg = key if key.is_a?(Class)
116
+ GraphQL.enumerate(sub_value).each do |val|
117
+ options << hash_for(val, klass_arg)
118
+ end
119
+ end
120
+ end
121
+
122
+ list.reduce(:product).flatten.each_slice(list.size).map { |a| a.reduce(:^) }
123
+ end
124
+
125
+ # By default, get the hash of the value. If class is provided, add
126
+ # it as part of the hash (similar to how ActiveRecord calculates
127
+ # the hash for a model's record)
128
+ def hash_for(value, klass = nil)
129
+ if !klass.nil?
130
+ klass.hash ^ value.hash
131
+ elsif value.is_a?(Numeric)
132
+ value
133
+ else
134
+ value.hash
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module GraphQL
5
+ module Subscription
6
+ module Store
7
+ # = GraphQL Memory Subscription Store
8
+ #
9
+ # This store will save all the subscriptions in a very similar way that
10
+ # the TypeMap works, using nested concurrent maps that points out to
11
+ # them. Everything is based on the sid of the subscription
12
+ class Memory < Base
13
+ attr_reader :list, :index
14
+
15
+ def initialize
16
+ # The list store a simple association between sid and
17
+ @list = Concurrent::Map.new
18
+
19
+ # This store the index in a way that is possible to search
20
+ # subscriptions in a fast manner
21
+ @index = Concurrent::Map.new do |h1, key1| # Fields
22
+ scopes = Concurrent::Map.new do |h2, key2| # Scopes
23
+ arguments = Concurrent::Map.new do |h3, key3| # Arguments
24
+ h3.fetch_or_store(key3, Concurrent::Array.new) # SIDs
25
+ end
26
+
27
+ h2.fetch_or_store(key2, arguments)
28
+ end
29
+
30
+ h1.fetch_or_store(key1, scopes)
31
+ end
32
+ end
33
+
34
+ def serialize(**xargs)
35
+ return xargs if !xargs.key?(:field) || xargs[:field].is_a?(Numeric)
36
+
37
+ xargs[:field] = hash_for(xargs[:field])
38
+ xargs[:scope] = possible_scopes(xargs[:scope])
39
+ xargs[:args] = Array.wrap(xargs[:args]).map(&method(:hash_for))
40
+ xargs
41
+ end
42
+
43
+ def all
44
+ list.keys
45
+ end
46
+
47
+ def add(subscription)
48
+ if has?(subscription.sid)
49
+ raise ::ArgumentError, +"SID #{subscription.sid} is already taken."
50
+ end
51
+
52
+ # Rewrite the scope, to save memory
53
+ scope = possible_scopes(subscription.scope)&.first
54
+ subscription.instance_variable_set(:@scope, scope)
55
+
56
+ # Save to the list and to the index
57
+ list[subscription.sid] = subscription
58
+ index_set = subscription_to_index(subscription).reduce(index, &:[])
59
+ index_set << subscription.sid
60
+ subscription.sid
61
+ end
62
+
63
+ def fetch(*sids)
64
+ return if sids.none?
65
+
66
+ items = sids.map do |item|
67
+ instance?(item) ? item : list[item]
68
+ end
69
+
70
+ items.one? ? items.first : items
71
+ end
72
+
73
+ def remove(item)
74
+ return unless has?(item)
75
+
76
+ instance = instance?(item) ? item : fetch(item)
77
+ path = subscription_to_index(instance)
78
+ index.delete(instance.sid)
79
+
80
+ f_level = index[path[0]]
81
+ s_level = f_level[path[1]]
82
+ a_level = s_level[path[2]]
83
+
84
+ a_level.delete(instance.sid)
85
+ s_level.delete(path[2]) if a_level.empty?
86
+ f_level.delete(path[1]) if s_level.empty?
87
+ index.delete(path[0]) if f_level.empty?
88
+ end
89
+
90
+ def update!(item)
91
+ (instance?(item) ? item : fetch(item)).update!
92
+ end
93
+
94
+ def has?(item)
95
+ list.key?(instance?(item) ? item.sid : item)
96
+ end
97
+
98
+ def search(**xargs, &block)
99
+ xargs = serialize(**xargs)
100
+ field, scope, args = xargs.values_at(:field, :scope, :args)
101
+
102
+ if field.nil? && args.nil? && scope.nil?
103
+ list.each(&block) unless block.nil?
104
+ return all
105
+ end
106
+
107
+ [].tap do |result|
108
+ GraphQL.enumerate(field || index.keys).each do |key1|
109
+ GraphQL.enumerate(scope || index[key1].keys).each do |key2|
110
+ GraphQL.enumerate(args || index[key2].keys).each do |key3|
111
+ items = index.fetch(key1, nil)&.fetch(key2, nil)&.fetch(key3, nil)
112
+ items.each(&list.method(:[])).each(&block) unless block.nil?
113
+ result.concat(items || EMPTY_ARRAY)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ alias find_each search
121
+
122
+ protected
123
+
124
+ # Turn the request subscription into into the path of the index
125
+ def subscription_to_index(subscription)
126
+ [
127
+ subscription.field.hash,
128
+ subscription.scope,
129
+ subscription.args.hash,
130
+ ]
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module GraphQL
5
+ module Subscription
6
+ # = GraphQL Subscription Store
7
+ #
8
+ # Subscription store holds all the possible options for storing the
9
+ # subscriptions, allowing to segmentation by field, variables, and several
10
+ # other things according to the necessity
11
+ module Store
12
+ extend ActiveSupport::Autoload
13
+
14
+ autoload :Base
15
+ autoload :Memory
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module GraphQL
5
+ # = GraphQL Subscription
6
+ #
7
+ # A namespace for storing subscription-related objects like the provider
8
+ # for a stream/websocket provider, and the store, for where the
9
+ # subscriptions are stored
10
+ module Subscription
11
+ extend ActiveSupport::Autoload
12
+
13
+ autoload :Store
14
+ autoload :Provider
15
+ end
16
+ end
17
+ end