rails-graphql 0.2.1 → 1.0.0.beta

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 (297) 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 +646 -0
  6. data/ext/shared.c +482 -0
  7. data/ext/shared.h +177 -0
  8. data/lib/gql_parser.so +0 -0
  9. data/lib/rails/graphql/adapters/mysql_adapter.rb +59 -0
  10. data/lib/rails/graphql/adapters/pg_adapter.rb +25 -22
  11. data/lib/rails/graphql/adapters/sqlite_adapter.rb +17 -14
  12. data/lib/rails/graphql/alternative/field_set.rb +36 -0
  13. data/lib/rails/graphql/alternative/mutation.rb +17 -0
  14. data/lib/rails/graphql/alternative/query.rb +93 -0
  15. data/lib/rails/graphql/alternative/subscription.rb +17 -0
  16. data/lib/rails/graphql/alternative.rb +20 -0
  17. data/lib/rails/graphql/argument.rb +21 -24
  18. data/lib/rails/graphql/callback.rb +24 -9
  19. data/lib/rails/graphql/collectors/hash_collector.rb +14 -6
  20. data/lib/rails/graphql/collectors/idented_collector.rb +10 -7
  21. data/lib/rails/graphql/collectors/json_collector.rb +22 -17
  22. data/lib/rails/graphql/collectors.rb +4 -4
  23. data/lib/rails/graphql/config.rb +130 -15
  24. data/lib/rails/graphql/directive/cached_directive.rb +33 -0
  25. data/lib/rails/graphql/directive/deprecated_directive.rb +10 -10
  26. data/lib/rails/graphql/directive/include_directive.rb +5 -4
  27. data/lib/rails/graphql/directive/skip_directive.rb +5 -4
  28. data/lib/rails/graphql/directive.rb +118 -63
  29. data/lib/rails/graphql/errors.rb +33 -4
  30. data/lib/rails/graphql/event.rb +16 -5
  31. data/lib/rails/graphql/field/authorized_field.rb +19 -3
  32. data/lib/rails/graphql/field/input_field.rb +11 -10
  33. data/lib/rails/graphql/field/mutation_field.rb +42 -7
  34. data/lib/rails/graphql/field/output_field.rb +102 -13
  35. data/lib/rails/graphql/field/proxied_field.rb +31 -22
  36. data/lib/rails/graphql/field/resolved_field.rb +26 -24
  37. data/lib/rails/graphql/field/scoped_config.rb +10 -4
  38. data/lib/rails/graphql/field/subscription_field.rb +140 -0
  39. data/lib/rails/graphql/field/typed_field.rb +43 -22
  40. data/lib/rails/graphql/field.rb +70 -56
  41. data/lib/rails/graphql/global_id.rb +85 -0
  42. data/lib/rails/graphql/helpers/attribute_delegator.rb +5 -5
  43. data/lib/rails/graphql/helpers/inherited_collection/array.rb +50 -0
  44. data/lib/rails/graphql/helpers/inherited_collection/base.rb +43 -0
  45. data/lib/rails/graphql/helpers/inherited_collection/hash.rb +87 -0
  46. data/lib/rails/graphql/helpers/inherited_collection.rb +25 -76
  47. data/lib/rails/graphql/helpers/instantiable.rb +15 -0
  48. data/lib/rails/graphql/helpers/leaf_from_ar.rb +7 -7
  49. data/lib/rails/graphql/helpers/registerable.rb +44 -62
  50. data/lib/rails/graphql/helpers/unregisterable.rb +16 -0
  51. data/lib/rails/graphql/helpers/with_arguments.rb +31 -27
  52. data/lib/rails/graphql/helpers/with_assignment.rb +6 -6
  53. data/lib/rails/graphql/helpers/with_callbacks.rb +25 -8
  54. data/lib/rails/graphql/helpers/with_description.rb +71 -0
  55. data/lib/rails/graphql/helpers/with_directives.rb +54 -30
  56. data/lib/rails/graphql/helpers/with_events.rb +21 -23
  57. data/lib/rails/graphql/helpers/with_fields.rb +76 -22
  58. data/lib/rails/graphql/helpers/with_global_id.rb +22 -0
  59. data/lib/rails/graphql/helpers/with_name.rb +43 -0
  60. data/lib/rails/graphql/helpers/with_namespace.rb +7 -4
  61. data/lib/rails/graphql/helpers/with_owner.rb +8 -7
  62. data/lib/rails/graphql/helpers/with_schema_fields.rb +137 -55
  63. data/lib/rails/graphql/helpers/with_validator.rb +9 -9
  64. data/lib/rails/graphql/helpers.rb +10 -3
  65. data/lib/rails/graphql/introspection.rb +43 -36
  66. data/lib/rails/graphql/railtie.rb +88 -33
  67. data/lib/rails/graphql/railties/base_generator.rb +3 -9
  68. data/lib/rails/graphql/railties/channel.rb +157 -0
  69. data/lib/rails/graphql/railties/controller.rb +62 -17
  70. data/lib/rails/graphql/railties/controller_runtime.rb +5 -5
  71. data/lib/rails/graphql/railties/log_subscriber.rb +81 -14
  72. data/lib/rails/graphql/request/arguments.rb +24 -49
  73. data/lib/rails/graphql/request/backtrace.rb +191 -0
  74. data/lib/rails/graphql/request/component/field.rb +86 -65
  75. data/lib/rails/graphql/request/component/fragment.rb +72 -24
  76. data/lib/rails/graphql/request/component/operation/subscription.rb +164 -4
  77. data/lib/rails/graphql/request/component/operation.rb +63 -31
  78. data/lib/rails/graphql/request/component/spread.rb +68 -25
  79. data/lib/rails/graphql/request/component/typename.rb +27 -12
  80. data/lib/rails/graphql/request/component.rb +75 -36
  81. data/lib/rails/graphql/request/context.rb +18 -8
  82. data/lib/rails/graphql/request/errors.rb +16 -6
  83. data/lib/rails/graphql/request/event.rb +19 -8
  84. data/lib/rails/graphql/request/helpers/directives.rb +68 -27
  85. data/lib/rails/graphql/request/helpers/selection_set.rb +51 -25
  86. data/lib/rails/graphql/request/helpers/value_writers.rb +18 -16
  87. data/lib/rails/graphql/request/prepared_data.rb +98 -0
  88. data/lib/rails/graphql/request/steps/authorizable.rb +24 -14
  89. data/lib/rails/graphql/request/steps/organizable.rb +110 -48
  90. data/lib/rails/graphql/request/steps/{prepareable.rb → preparable.rb} +20 -7
  91. data/lib/rails/graphql/request/steps/{resolveable.rb → resolvable.rb} +15 -6
  92. data/lib/rails/graphql/request/strategy/cached_strategy.rb +64 -0
  93. data/lib/rails/graphql/request/strategy/dynamic_instance.rb +6 -6
  94. data/lib/rails/graphql/request/strategy/multi_query_strategy.rb +6 -13
  95. data/lib/rails/graphql/request/strategy/sequenced_strategy.rb +9 -9
  96. data/lib/rails/graphql/request/strategy.rb +131 -75
  97. data/lib/rails/graphql/request/subscription.rb +80 -0
  98. data/lib/rails/graphql/request.rb +305 -86
  99. data/lib/rails/graphql/schema.rb +240 -48
  100. data/lib/rails/graphql/shortcuts.rb +22 -3
  101. data/lib/rails/graphql/source/active_record/builders.rb +49 -35
  102. data/lib/rails/graphql/source/active_record_source.rb +70 -54
  103. data/lib/rails/graphql/source/base.rb +111 -0
  104. data/lib/rails/graphql/source/builder.rb +128 -0
  105. data/lib/rails/graphql/source/scoped_arguments.rb +31 -19
  106. data/lib/rails/graphql/source.rb +89 -213
  107. data/lib/rails/graphql/subscription/provider/action_cable.rb +112 -0
  108. data/lib/rails/graphql/subscription/provider/base.rb +191 -0
  109. data/lib/rails/graphql/subscription/provider.rb +18 -0
  110. data/lib/rails/graphql/subscription/store/base.rb +145 -0
  111. data/lib/rails/graphql/subscription/store/memory.rb +127 -0
  112. data/lib/rails/graphql/subscription/store.rb +19 -0
  113. data/lib/rails/graphql/subscription.rb +17 -0
  114. data/lib/rails/graphql/to_gql.rb +29 -32
  115. data/lib/rails/graphql/type/enum/directive_location_enum.rb +11 -11
  116. data/lib/rails/graphql/type/enum/type_kind_enum.rb +3 -3
  117. data/lib/rails/graphql/type/enum.rb +34 -48
  118. data/lib/rails/graphql/type/input.rb +74 -23
  119. data/lib/rails/graphql/type/interface.rb +16 -26
  120. data/lib/rails/graphql/type/object/directive_object.rb +4 -4
  121. data/lib/rails/graphql/type/object/enum_value_object.rb +3 -3
  122. data/lib/rails/graphql/type/object/field_object.rb +24 -6
  123. data/lib/rails/graphql/type/object/input_value_object.rb +3 -3
  124. data/lib/rails/graphql/type/object/schema_object.rb +5 -8
  125. data/lib/rails/graphql/type/object/type_object.rb +29 -19
  126. data/lib/rails/graphql/type/object.rb +26 -23
  127. data/lib/rails/graphql/type/scalar/any_scalar.rb +30 -0
  128. data/lib/rails/graphql/type/scalar/bigint_scalar.rb +5 -5
  129. data/lib/rails/graphql/type/scalar/binary_scalar.rb +3 -3
  130. data/lib/rails/graphql/type/scalar/boolean_scalar.rb +8 -8
  131. data/lib/rails/graphql/type/scalar/date_scalar.rb +3 -3
  132. data/lib/rails/graphql/type/scalar/date_time_scalar.rb +3 -3
  133. data/lib/rails/graphql/type/scalar/decimal_scalar.rb +3 -3
  134. data/lib/rails/graphql/type/scalar/float_scalar.rb +5 -5
  135. data/lib/rails/graphql/type/scalar/id_scalar.rb +6 -5
  136. data/lib/rails/graphql/type/scalar/int_scalar.rb +6 -5
  137. data/lib/rails/graphql/type/scalar/json_scalar.rb +39 -0
  138. data/lib/rails/graphql/type/scalar/string_scalar.rb +18 -4
  139. data/lib/rails/graphql/type/scalar/time_scalar.rb +5 -5
  140. data/lib/rails/graphql/type/scalar.rb +25 -22
  141. data/lib/rails/graphql/type/union.rb +14 -16
  142. data/lib/rails/graphql/type.rb +34 -25
  143. data/lib/rails/graphql/type_map.rb +256 -164
  144. data/lib/rails/graphql/uri.rb +166 -0
  145. data/lib/rails/graphql/version.rb +15 -3
  146. data/lib/rails/graphql.rake +3 -0
  147. data/lib/rails/graphql.rb +85 -52
  148. data/lib/rails-graphql.rb +1 -1
  149. data/test/assets/en.yml +29 -0
  150. data/test/assets/introspection-mem.txt +1 -1
  151. data/test/assets/mem.gql +18 -45
  152. data/test/assets/mysql.gql +392 -0
  153. data/test/assets/sqlite.gql +21 -12
  154. data/test/assets/translate.gql +335 -0
  155. data/test/config.rb +18 -8
  156. data/test/graphql/schema_test.rb +12 -19
  157. data/test/graphql/source_test.rb +8 -75
  158. data/test/graphql/type/enum_test.rb +207 -203
  159. data/test/graphql/type/input_test.rb +14 -9
  160. data/test/graphql/type/interface_test.rb +4 -4
  161. data/test/graphql/type/scalar/any_scalar_test.rb +38 -0
  162. data/test/graphql/type/scalar/boolean_scalar_test.rb +6 -3
  163. data/test/graphql/type/scalar/json_scalar_test.rb +23 -0
  164. data/test/graphql/type_map_test.rb +51 -66
  165. data/test/graphql/type_test.rb +0 -19
  166. data/test/graphql_test.rb +1 -1
  167. data/test/integration/{authorization/authorization_test.rb → authorization_test.rb} +40 -14
  168. data/test/integration/config.rb +36 -3
  169. data/test/integration/customization_test.rb +39 -0
  170. data/test/integration/global_id_test.rb +99 -0
  171. data/test/integration/memory/star_wars_introspection_test.rb +24 -16
  172. data/test/integration/memory/star_wars_query_test.rb +54 -3
  173. data/test/integration/memory/star_wars_validation_test.rb +1 -1
  174. data/test/integration/mysql/star_wars_introspection_test.rb +25 -0
  175. data/test/integration/persisted_query_test.rb +87 -0
  176. data/test/integration/resolver_precedence_test.rb +154 -0
  177. data/test/integration/schemas/memory.rb +22 -7
  178. data/test/integration/schemas/mysql.rb +62 -0
  179. data/test/integration/schemas/sqlite.rb +21 -12
  180. data/test/integration/sqlite/star_wars_global_id_test.rb +83 -0
  181. data/test/integration/sqlite/star_wars_introspection_test.rb +10 -0
  182. data/test/integration/sqlite/star_wars_query_test.rb +14 -1
  183. data/test/integration/translate_test.rb +61 -0
  184. data/test/test_ext.rb +16 -13
  185. metadata +108 -157
  186. data/ext/depend +0 -3
  187. data/ext/graphqlparser/Ast.cpp +0 -346
  188. data/ext/graphqlparser/Ast.h +0 -1214
  189. data/ext/graphqlparser/AstNode.h +0 -36
  190. data/ext/graphqlparser/AstVisitor.h +0 -137
  191. data/ext/graphqlparser/GraphQLParser.cpp +0 -76
  192. data/ext/graphqlparser/GraphQLParser.h +0 -55
  193. data/ext/graphqlparser/JsonVisitor.cpp +0 -161
  194. data/ext/graphqlparser/JsonVisitor.cpp.inc +0 -456
  195. data/ext/graphqlparser/JsonVisitor.h +0 -121
  196. data/ext/graphqlparser/JsonVisitor.h.inc +0 -110
  197. data/ext/graphqlparser/VERSION +0 -1
  198. data/ext/graphqlparser/c/GraphQLAst.cpp +0 -324
  199. data/ext/graphqlparser/c/GraphQLAst.h +0 -180
  200. data/ext/graphqlparser/c/GraphQLAstForEachConcreteType.h +0 -44
  201. data/ext/graphqlparser/c/GraphQLAstNode.cpp +0 -25
  202. data/ext/graphqlparser/c/GraphQLAstNode.h +0 -33
  203. data/ext/graphqlparser/c/GraphQLAstToJSON.cpp +0 -21
  204. data/ext/graphqlparser/c/GraphQLAstToJSON.h +0 -24
  205. data/ext/graphqlparser/c/GraphQLAstVisitor.cpp +0 -55
  206. data/ext/graphqlparser/c/GraphQLAstVisitor.h +0 -53
  207. data/ext/graphqlparser/c/GraphQLParser.cpp +0 -35
  208. data/ext/graphqlparser/c/GraphQLParser.h +0 -54
  209. data/ext/graphqlparser/dump_json_ast.cpp +0 -48
  210. data/ext/graphqlparser/lexer.lpp +0 -324
  211. data/ext/graphqlparser/parser.ypp +0 -693
  212. data/ext/graphqlparser/parsergen/lexer.cpp +0 -2633
  213. data/ext/graphqlparser/parsergen/lexer.h +0 -528
  214. data/ext/graphqlparser/parsergen/location.hh +0 -189
  215. data/ext/graphqlparser/parsergen/parser.tab.cpp +0 -3300
  216. data/ext/graphqlparser/parsergen/parser.tab.hpp +0 -646
  217. data/ext/graphqlparser/parsergen/position.hh +0 -179
  218. data/ext/graphqlparser/parsergen/stack.hh +0 -156
  219. data/ext/graphqlparser/syntaxdefs.h +0 -19
  220. data/ext/libgraphqlparser/AstNode.h +0 -36
  221. data/ext/libgraphqlparser/CMakeLists.txt +0 -148
  222. data/ext/libgraphqlparser/CONTRIBUTING.md +0 -23
  223. data/ext/libgraphqlparser/GraphQLParser.cpp +0 -76
  224. data/ext/libgraphqlparser/GraphQLParser.h +0 -55
  225. data/ext/libgraphqlparser/JsonVisitor.cpp +0 -161
  226. data/ext/libgraphqlparser/JsonVisitor.h +0 -121
  227. data/ext/libgraphqlparser/LICENSE +0 -22
  228. data/ext/libgraphqlparser/README.clang-tidy +0 -7
  229. data/ext/libgraphqlparser/README.md +0 -84
  230. data/ext/libgraphqlparser/ast/ast.ast +0 -203
  231. data/ext/libgraphqlparser/ast/ast.py +0 -61
  232. data/ext/libgraphqlparser/ast/c.py +0 -100
  233. data/ext/libgraphqlparser/ast/c.pyc +0 -0
  234. data/ext/libgraphqlparser/ast/c_impl.py +0 -61
  235. data/ext/libgraphqlparser/ast/c_impl.pyc +0 -0
  236. data/ext/libgraphqlparser/ast/c_visitor_impl.py +0 -39
  237. data/ext/libgraphqlparser/ast/c_visitor_impl.pyc +0 -0
  238. data/ext/libgraphqlparser/ast/casing.py +0 -26
  239. data/ext/libgraphqlparser/ast/casing.pyc +0 -0
  240. data/ext/libgraphqlparser/ast/cxx.py +0 -197
  241. data/ext/libgraphqlparser/ast/cxx.pyc +0 -0
  242. data/ext/libgraphqlparser/ast/cxx_impl.py +0 -61
  243. data/ext/libgraphqlparser/ast/cxx_impl.pyc +0 -0
  244. data/ext/libgraphqlparser/ast/cxx_json_visitor_header.py +0 -42
  245. data/ext/libgraphqlparser/ast/cxx_json_visitor_header.pyc +0 -0
  246. data/ext/libgraphqlparser/ast/cxx_json_visitor_impl.py +0 -80
  247. data/ext/libgraphqlparser/ast/cxx_json_visitor_impl.pyc +0 -0
  248. data/ext/libgraphqlparser/ast/cxx_visitor.py +0 -64
  249. data/ext/libgraphqlparser/ast/cxx_visitor.pyc +0 -0
  250. data/ext/libgraphqlparser/ast/js.py +0 -65
  251. data/ext/libgraphqlparser/ast/license.py +0 -10
  252. data/ext/libgraphqlparser/ast/license.pyc +0 -0
  253. data/ext/libgraphqlparser/c/GraphQLAstNode.cpp +0 -25
  254. data/ext/libgraphqlparser/c/GraphQLAstNode.h +0 -33
  255. data/ext/libgraphqlparser/c/GraphQLAstToJSON.cpp +0 -21
  256. data/ext/libgraphqlparser/c/GraphQLAstToJSON.h +0 -24
  257. data/ext/libgraphqlparser/c/GraphQLAstVisitor.cpp +0 -55
  258. data/ext/libgraphqlparser/c/GraphQLAstVisitor.h +0 -53
  259. data/ext/libgraphqlparser/c/GraphQLParser.cpp +0 -35
  260. data/ext/libgraphqlparser/c/GraphQLParser.h +0 -54
  261. data/ext/libgraphqlparser/clang-tidy-all.sh +0 -3
  262. data/ext/libgraphqlparser/cmake/version.cmake +0 -16
  263. data/ext/libgraphqlparser/dump_json_ast.cpp +0 -48
  264. data/ext/libgraphqlparser/go/README.md +0 -20
  265. data/ext/libgraphqlparser/go/callbacks.go +0 -18
  266. data/ext/libgraphqlparser/go/gotest.go +0 -64
  267. data/ext/libgraphqlparser/lexer.lpp +0 -324
  268. data/ext/libgraphqlparser/libgraphqlparser.pc.in +0 -11
  269. data/ext/libgraphqlparser/parser.ypp +0 -693
  270. data/ext/libgraphqlparser/parsergen/lexer.cpp +0 -2633
  271. data/ext/libgraphqlparser/parsergen/lexer.h +0 -528
  272. data/ext/libgraphqlparser/parsergen/location.hh +0 -189
  273. data/ext/libgraphqlparser/parsergen/parser.tab.cpp +0 -3300
  274. data/ext/libgraphqlparser/parsergen/parser.tab.hpp +0 -646
  275. data/ext/libgraphqlparser/parsergen/position.hh +0 -179
  276. data/ext/libgraphqlparser/parsergen/stack.hh +0 -156
  277. data/ext/libgraphqlparser/python/CMakeLists.txt +0 -14
  278. data/ext/libgraphqlparser/python/README.md +0 -5
  279. data/ext/libgraphqlparser/python/example.py +0 -31
  280. data/ext/libgraphqlparser/syntaxdefs.h +0 -19
  281. data/ext/libgraphqlparser/test/BuildCAPI.c +0 -5
  282. data/ext/libgraphqlparser/test/CMakeLists.txt +0 -25
  283. data/ext/libgraphqlparser/test/JsonVisitorTests.cpp +0 -28
  284. data/ext/libgraphqlparser/test/ParserTests.cpp +0 -352
  285. data/ext/libgraphqlparser/test/kitchen-sink.graphql +0 -59
  286. data/ext/libgraphqlparser/test/kitchen-sink.json +0 -1
  287. data/ext/libgraphqlparser/test/schema-kitchen-sink.graphql +0 -78
  288. data/ext/libgraphqlparser/test/schema-kitchen-sink.json +0 -1
  289. data/ext/libgraphqlparser/test/valgrind.supp +0 -33
  290. data/ext/version.cpp +0 -21
  291. data/lib/graphqlparser.so +0 -0
  292. data/lib/rails/graphql/native/functions.rb +0 -38
  293. data/lib/rails/graphql/native/location.rb +0 -41
  294. data/lib/rails/graphql/native/pointers.rb +0 -23
  295. data/lib/rails/graphql/native/visitor.rb +0 -349
  296. data/lib/rails/graphql/native.rb +0 -56
  297. data/test/integration/schemas/authorization.rb +0 -12
