graphiti 2.0.0.beta.2 → 2.0.0.beta.4

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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -86
  3. data/.github/workflows/docs.yml +60 -0
  4. data/.github/workflows/release.yml +8 -8
  5. data/.gitignore +7 -0
  6. data/.npmrc +9 -0
  7. data/.standard.yml +4 -1
  8. data/Appraisals +33 -32
  9. data/CHANGELOG.md +41 -0
  10. data/README.md +13 -2
  11. data/UPGRADING.md +2 -68
  12. data/docs/concepts/backends-and-models.md +122 -0
  13. data/docs/concepts/endpoints.md +183 -0
  14. data/docs/concepts/links.md +212 -0
  15. data/docs/concepts/overview.md +80 -0
  16. data/docs/concepts/persisting.md +376 -0
  17. data/docs/concepts/relationships.md +527 -0
  18. data/docs/concepts/resources.md +677 -0
  19. data/docs/getting-started/first-api.md +289 -0
  20. data/docs/getting-started/installation.md +185 -0
  21. data/docs/intro.md +307 -0
  22. data/docs/js/authentication.md +63 -0
  23. data/docs/js/ddau.md +20 -0
  24. data/docs/js/extra-params.md +41 -0
  25. data/docs/js/index.md +112 -0
  26. data/docs/js/installation.md +120 -0
  27. data/docs/js/middleware.md +72 -0
  28. data/docs/js/models.md +202 -0
  29. data/docs/js/reads.md +494 -0
  30. data/docs/js/state-syncing.md +100 -0
  31. data/docs/js/writes.md +373 -0
  32. data/docs/reference/vandal.md +63 -0
  33. data/docs/reference/why.md +13 -0
  34. data/docs/topics/authorization.md +155 -0
  35. data/docs/topics/caching.md +55 -0
  36. data/docs/topics/customizing-sideloads.md +156 -0
  37. data/docs/topics/debugging.md +216 -0
  38. data/docs/topics/error-handling.md +210 -0
  39. data/docs/topics/etags.md +46 -0
  40. data/docs/topics/hopping-relationships.md +149 -0
  41. data/docs/topics/json-attributes.md +77 -0
  42. data/docs/topics/openstruct-models.md +50 -0
  43. data/docs/topics/remote-resources.md +291 -0
  44. data/docs/topics/testing.md +894 -0
  45. data/docs/topics/without-activerecord.md +324 -0
  46. data/docs/tutorial/index.md +58 -0
  47. data/docs/tutorial/step_0.md +107 -0
  48. data/docs/tutorial/step_1.md +199 -0
  49. data/docs/tutorial/step_2.md +312 -0
  50. data/docs/tutorial/step_3.md +142 -0
  51. data/docs/tutorial/step_4.md +135 -0
  52. data/docs/tutorial/step_5.md +69 -0
  53. data/docs/tutorial/step_6.md +82 -0
  54. data/docs/tutorial/step_7.md +205 -0
  55. data/docs/tutorial/step_8.md +128 -0
  56. data/docs/tutorial/step_9.md +171 -0
  57. data/docs/upgrading.md +265 -0
  58. data/gemfiles/rails_7_1.gemfile +4 -3
  59. data/gemfiles/{rails_7_2_graphiti_rails.gemfile → rails_7_2.gemfile} +3 -3
  60. data/gemfiles/{rails_8_1_graphiti_rails.gemfile → rails_8_0.gemfile} +3 -3
  61. data/gemfiles/{rails_8_0_graphiti_rails.gemfile → rails_8_1.gemfile} +3 -3
  62. data/graphiti.gemspec +7 -5
  63. data/{deprecated_generators → lib/generators}/graphiti/api_test_generator.rb +7 -1
  64. data/{deprecated_generators → lib/generators}/graphiti/generator_mixin.rb +14 -1
  65. data/{deprecated_generators → lib/generators}/graphiti/install_generator.rb +19 -13
  66. data/{deprecated_generators → lib/generators}/graphiti/resource_generator.rb +43 -6
  67. data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
  68. data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
  69. data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
  70. data/lib/graphiti/configuration.rb +2 -2
  71. data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
  72. data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
  73. data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
  74. data/lib/graphiti/error_serializers/validation.rb +143 -0
  75. data/lib/graphiti/errors.rb +4 -23
  76. data/lib/graphiti/query.rb +1 -1
  77. data/lib/graphiti/rails/context.rb +33 -0
  78. data/lib/graphiti/rails/controller.rb +41 -0
  79. data/lib/graphiti/rails/debugging.rb +18 -0
  80. data/lib/graphiti/rails/exception_handlers.rb +77 -0
  81. data/lib/graphiti/rails/railtie.rb +139 -0
  82. data/lib/graphiti/rails/responders.rb +21 -0
  83. data/lib/graphiti/rails/test_helpers.rb +22 -0
  84. data/lib/graphiti/rails.rb +47 -29
  85. data/lib/graphiti/resource/configuration.rb +1 -0
  86. data/lib/graphiti/resource/interface.rb +2 -2
  87. data/lib/graphiti/resource/persistence.rb +14 -2
  88. data/lib/graphiti/resource/remote.rb +2 -2
  89. data/lib/graphiti/resource/sideloading.rb +1 -1
  90. data/lib/graphiti/resource.rb +13 -1
  91. data/lib/graphiti/responders.rb +7 -20
  92. data/lib/graphiti/schema.rb +5 -1
  93. data/lib/graphiti/schema_diff.rb +4 -0
  94. data/lib/graphiti/scope.rb +45 -37
  95. data/lib/graphiti/serializer.rb +6 -0
  96. data/lib/graphiti/sideload/belongs_to.rb +38 -5
  97. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
  98. data/lib/graphiti/sideload.rb +54 -35
  99. data/lib/graphiti/spec_helpers/errors.rb +73 -0
  100. data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
  101. data/lib/graphiti/spec_helpers/helpers.rb +107 -0
  102. data/lib/graphiti/spec_helpers/node.rb +88 -0
  103. data/lib/graphiti/spec_helpers/rspec.rb +147 -0
  104. data/lib/graphiti/spec_helpers.rb +53 -0
  105. data/lib/graphiti/util/include_params.rb +2 -2
  106. data/lib/graphiti/util/persistence.rb +10 -11
  107. data/lib/graphiti/util/serializer_relationships.rb +41 -5
  108. data/lib/graphiti/version.rb +1 -1
  109. data/lib/graphiti-rails.rb +11 -0
  110. data/lib/graphiti.rb +34 -10
  111. data/lib/graphiti_errors.rb +11 -0
  112. data/lib/graphiti_spec_helpers/rspec.rb +3 -0
  113. data/lib/graphiti_spec_helpers.rb +11 -0
  114. data/lib/{graphiti/deprecated_tasks.rb → tasks/graphiti.rake} +6 -1
  115. data/package-lock.json +6199 -0
  116. data/package.json +5 -4
  117. data/website/.gitignore +20 -0
  118. data/website/README.md +43 -0
  119. data/website/docusaurus.config.js +141 -0
  120. data/website/package-lock.json +19474 -0
  121. data/website/package.json +46 -0
  122. data/website/sidebars.js +82 -0
  123. data/website/src/css/custom.css +58 -0
  124. data/website/src/pages/markdown-page.mdx +7 -0
  125. data/website/static/.nojekyll +0 -0
  126. data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
  127. data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
  128. data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
  129. data/website/static/1.13/2019/10/14/tutorial.html +198 -0
  130. data/website/static/1.13/CNAME +1 -0
  131. data/website/static/1.13/README.md +16 -0
  132. data/website/static/1.13/assets/css/syntax.css +60 -0
  133. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  134. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  135. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  136. data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
  137. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  138. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  139. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  140. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  141. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
  142. data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
  143. data/website/static/1.13/assets/img/backend.gif +0 -0
  144. data/website/static/1.13/assets/img/conformity.png +0 -0
  145. data/website/static/1.13/assets/img/error_payload.png +0 -0
  146. data/website/static/1.13/assets/img/gh.png +0 -0
  147. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  148. data/website/static/1.13/assets/img/logo-500.png +0 -0
  149. data/website/static/1.13/assets/img/logo.png +0 -0
  150. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  151. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  152. data/website/static/1.13/assets/img/persist.jpg +0 -0
  153. data/website/static/1.13/assets/img/resource.gif +0 -0
  154. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  155. data/website/static/1.13/assets/img/rest1.gif +0 -0
  156. data/website/static/1.13/assets/img/rest2.gif +0 -0
  157. data/website/static/1.13/assets/img/rest3.gif +0 -0
  158. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  159. data/website/static/1.13/assets/img/why.png +0 -0
  160. data/website/static/1.13/assets/js/highlight.pack.js +2 -0
  161. data/website/static/1.13/assets/main.css +15518 -0
  162. data/website/static/1.13/assets/main.css.map +1 -0
  163. data/website/static/1.13/bin/bundle +109 -0
  164. data/website/static/1.13/bin/jekyll +27 -0
  165. data/website/static/1.13/bin/kramdown +27 -0
  166. data/website/static/1.13/bin/listen +27 -0
  167. data/website/static/1.13/bin/rake +27 -0
  168. data/website/static/1.13/bin/rougify +27 -0
  169. data/website/static/1.13/bin/safe_yaml +27 -0
  170. data/website/static/1.13/bin/sass +27 -0
  171. data/website/static/1.13/bin/sass-convert +27 -0
  172. data/website/static/1.13/bin/scss +27 -0
  173. data/website/static/1.13/blog.html +259 -0
  174. data/website/static/1.13/cheatsheet.html +316 -0
  175. data/website/static/1.13/cookbooks/authorization.md +0 -0
  176. data/website/static/1.13/cookbooks/caching.md +0 -0
  177. data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
  178. data/website/static/1.13/cookbooks/etags.md +0 -0
  179. data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
  180. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  181. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  182. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  183. data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
  184. data/website/static/1.13/features.html +249 -0
  185. data/website/static/1.13/feed.xml +106 -0
  186. data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
  187. data/website/static/1.13/guides/concepts/debugging.html +440 -0
  188. data/website/static/1.13/guides/concepts/endpoints.html +432 -0
  189. data/website/static/1.13/guides/concepts/error-handling.html +396 -0
  190. data/website/static/1.13/guides/concepts/links.html +501 -0
  191. data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
  192. data/website/static/1.13/guides/concepts/resources.html +2176 -0
  193. data/website/static/1.13/guides/concepts/testing.html +1469 -0
  194. data/website/static/1.13/guides/getting-started/installation.html +420 -0
  195. data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
  196. data/website/static/1.13/guides/index.html +269 -0
  197. data/website/static/1.13/guides/overview.html +325 -0
  198. data/website/static/1.13/guides/upgrading-2-0.html +193 -0
  199. data/website/static/1.13/guides/upgrading.html +314 -0
  200. data/website/static/1.13/guides/vandal.html +282 -0
  201. data/website/static/1.13/guides/why.html +1121 -0
  202. data/website/static/1.13/index.html +72 -0
  203. data/website/static/1.13/js/authentication.html +295 -0
  204. data/website/static/1.13/js/ddau.html +238 -0
  205. data/website/static/1.13/js/extra-params.html +270 -0
  206. data/website/static/1.13/js/index.html +321 -0
  207. data/website/static/1.13/js/installation.html +637 -0
  208. data/website/static/1.13/js/introduction.html +257 -0
  209. data/website/static/1.13/js/middleware.html +318 -0
  210. data/website/static/1.13/js/reads/fieldsets.html +271 -0
  211. data/website/static/1.13/js/reads/filtering.html +289 -0
  212. data/website/static/1.13/js/reads/includes.html +260 -0
  213. data/website/static/1.13/js/reads/index.html +497 -0
  214. data/website/static/1.13/js/reads/nested-queries.html +353 -0
  215. data/website/static/1.13/js/reads/pagination.html +260 -0
  216. data/website/static/1.13/js/reads/sorting.html +265 -0
  217. data/website/static/1.13/js/reads/statistics.html +289 -0
  218. data/website/static/1.13/js/state-syncing.html +340 -0
  219. data/website/static/1.13/js/writes/deferred.html +296 -0
  220. data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
  221. data/website/static/1.13/js/writes/index.html +391 -0
  222. data/website/static/1.13/js/writes/nested.html +330 -0
  223. data/website/static/1.13/js/writes/validations.html +272 -0
  224. data/website/static/1.13/quickstart.html +660 -0
  225. data/website/static/1.13/template +161 -0
  226. data/website/static/1.13/tutorial/index.html +250 -0
  227. data/website/static/1.13/tutorial/step_0.html +292 -0
  228. data/website/static/1.13/tutorial/step_1.html +517 -0
  229. data/website/static/1.13/tutorial/step_2.html +481 -0
  230. data/website/static/1.13/tutorial/step_3.html +323 -0
  231. data/website/static/1.13/tutorial/step_4.html +318 -0
  232. data/website/static/1.13/tutorial/step_5.html +265 -0
  233. data/website/static/1.13/tutorial/step_6.html +276 -0
  234. data/website/static/1.13/tutorial/step_7.html +390 -0
  235. data/website/static/1.13/tutorial/step_8.html +316 -0
  236. data/website/static/1.13/tutorial/step_9.html +365 -0
  237. data/website/static/assets/img/error_payload.png +0 -0
  238. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  239. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  240. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  241. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  242. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  243. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  244. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  245. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  246. data/website/static/assets/img/meta_total_count.png +0 -0
  247. data/website/static/img/docusaurus-social-card.jpg +0 -0
  248. data/website/static/img/docusaurus.png +0 -0
  249. data/website/static/img/favicon.ico +0 -0
  250. data/website/static/img/logo.png +0 -0
  251. data/website/static/img/logo.svg +1 -0
  252. data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
  253. data/website/static/img/undraw_docusaurus_react.svg +170 -0
  254. data/website/static/img/undraw_docusaurus_tree.svg +40 -0
  255. metadata +245 -46
  256. data/gemfiles/rails_6.gemfile +0 -18
  257. data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
  258. data/gemfiles/rails_7.gemfile +0 -18
  259. data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
  260. data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
  261. data/lib/graphiti/railtie.rb +0 -121
  262. /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
  263. /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
  264. /data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +0 -0
  265. /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
  266. /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
  267. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
  268. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
  269. /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