@@ -0,0 +1,191 @@
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)
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)
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, broadcasting: true) \
113
+ unless subscriptions.one? || first.broadcastable?
114
+
115
+ subscriptions.each { |item| update(item, data) }
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
+ update_all(*find_each(**options))
127
+ end
128
+
129
+ # Get the payload that should be used when unsubscribing
130
+ def unsubscribed_payload
131
+ Request::Component::Operation::Subscription::UNSUBSCRIBED_PAYLOAD
132
+ end
133
+
134
+ # Check if the given +value+ indicates that it is unsubscribing
135
+ def unsubscribing?(value)
136
+ value == Request::Component::Operation::Subscription::UNSUBSCRIBED_RESULT
137
+ end
138
+
139
+ protected
140
+ attr_reader :store, :logger
141
+
142
+ # Make sure to set sub provider as not abstract
143
+ def inherited(other)
144
+ other.abstract = false
145
+ super
146
+ end
147
+
148
+ # Check if the given +object+ is a subscription instance
149
+ def instance?(object)
150
+ object.is_a?(Request::Subscription)
151
+ end
152
+
153
+ # Logo a given +event+ for the given +item+
154
+ def log(event, item = nil, &block)
155
+ data = { item: item, type: event, provider: self }
156
+ ActiveSupport::Notifications.instrument('subscription.graphql', **data, &block)
157
+ end
158
+
159
+ # Create a new request and execute, but using all the information
160
+ # stored in the provided +subscription+
161
+ def execute(subscription, broadcasting: false, **xargs)
162
+ catch(:skip_subscription_update) do
163
+ context = subscription.context.dup
164
+ context[:broadcasting] = true if broadcasting
165
+ xargs.reverse_merge(context: context, as: :string)
166
+
167
+ namespace = subscription.schema
168
+ Request.execute(nil, **xargs, namespace: namespace, hash: subscription.sid)
169
+ end
170
+ end
171
+
172
+ # Make sure to rewrite this method so that you can properly execute
173
+ # methods asynchronously. Remember to
174
+ def async_exec(method_name, *args, **xargs)
175
+ @mutex.synchronize { send(method_name, *args, **xargs) }
176
+ end
177
+
178
+ # Make sure that the settings provided are enough to operate
179
+ def validate!
180
+ valid = defined?(@store) && @store.is_a?(Subscription::Store::Base)
181
+ raise ValidationError, (+<<~MSG).squish unless valid
182
+ Unable to setup #{self.class.name} because a proper store was not
183
+ defined, "#{@store.inspect}" was provided.
184
+ MSG
185
+ end
186
+
187
+ end
188
+ end
189
+ end
190
+ end
191
+ 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,145 @@
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
+ # Check if a given sid or instance is stored
69
+ def has?(item)
70
+ raise NotImplementedError, +"#{self.class.name} does not implement has?"
71
+ end
72
+
73
+ # Search one or more subscriptions by the list of provided options and
74
+ # return the list of sids that matched. A block can be provided to go
75
+ # through each of the found results, yield the object itself instead
76
+ # of the sid
77
+ def search(**options, &block)
78
+ raise NotImplementedError, +"#{self.class.name} does not implement search"
79
+ end
80
+
81
+ alias find_each search
82
+
83
+ protected
84
+
85
+ # Check if the given +object+ is a subscription instance
86
+ def instance?(object)
87
+ object.is_a?(Request::Subscription)
88
+ end
89
+
90
+ # Transform a scope in several possible scopes, as in:
91
+ # nil => nil
92
+ # :user => [[:user]]
93
+ # User.find(1) => [[NNN1]] # .hash
94
+ # [User.find(1), :sample] => [[NNN1, :sample]]
95
+ # { User => 1, other: :profile } => [[NNN1, :profile]]
96
+ # { User => [1, 2], other: :profile } => [[NNN1, :profile], [NNN2, :profile]]
97
+ def possible_scopes(scope)
98
+ return if scope.nil? || scope === EMPTY_ARRAY
99
+
100
+ list = Array.wrap(scope).each_with_object([]) do |value, result|
101
+ result << options = []
102
+
103
+ next GraphQL.enumerate(value).each do |val|
104
+ options << hash_for(val)
105
+ end unless value.is_a?(Hash)
106
+
107
+ value.each.with_index do |(key, sub_value), idx|
108
+ result << options = [] if idx > 0
109
+
110
+ klass_arg = key if key.is_a?(Class)
111
+ GraphQL.enumerate(sub_value).each do |val|
112
+ options << hash_for(val, klass_arg)
113
+ end
114
+ end
115
+ end
116
+
117
+ list.reduce(:product).flatten.each_slice(list.size).map { |a| a.reduce(:^) }
118
+ end
119
+
120
+ # By default, get the hash of the value. If class is provided, add
121
+ # it as part of the hash (similar to how ActiveRecord calculates
122
+ # the hash for a model's record)
123
+ def hash_for(value, klass = nil)
124
+ if !klass.nil?
125
+ klass.hash ^ value.hash
126
+ elsif extract_class_from?(value)
127
+ value.class.hash ^ value.id.hash
128
+ elsif value.is_a?(Numeric)
129
+ value
130
+ else
131
+ value.hash
132
+ end
133
+ end
134
+
135
+ # Check if ActiveRecord::Base is available and then if the object
136
+ # provided is an instance of it, so that the serialize can work
137
+ # correctly
138
+ def extract_class_from?(value)
139
+ defined?(ActiveRecord) && value.is_a?(ActiveRecord::Base)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,127 @@
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
+ list[subscription.sid] = subscription
53
+ index_set = subscription_to_index(subscription).reduce(index, &:[])
54
+ index_set << subscription.sid
55
+ subscription.sid
56
+ end
57
+
58
+ def fetch(*sids)
59
+ if sids.none?
60
+ nil
61
+ elsif sids.one?
62
+ list[sids.first]
63
+ else
64
+ sids.map(&list.method(:[]))
65
+ end
66
+ end
67
+
68
+ def remove(item)
69
+ return unless has?(item)
70
+
71
+ instance = instance?(item) ? item : fetch(item)
72
+ path = subscription_to_index(instance)
73
+ index.delete(instance.sid)
74
+
75
+ f_level = index[path[0]]
76
+ s_level = f_level[path[1]]
77
+ a_level = s_level[path[2]]
78
+
79
+ a_level.delete(instance.sid)
80
+ s_level.delete(path[2]) if a_level.empty?
81
+ f_level.delete(path[1]) if s_level.empty?
82
+ index.delete(path[0]) if f_level.empty?
83
+ end
84
+
85
+ def has?(item)
86
+ list.key?(instance?(item) ? item.sid : item)
87
+ end
88
+
89
+ def search(**xargs, &block)
90
+ xargs = serialize(**xargs)
91
+ field, scope, args = xargs.values_at(:field, :scope, :args)
92
+
93
+ if field.nil? && args.nil? && scope.nil?
94
+ list.each(&block) unless block.nil?
95
+ return all
96
+ end
97
+
98
+ [].tap do |result|
99
+ GraphQL.enumerate(field || index.keys).each do |key1|
100
+ GraphQL.enumerate(scope || index[key1].keys).each do |key2|
101
+ GraphQL.enumerate(args || index[key2].keys).each do |key3|
102
+ items = index.fetch(key1, nil)&.fetch(key2, nil)&.fetch(key3, nil)
103
+ items.each(&list.method(:[])).each(&block) unless block.nil?
104
+ result.concat(items || EMPTY_ARRAY)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ alias find_each search
112
+
113
+ protected
114
+
115
+ # Turn the request subscription into into the path of the index
116
+ def subscription_to_index(subscription)
117
+ [
118
+ hash_for(subscription.field),
119
+ possible_scopes(subscription.scope)&.first,
120
+ hash_for(subscription.args),
121
+ ]
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ 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
@@ -3,8 +3,8 @@
3
3
  require 'arel/visitors/visitor'
4
4
 
5
5
  # rubocop:disable Naming/MethodParameterName, Naming/MethodName
6
- module Rails # :nodoc:
7
- module GraphQL # :nodoc:
6
+ module Rails
7
+ module GraphQL
8
8
  # = GraphQL ToGQL
9
9
  #
10
10
  # This class can turn any class related to GraphQL into its GraphQL string
@@ -35,14 +35,13 @@ module Rails # :nodoc:
35
35
  collector ||= Collectors::IdentedCollector.new
36
36
  @with_descriptions = with_descriptions
37
37
  @with_spec = with_spec.nil? ? schema.introspection? : with_spec
38
+ @namespace = schema.namespace
38
39
 
39
- accept(schema, collector).eol
40
-
41
- GraphQL.type_map.each_from(schema.namespace, base_class: :Type)
40
+ GraphQL.type_map.each_from(@namespace, base_class: :Type)
42
41
  .group_by(&:kind).values_at(*DESCRIBE_TYPES)
43
- .each do |items|
42
+ .prepend([schema]).each do |items|
44
43
  items&.sort_by(&:gql_name)&.each do |item|
45
- next if !@with_spec && item.internal?
44
+ next if !@with_spec && item.try(:internal?)
46
45
 
47
46
  next visit_Rails_GraphQL_Type_Object(item, collector).eol \