@@ -1,38 +1,56 @@
1
+ require "rescue_registry"
2
+ require "graphiti"
3
+ require "rails"
4
+
1
5
  module Graphiti
2
- # Rails Integration. Mix this in to ApplicationController.
3
- #
4
- # * Mixes in Base
5
- # * Adds a global around_action (see Base#wrap_context)
6
- #
7
- # @see Base#render_jsonapi
8
- # @see Base#wrap_context
6
+ # Rails integration for Graphiti. See {file:README.md} for more details.
9
7
  module Rails
8
+ # Graphiti::Rails was a controller mixin before it became this namespace.
9
+ # Including a namespace is not an error, it just does nothing, so honour
10
+ # the old spelling rather than leave a controller silently without its
11
+ # context, debugger and exception handlers.
10
12
  def self.included(klass)
11
- backtrace = ::Rails::VERSION::MAJOR == 4 ? caller(2) : caller_locations(2)
12
- Graphiti::DEPRECATOR.deprecation_warning("Including Graphiti::Rails", "Use graphiti-rails instead. See https://www.graphiti.dev/guides/graphiti-rails-migration for details.", backtrace)
13
-
14
- klass.class_eval do
15
- include Graphiti::Context
16
- include GraphitiErrors
17
- around_action :wrap_context
18
- around_action :debug
19
- end
20
- end
13
+ DEPRECATOR.deprecation_warning(
14
+ "Including Graphiti::Rails",
15
+ "include Graphiti::Rails::Controller instead"
16
+ )
21
17
 