48
47
  if item.is_a?(::OpenStruct) && item.object?
@@ -109,8 +108,9 @@ module Rails # :nodoc:
109
108
  visit_directives(o.directives, collector)
110
109
 
111
110
  collector.indented(' {', '}') do
112
- Helpers::WithSchemaFields::SCHEMA_FIELD_TYPES.each do |key, name|
111
+ Helpers::WithSchemaFields::TYPE_FIELD_CLASS.each_key do |key|
113
112
  next unless key.eql?(:query) || o.fields_for(key).present?
113
+ name = o.type_name_for(key)
114
114
 
115
115
  collector << key.to_s
116
116
  collector << ': '
@@ -152,14 +152,6 @@ module Rails # :nodoc:
152
152
  visit_Rails_GraphQL_Field(o, collector)
153
153
  end
154
154
 
155
- def visit_Rails_GraphQL_Field_OutputField(o, collector)
156
- visit_Rails_GraphQL_Field(o, collector)
157
- end
158
-
159
- def visit_Rails_GraphQL_Field_InputField(o, collector)
160
- visit_Rails_GraphQL_Field(o, collector)
161
- end
162
-
163
155
  def visit_Rails_GraphQL_Type_Enum(o, collector)
164
156
  visit_description(o, collector)
165
157
  collector << 'enum '