22
- def wrap_context
23
- Graphiti.with_context(jsonapi_context, action_name.to_sym) do
24
- yield
25
- end
18
+ klass.include(Controller)
26
19
  end
27
20
 
28
- def debug
29
- Debugger.debug do
30
- yield
31
- end
32
- end
21
+ # graphiti-rails had its own, and apps that silenced it by name still resolve.
22
+ DEPRECATOR = Graphiti::DEPRECATOR
33
23
 
34
- def jsonapi_context
35
- self
36
- end
24
+ autoload :ConflictRequestHandler, "graphiti/rails/exception_handlers"
25
+ autoload :Context, "graphiti/rails/context"
26
+ autoload :Controller, "graphiti/rails/controller"
27
+ autoload :Debugging, "graphiti/rails/debugging"
28
+ autoload :ExceptionHandler, "graphiti/rails/exception_handlers"
29
+ autoload :FallbackHandler, "graphiti/rails/exception_handlers"
30
+ autoload :InvalidRequestHandler, "graphiti/rails/exception_handlers"
31
+ autoload :Responders, "graphiti/rails/responders"
32
+ autoload :TestHelpers, "graphiti/rails/test_helpers"
33
+
34
+ # @!attribute self.handled_exception_formats
35
+ # A list of formats as symbols whose exceptions will be handled by Graphiti. See {Railtie}.
36
+ cattr_accessor :handled_exception_formats, default: []
37
+
38
+ # @!attribute self.respond_to_formats
39
+ # A list of formats as symbols which will be available for Graphiti::Rails::Responders. See {Railtie}.
40
+ cattr_accessor :respond_to_formats, default: []
37
41
  end
42
+
43
+ # Deprecated. Was core's own responders mixin, superseded by the one that came
44
+ # in with graphiti-rails. Remove in 3.0.
45
+ Responders = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
46
+ "Graphiti::Responders",
47
+ "Graphiti::Rails::Responders",
48
+ DEPRECATOR
49
+ )
38
50
  end
51
+
52
+ ActiveSupport.on_load(:active_record) do
53
+ require "graphiti/adapters/active_record"
54
+ end
55
+
56
+ require "graphiti/rails/railtie"
@@ -90,6 +90,7 @@ module Graphiti
90
90
  :attributes_schema_by_default,
91
91
  :relationships_readable_by_default,
92
92
  :relationships_writable_by_default,
93
+ :always_include_resource_ids_by_default,
93
94
  :filters_accept_nil_by_default,
94
95
  :filters_deny_empty_by_default,
95
96
  :graphql_entrypoint,
@@ -84,8 +84,8 @@ module Graphiti
84
84
 
85
85
  if context&.respond_to?(:request)
86
86
  path = context.request.env["PATH_INFO"]
87
- unless allow_request?(path, params, context_namespace)
88
- raise Errors::InvalidEndpoint.new(self, path, context_namespace)
87
+ unless allow_request?(path, params, current_action)
88
+ raise Errors::InvalidEndpoint.new(self, path, current_action)
89
89
  end
90
90
  end
91
91
  end
@@ -93,11 +93,23 @@ module Graphiti
93
93
  model_instance
94
94
  end
95
95
 
96
+ # The model built by a prior ResourceProxy#assign_attributes, present
97
+ # for the duration of the save that persists it
98
+ attr_reader :assigned_model
99
+
100
+ # @api private
101
+ def with_assigned_model(model)
102
+ @assigned_model = model
103
+ yield
104
+ ensure
105
+ @assigned_model = nil
106
+ end
107
+
96
108
  # Attributes are assigned before the persistence callbacks fire, so
97
109
  # around_persistence receives the assigned model - its pre-yield
98
110
  # position is the last chance to touch the model before save, inside
99
111
  # the transaction. Modify attributes in before_attributes instead.
100
- def create(create_params, meta = nil, assigned_model: nil)
112
+ def create(create_params, meta = nil)
101
113
  model_instance = assigned_model || assign(create_params, meta, :create)
102
114
 
103
115
  run_callbacks :persistence, :create, model_instance, meta do
@@ -111,7 +123,7 @@ module Graphiti
111
123
  model_instance
112
124
  end
113
125
 
114
- def update(update_params, meta = nil, assigned_model: nil)
126
+ def update(update_params, meta = nil)
115
127
  model_instance = assigned_model || assign(update_params, meta, :update)
116
128
 
117
129
  run_callbacks :persistence, :update, model_instance, meta do
@@ -38,8 +38,8 @@ module Graphiti
38
38
  # Forward all headers