@@ -178,9 +170,7 @@ module Rails # :nodoc:
178
170
  collector << o.gql_name
179
171
  visit_directives(o.directives, collector)
180
172
 
181
- collector.indented(' {', '}') do
182
- o.fields.each_value { |x| visit(x, collector) }
183
- end
173
+ collector.indented(' {', '}') { visit_fields(o.fields, collector) }
184
174
  end
185
175
 
186
176
  def visit_Rails_GraphQL_Type_Interface(o, collector)
@@ -190,9 +180,7 @@ module Rails # :nodoc:
190
180
  collector << o.gql_name
191
181
  visit_directives(o.directives, collector)
192
182
 
193
- collector.indented(' {', '}') do
194
- o.fields.each_value { |x| visit(x, collector) }
195
- end
183
+ collector.indented(' {', '}') { visit_fields(o.fields, collector) }
196
184
  end
197
185
 
198
186
  def visit_Rails_GraphQL_Type_Object(o, collector)
@@ -211,9 +199,7 @@ module Rails # :nodoc:
211
199
 
212
200
  visit_directives(o.directives, collector)
213
201
 
214
- collector.indented(' {', '}') do
215
- o.fields.each_value { |x| visit(x, collector) }
216
- end
202
+ collector.indented(' {', '}') { visit_fields(o.fields, collector) }
217
203
  end