39
39
  def request_headers
40
40
  {}.tap do |headers|
41
- # TODO: Maybe handle this in graphiti-rails
42
- if defined?(Rails) && context
41
+ # TODO: Maybe handle this in the Rails integration
42
+ if defined?(::Rails) && context
43
43
  raw = context.request.headers.to_h
44
44
  if (auth = raw["HTTP_AUTHORIZATION"])
45
45
  headers["Authorization"] = auth
@@ -128,7 +128,7 @@ module Graphiti
128
128
  # If eager loading, ensure routes are loaded first, then apply
129
129
  # This happens in Railtie
130
130
  def eagerly_apply_sideload?(sideload)
131
- # TODO: Maybe handle this in graphiti-rails
131
+ # TODO: Maybe handle this in the Rails integration
132
132
  if defined?(::Rails) && (app = ::Rails.application)
133
133
  app.config.eager_load ? false : true
134
134
  else
@@ -51,10 +51,22 @@ module Graphiti
51
51
  self.class.context
52
52
  end
53
53
 
54
- def self.context_namespace
54
+ # Rails sets this from action_name, so it is whatever the controller action
55
+ # is called, not a fixed list. Persistence overrides it with :create/:update
56
+ # while saving, and :show while resolving sideloads afterwards.
57
+ def self.current_action
55
58
  Graphiti.context[:namespace]
56
59
  end
57
60
 
61
+ def current_action
62
+ self.class.current_action
63
+ end
64
+
65
+ def self.context_namespace
66
+ Graphiti::DEPRECATOR.deprecation_warning(:context_namespace, "Use #current_action instead")
67
+ current_action
68
+ end
69
+
58
70
  def context_namespace
59
71
  self.class.context_namespace
60
72
  end
@@ -1,21 +1,8 @@
1
- # If you're using Rails + responders gem to get respond_with
2
- module Graphiti
3
- module Responders
4
- extend ActiveSupport::Concern
1
+ # Deprecated entry point. graphiti required this path itself in 1.x, so an
2
+ # explicit require left behind would otherwise raise LoadError. Remove in 3.0.
3
+ require "graphiti/rails"
5
4
 
6
- included do
7
- backtrace = ::Rails::VERSION::MAJOR == 4 ? caller(2) : caller_locations(2)
8
- DEPRECATOR.deprecation_warning("Including Graphiti::Responders", "Use graphiti-rails instead. See https://www.graphiti.dev/guides/graphiti-rails-migration for details.", backtrace)
9
- include ActionController::MimeResponds
10
- respond_to(*Graphiti.config.respond_to)
11
- end
12
-
13
- # Override to avoid location url generation (for now)
14
- def respond_with(*args, &blk)
15
- opts = args.extract_options!
16
- opts[:location] = nil
17
- args << opts
18
- super(*args, &blk)
19
- end
20
- end
21
- end
5
+ Graphiti::DEPRECATOR.warn(
6
+ 'require "graphiti/responders" is deprecated. Use "graphiti/rails" and include ' \
7
+ "Graphiti::Rails::Responders."
8
+ )
@@ -3,7 +3,7 @@ module Graphiti
3
3
  attr_reader :resources
4
4
 
5
5
  def self.generate(resources = nil)
6
- # TODO: Maybe handle this in graphiti-rails
6
+ # TODO: Maybe handle this in the Rails integration
7
7
  ::Rails.application.eager_load! if defined?(::Rails)
8
8
  resources ||= Graphiti.resources.reject(&:abstract_class?)
9
9
  resources.reject! { |r| r.name.nil? }
@@ -251,6 +251,10 @@ module Graphiti
251
251
  schema[:guard] = true
252
252
  end
253
253
 
254
+ if config.always_include_resource_ids?
255
+ schema[:linkage] = true
256
+ end
257
+
254
258
  r[name] = schema
255
259
  end
256
260
  end
@@ -106,6 +106,10 @@ module Graphiti
106
106
  @errors << "#{old_resource[:name]}: relationship #{name.inspect} became guarded."
107
107
  end
108
108
 
109
+ if old_rel[:linkage] && !new_rel[:linkage]
110
+ @errors << "#{old_resource[:name]}: relationship #{name.inspect} no longer includes resource linkage."
111
+ end
112
+
109
113
  if new_rel[:resource] != old_rel[:resource]
110
114
  @errors << "#{old_resource[:name]}: relationship #{name.inspect} changed resource from #{old_rel[:resource]} to #{new_rel[:resource]}."
111
115
  end
@@ -48,29 +48,25 @@ module Graphiti
48
48
  # Thread/Fiber storage snapshots, and Rails executor wrappers on every
49
49
  # request purely to drive a thread pool that is intentionally synchronous.
50
50
  # See https://github.com/graphiti-api/graphiti/issues/505
51
- return sync_resolve(&blk) unless Graphiti.config.concurrency
52
-
53
- future_resolve.value!
51
+ if Graphiti.config.concurrency
52
+ future_resolve(&blk).value!
53
+ else
54
+ sync_resolve(&blk)
55
+ end
54
56
  end
55
57
 
56
58
  def resolve_sideloads(results)
57
- return sync_resolve_sideloads(results) unless Graphiti.config.concurrency
58
-
59
- future_resolve_sideloads(results).value!
59
+ if Graphiti.config.concurrency
60
+ future_resolve_sideloads(results).value!
61
+ else
62
+ sync_resolve_sideloads(results)
63
+ end
60
64
  end
61
65
 
62
- def future_resolve
66
+ def future_resolve(&blk)
63
67
  return Concurrent::Promises.fulfilled_future([], self.class.global_thread_pool_executor) if @query.zero_results?
64
68
 
65
- resolved = broadcast_data { |payload|
66
- @object = @resource.before_resolve(@object, @query)
67
- payload[:results] = @resource.resolve(@object)
68
- payload[:results]
69
- }
70
- resolved.compact!
71
- assign_serializer(resolved)
72
- yield resolved if block_given?
73
- @opts[:after_resolve]&.call(resolved)
69
+ resolved = resolve_primary_data(&blk)
74
70
  sideloaded = @query.parents.any?
75
71
  close_adapter = Graphiti.config.concurrency && sideloaded
76
72
  if close_adapter
@@ -125,9 +121,33 @@ module Graphiti
125
121
  # Synchronous counterpart to #future_resolve, used when concurrency is off.
126
122
  # Resolves the resource and its sideloads inline without any promise
127
123
  # machinery. See #resolve.
128
- def sync_resolve
124
+ def sync_resolve(&blk)
129
125
  return [] if @query.zero_results?
130
126
 