218
204
 
219
205
  def visit_Rails_GraphQL_Type_Scalar(o, collector)
@@ -260,8 +246,16 @@ module Rails # :nodoc:
260
246
  visit_typed_object(o, collector)
261
247
  end
262
248
 
249
+ def visit_fields(list, collector)
250
+ return if list.blank?
251
+
252
+ list.values.sort_by do |field|
253
+ field.name == :id ? '' : field.gql_name
254
+ end.each { |x| visit(x, collector) }
255
+ end
256
+
263
257
  def visit_arguments(list, collector)
264
- return if list.empty?
258
+ return if list.blank?
265
259
 
266
260
  indented = @with_descriptions && list.values.any?(&:description?)
267
261
 
@@ -292,15 +286,18 @@ module Rails # :nodoc:
292
286
  end
293
287
 
294
288
  def visit_description(o, collector)
295
- return unless @with_descriptions && o.description?
289
+ return unless @with_descriptions
290
+
291
+ args = o.method(:description).arity == 0 ? [] : [@namespace]
292
+ return if (desc = o.description(*args)).nil?
296
293
 
297
- if o.description.lines.size === 1
298
- collector << o.description.inspect
294
+ if desc.lines.size === 1
295
+ collector << desc.inspect
299
296
  else
300
297
  collector << '"""'
301
298
  collector.eol
302
299
 
303
- collector << o.description
300
+ collector << desc
304
301
  collector.eol
305
302
 
306
303
  collector << '"""'
@@ -344,7 +341,7 @@ module Rails # :nodoc:
344
341
  collector << ' = ' << o.to_json(o.default) if o.try(:default_value?)
345
342
  end
346
343
 
347
- def visit(object, collector = nil) # :nodoc:
344
+ def visit(object, collector = nil)
348
345
  object_class = object.is_a?(Module) ? object : object.class
349
346
  dispatch_method = dispatch[object_class]
350
347
  if collector
@@ -358,7 +355,7 @@ module Rails # :nodoc:
358
355
  respond_to?(dispatch[klass], true)
359
356
  end
360
357
 
361
- raise(::TypeError, "Cannot visit #{object_class}") unless superklass
358
+ raise(::TypeError, +"Cannot visit #{object_class}") unless superklass
362
359
  dispatch[object_class] = dispatch[superklass]
363
360
  retry
364
361
  end