127
+ resolved = resolve_primary_data(&blk)
128
+ sync_resolve_sideloads(resolved)
129
+ resolved
130
+ end
131
+
132
+ # Synchronous counterpart to #future_resolve_sideloads, used when
133
+ # concurrency is off. Resolves each sideload inline. See #resolve_sideloads.
134
+ def sync_resolve_sideloads(results)
135
+ return if results == []
136
+
137
+ each_applicable_sideload do |sideload, sideload_query|
138
+ Graphiti.config.before_sideload&.call(Graphiti.context)
139
+ sideload.resolve(results, sideload_query, @resource)
140
+ end
141
+
142
+ # Match pre-1.8 semantics: the non-concurrent resolve_sideloads returned
143
+ # nil (not the sideloads Hash). Callers don't rely on the return value.
144
+ nil
145
+ end
146
+
147
+ # Resolve this scope's own data: run hooks, resolve the resource, and
148
+ # decorate the results. Shared by the sync and future paths — everything
149
+ # here runs inline on the calling thread in both modes.
150
+ def resolve_primary_data
131
151
  resolved = broadcast_data { |payload|
132
152
  @object = @resource.before_resolve(@object, @query)
133
153
  payload[:results] = @resource.resolve(@object)
@@ -137,40 +157,28 @@ module Graphiti
137
157
  assign_serializer(resolved)
138
158
  yield resolved if block_given?
139
159
  @opts[:after_resolve]&.call(resolved)
140
- sync_resolve_sideloads(resolved) unless @query.sideloads.empty?
141
160
  resolved
142
161
  end
143
162
 
144
- # Synchronous counterpart to #future_resolve_sideloads, used when
145
- # concurrency is off. Resolves each sideload inline. See #resolve_sideloads.
146
- def sync_resolve_sideloads(results)
147
- return if results == []
148
-
149
- @query.sideloads.each_pair do |name, q|
163
+ def each_applicable_sideload
164
+ @query.sideloads.each_pair do |name, sideload_query|
150
165
  sideload = @resource.class.sideload(name)
151
166
  next if sideload.nil? || sideload.shared_remote?
152
167
 
153
- Graphiti.config.before_sideload&.call(Graphiti.context)
154
- sideload.resolve(results, q, @resource)
168
+ yield sideload, sideload_query
155
169
  end
156
-
157
- # Match pre-1.8 semantics: the non-concurrent resolve_sideloads returned
158
- # nil (not the sideloads Hash). Callers don't rely on the return value.
159
- nil
160
170
  end
161
171
 
162
172
  def future_resolve_sideloads(results)
163
173
  return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
164
174
 
165
- sideload_promises = @query.sideloads.filter_map do |name, q|
166
- sideload = @resource.class.sideload(name)
167
- next if sideload.nil? || sideload.shared_remote?
168
-
169
- p = future_with_context(results, q, @resource) do |parent_results, sideload_query, parent_resource|
175
+ sideload_promises = []
176
+ each_applicable_sideload do |sideload, sideload_query|
177
+ promise = future_with_context(results, sideload_query, @resource) do |parent_results, future_query, parent_resource|
170
178
  Graphiti.config.before_sideload&.call(Graphiti.context)
171
- sideload.future_resolve(parent_results, sideload_query, parent_resource)
179
+ sideload.future_resolve(parent_results, future_query, parent_resource)
172
180
  end
173
- p.flat
181
+ sideload_promises << promise.flat
174
182
  end
175
183
 
176
184
  Concurrent::Promises.zip_futures_on(self.class.global_thread_pool_executor, *sideload_promises)
@@ -13,10 +13,16 @@ module Graphiti
13
13
  class_attribute :attributes_applied_via_resource
14
14
  class_attribute :extra_attributes_applied_via_resource
15
15
  class_attribute :relationship_condition_blocks
16
+ # Which sideload each generated relationship block was built for. A
17
+ # subclass inherits its parent's blocks, so this is how a redeclared
18
+ # relationship is told apart from one already applied, and from one the
19
+ # application wrote by hand.
20
+ class_attribute :relationship_sideloads
16
21
  self.attributes_applied_via_resource = []
17
22
  self.extra_attributes_applied_via_resource = []
18
23
  # See #requested_relationships
19
24
  self.relationship_condition_blocks ||= {}
25
+ self.relationship_sideloads ||= {}
20
26
 
21
27
  def self.inherited(klass)
22
28
  super
@@ -1,13 +1,46 @@
1
1
  class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
2
- def initialize(name, opts)
3
- opts = {always_include_resource_ids: false}.merge(opts)
4
- super(name, opts)
5
- end
6
-
7
2
  def type
8
3
  :belongs_to
9
4
  end
10
5
 
6
+ def default_include_resource_ids?
7
+ linkage_from_foreign_key?
8
+ end
9
+
10
+ # The parent already carries the foreign key, and for a plain belongs_to
11
+ # that key *is* the related id, so linkage costs nothing. Anything that can
12
+ # change which record the relationship resolves to, or what type it carries,
13
+ # has to load the association instead:
14
+ #
15
+ # - a scope/params block or a base_scope can filter out the record the
16
+ # foreign key points at, so the key would claim a relationship the API
17
+ # would not actually return
18
+ # - a polymorphic target takes its type from the record, not from the
19
+ # relationship, so the key alone cannot say what type the id has
20
+ # - a remote resource has no local foreign key to read
21
+ # - a custom primary_key points the relationship at some other column, so
22
+ # the key holds that column's value rather than the related id
23
+ def linkage_from_foreign_key?
24
+ # Ask before resolving #resource: an unreadable relationship renders
25
+ # nothing, and its resource class may not even be inferrable.
26
+ return false unless readable?
27
+ return false unless foreign_key_is_related_id?
28
+ return false if polymorphic_child?
29
+ return false if self.class.scope_proc || self.class.params_proc
30
+ return false if @base_scope
31
+ return false if remote?
32
+ return false if resource.class.polymorphic.present?
33
+
34
+ true
35
+ end
36
+
37
+ # The foreign key can stand in for the related id only when the two hold the
38
+ # same value. base_filter matches the key against primary_key, so pointing
39
+ # that at another column means the key holds that column instead.
40
+ def foreign_key_is_related_id?
41
+ primary_key == :id
42
+ end
43
+
11
44
  def load_params(parents, query)
12
45
  query.hash.tap do |hash|
13
46
  hash[:filter] ||= {}
@@ -108,40 +108,44 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
108
108
  end
109
109
 
110
110
  def resolve(parents, query, graph_parent)
111
- return future_resolve(parents, query, graph_parent).value! if Graphiti.config.concurrency
111
+ if Graphiti.config.concurrency
112
+ future_resolve(parents, query, graph_parent).value!
113
+ else
114
+ sync_resolve(parents, query, graph_parent)
115
+ end
116
+ end
112
117
 
113
- parents.group_by(&grouper.field_name).each_pair do |group_name, group|
114
- next if group_name.nil? || grouper.ignore?(group_name)
118
+ def future_resolve(parents, query, graph_parent)
119
+ promises = []
120
+ each_resolvable_group(parents, query) do |child, group, child_query|
121
+ promises << child.future_resolve(group, child_query, graph_parent)
122
+ end
123
+ Concurrent::Promises.zip(*promises)
124
+ end
115
125
 
116
- match = ->(c) { c.group_name == group_name.to_sym }
117
- if (sideload = children.values.find(&match))
118
- duped = remove_invalid_sideloads(sideload.resource, query)
119
- sideload.resolve(group, duped, graph_parent)
120
- else
121
- err = ::Graphiti::Errors::PolymorphicSideloadChildNotFound
122
- raise err.new(self, group_name)
123
- end
126
+ private
127
+
128
+ def sync_resolve(parents, query, graph_parent)
129
+ each_resolvable_group(parents, query) do |child, group, child_query|
130
+ child.resolve(group, child_query, graph_parent)
124
131
  end
125
132
  end
126
133
 
127
- def future_resolve(parents, query, graph_parent)
128
- promises = parents.group_by(&grouper.field_name).filter_map do |(group_name, group)|
134
+ # Group parents by their polymorphic type and yield each group's child
135
+ # sideload alongside a query pruned to the sideloads that child supports.
136
+ def each_resolvable_group(parents, query)
137
+ parents.group_by(&grouper.field_name).each_pair do |group_name, group|
129
138
  next if group_name.nil? || grouper.ignore?(group_name)
130
139
 
131
- match = ->(c) { c.group_name == group_name.to_sym }
132
- if (sideload = children.values.find(&match))
133
- duped = remove_invalid_sideloads(sideload.resource, query)
134
- sideload.future_resolve(group, duped, graph_parent)
135
- else
136
- err = ::Graphiti::Errors::PolymorphicSideloadChildNotFound
137
- raise err.new(self, group_name)
140
+ child = children.values.find { |candidate| candidate.group_name == group_name.to_sym }
141
+ unless child
142
+ raise ::Graphiti::Errors::PolymorphicSideloadChildNotFound.new(self, group_name)
138
143
  end
144
+
145
+ yield child, group, remove_invalid_sideloads(child.resource, query)
139
146
  end
140
- Concurrent::Promises.zip(*promises)
141
147
  end
142
148
 
143
- private
144
-
145
149
  # We may be requesting a relationship that some subclasses support,
146
150
  # but not others. Remove anything we don't support.
147
151
  # TODO: spec to ensure this dupe logic doesn't mutate the original
@@ -122,8 +122,25 @@ module Graphiti
122
122
  !!@polymorphic_as
123
123
  end
124
124
 
125
+ # False everywhere but a plain belongs_to - see
126
+ # Sideload::BelongsTo#linkage_from_foreign_key?.
127
+ def linkage_from_foreign_key?
128
+ false
129
+ end
130
+
131
+ # nil at either of the first two levels means "not specified" rather
132
+ # than "false".
125
133
  def always_include_resource_ids?
126
- !!@always_include_resource_ids
134
+ return !!@always_include_resource_ids unless @always_include_resource_ids.nil?
135
+
136
+ configured = parent_resource_class&.always_include_resource_ids_by_default
137
+ return !!configured unless configured.nil?
138
+
139
+ default_include_resource_ids?
140
+ end
141
+
142
+ def default_include_resource_ids?
143
+ false
127
144
  end
128
145
 
129
146
  def link?
@@ -292,48 +309,19 @@ module Graphiti
292
309
  children.replace(associated) if track_associated
293
310
  end
294
311
 
295
- # Synchronous counterpart to #future_resolve, used when concurrency is off.
296
- # Mirrors #future_resolve but resolves inline via the synchronous
297
- # Scope#resolve / #load paths (no promises). See Scope#sync_resolve_sideloads.
298
312
  def resolve(parents, query, graph_parent)
299
- return future_resolve(parents, query, graph_parent).value! if Graphiti.config.concurrency
300
-
301
- if single? && parents.length > 1
302
- raise Errors::SingularSideload.new(self, parents.length)
303
- end
304
-
305
- if self.class.scope_proc
306
- sideload_scope = fire_scope(parents)
307
- sideload_scope = Scope.new sideload_scope,
308
- resource,
309
- query,
310
- parent: graph_parent,
311
- sideload: self,
312
- sideload_parent_length: parents.length,
313
- default_paginate: false
314
- sideload_scope.resolve do |sideload_results|
315
- fire_assign(parents, sideload_results)
316
- end
313
+ if Graphiti.config.concurrency
314
+ future_resolve(parents, query, graph_parent).value!
317
315
  else
318
- load(parents, query, graph_parent)
316
+ sync_resolve(parents, query, graph_parent)
319
317
  end
320
318
  end
321
319
 
322
320
  def future_resolve(parents, query, graph_parent)
323
- if single? && parents.length > 1
324
- raise Errors::SingularSideload.new(self, parents.length)
325
- end
321
+ assert_singular!(parents)
326
322
 
327
323
  if self.class.scope_proc
328
- sideload_scope = fire_scope(parents)
329
- sideload_scope = Scope.new sideload_scope,
330
- resource,
331
- query,
332
- parent: graph_parent,
333
- sideload: self,
334
- sideload_parent_length: parents.length,
335
- default_paginate: false
336
- sideload_scope.future_resolve do |sideload_results|
324
+ build_sideload_scope(parents, query, graph_parent).future_resolve do |sideload_results|
337
325
  fire_assign(parents, sideload_results)
338
326
  end
339
327
  else
@@ -401,6 +389,37 @@ module Graphiti
401
389
 
402
390
  private
403
391
 
392
+ # Synchronous counterpart to #future_resolve, used when concurrency is off.
393
+ # Resolves inline via the synchronous Scope#resolve / #load paths (no
394
+ # promises). See Scope#sync_resolve_sideloads.
395
+ def sync_resolve(parents, query, graph_parent)
396
+ assert_singular!(parents)
397
+
398
+ if self.class.scope_proc
399
+ build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
400
+ fire_assign(parents, sideload_results)
401
+ end
402
+ else
403
+ load(parents, query, graph_parent)
404
+ end
405
+ end
406
+
407
+ def assert_singular!(parents)
408
+ if single? && parents.length > 1
409
+ raise Errors::SingularSideload.new(self, parents.length)
410
+ end
411
+ end
412
+
413
+ def build_sideload_scope(parents, query, graph_parent)
414
+ Scope.new fire_scope(parents),
415
+ resource,
416
+ query,
417
+ parent: graph_parent,
418
+ sideload: self,
419
+ sideload_parent_length: parents.length,
420
+ default_paginate: false
421
+ end
422
+
404
423
  def future_load(parents, query, graph_parent)
405
424
  proxy = build_resource_proxy(parents, query, graph_parent)
406
425
  proxy.respond_to?(:future_resolve_data) ? proxy.future_resolve_data : Concurrent::Promises.fulfilled_future(proxy)