graphiti 2.0.0.beta.3 → 2.0.0.beta.5

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -86
  3. data/.github/workflows/docs.yml +57 -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 +48 -0
  10. data/README.md +13 -2
  11. data/UPGRADING.md +2 -58
  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 +260 -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 -23
  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 +68 -7
  67. data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +1 -1
  68. data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
  69. data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
  70. data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
  71. data/lib/graphiti/configuration.rb +2 -2
  72. data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
  73. data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
  74. data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
  75. data/lib/graphiti/error_serializers/validation.rb +143 -0
  76. data/lib/graphiti/errors.rb +4 -4
  77. data/lib/graphiti/query.rb +1 -1
  78. data/lib/graphiti/rails/context.rb +33 -0
  79. data/lib/graphiti/rails/controller.rb +41 -0
  80. data/lib/graphiti/rails/debugging.rb +18 -0
  81. data/lib/graphiti/rails/exception_handlers.rb +77 -0
  82. data/lib/graphiti/rails/railtie.rb +139 -0
  83. data/lib/graphiti/rails/rake_helpers.rb +42 -0
  84. data/lib/graphiti/rails/responders.rb +21 -0
  85. data/lib/graphiti/rails/test_helpers.rb +22 -0
  86. data/lib/graphiti/rails.rb +47 -29
  87. data/lib/graphiti/resource/configuration.rb +1 -0
  88. data/lib/graphiti/resource/interface.rb +2 -2
  89. data/lib/graphiti/resource/remote.rb +2 -2
  90. data/lib/graphiti/resource/sideloading.rb +1 -1
  91. data/lib/graphiti/resource.rb +13 -1
  92. data/lib/graphiti/responders.rb +7 -20
  93. data/lib/graphiti/schema.rb +5 -1
  94. data/lib/graphiti/schema_diff.rb +4 -0
  95. data/lib/graphiti/scope.rb +45 -37
  96. data/lib/graphiti/serializer.rb +6 -0
  97. data/lib/graphiti/sideload/belongs_to.rb +38 -5
  98. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
  99. data/lib/graphiti/sideload.rb +54 -35
  100. data/lib/graphiti/spec_helpers/errors.rb +73 -0
  101. data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
  102. data/lib/graphiti/spec_helpers/helpers.rb +107 -0
  103. data/lib/graphiti/spec_helpers/matchers.rb +196 -0
  104. data/lib/graphiti/spec_helpers/node.rb +88 -0
  105. data/lib/graphiti/spec_helpers/rspec.rb +148 -0
  106. data/lib/graphiti/spec_helpers.rb +54 -0
  107. data/lib/graphiti/util/include_params.rb +2 -2
  108. data/lib/graphiti/util/serializer_relationships.rb +41 -5
  109. data/lib/graphiti/version.rb +1 -1
  110. data/lib/graphiti-rails.rb +11 -0
  111. data/lib/graphiti.rb +34 -10
  112. data/lib/graphiti_errors.rb +11 -0
  113. data/lib/graphiti_spec_helpers/rspec.rb +3 -0
  114. data/lib/graphiti_spec_helpers.rb +11 -0
  115. data/lib/tasks/graphiti.rake +29 -0
  116. data/package-lock.json +6199 -0
  117. data/package.json +5 -4
  118. data/website/.gitignore +20 -0
  119. data/website/README.md +43 -0
  120. data/website/docusaurus.config.js +197 -0
  121. data/website/package-lock.json +19474 -0
  122. data/website/package.json +46 -0
  123. data/website/sidebars.js +82 -0
  124. data/website/src/css/custom.css +85 -0
  125. data/website/src/pages/markdown-page.mdx +7 -0
  126. data/website/static/.nojekyll +0 -0
  127. data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
  128. data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
  129. data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
  130. data/website/static/1.13/2019/10/14/tutorial.html +198 -0
  131. data/website/static/1.13/CNAME +1 -0
  132. data/website/static/1.13/README.md +16 -0
  133. data/website/static/1.13/assets/css/syntax.css +60 -0
  134. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  135. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  136. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  137. data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
  138. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  139. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  140. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  141. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  142. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
  143. data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
  144. data/website/static/1.13/assets/img/backend.gif +0 -0
  145. data/website/static/1.13/assets/img/conformity.png +0 -0
  146. data/website/static/1.13/assets/img/error_payload.png +0 -0
  147. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  148. data/website/static/1.13/assets/img/gh.png +0 -0
  149. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  150. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  151. data/website/static/1.13/assets/img/logo-500.png +0 -0
  152. data/website/static/1.13/assets/img/logo.png +0 -0
  153. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  154. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  155. data/website/static/1.13/assets/img/persist.jpg +0 -0
  156. data/website/static/1.13/assets/img/resource.gif +0 -0
  157. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  158. data/website/static/1.13/assets/img/rest1.gif +0 -0
  159. data/website/static/1.13/assets/img/rest2.gif +0 -0
  160. data/website/static/1.13/assets/img/rest3.gif +0 -0
  161. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  162. data/website/static/1.13/assets/img/sunrise.png +0 -0
  163. data/website/static/1.13/assets/img/why.png +0 -0
  164. data/website/static/1.13/assets/js/highlight.pack.js +2 -0
  165. data/website/static/1.13/assets/main.css +15518 -0
  166. data/website/static/1.13/assets/main.css.map +1 -0
  167. data/website/static/1.13/bin/bundle +109 -0
  168. data/website/static/1.13/bin/jekyll +27 -0
  169. data/website/static/1.13/bin/kramdown +27 -0
  170. data/website/static/1.13/bin/listen +27 -0
  171. data/website/static/1.13/bin/rake +27 -0
  172. data/website/static/1.13/bin/rougify +27 -0
  173. data/website/static/1.13/bin/safe_yaml +27 -0
  174. data/website/static/1.13/bin/sass +27 -0
  175. data/website/static/1.13/bin/sass-convert +27 -0
  176. data/website/static/1.13/bin/scss +27 -0
  177. data/website/static/1.13/blog.html +259 -0
  178. data/website/static/1.13/cheatsheet.html +316 -0
  179. data/website/static/1.13/cookbooks/authorization.md +0 -0
  180. data/website/static/1.13/cookbooks/caching.md +0 -0
  181. data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
  182. data/website/static/1.13/cookbooks/etags.md +0 -0
  183. data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
  184. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  185. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  186. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  187. data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
  188. data/website/static/1.13/features.html +249 -0
  189. data/website/static/1.13/feed.xml +106 -0
  190. data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
  191. data/website/static/1.13/guides/concepts/debugging.html +440 -0
  192. data/website/static/1.13/guides/concepts/endpoints.html +432 -0
  193. data/website/static/1.13/guides/concepts/error-handling.html +396 -0
  194. data/website/static/1.13/guides/concepts/links.html +501 -0
  195. data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
  196. data/website/static/1.13/guides/concepts/resources.html +2176 -0
  197. data/website/static/1.13/guides/concepts/testing.html +1469 -0
  198. data/website/static/1.13/guides/getting-started/installation.html +420 -0
  199. data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
  200. data/website/static/1.13/guides/index.html +269 -0
  201. data/website/static/1.13/guides/overview.html +325 -0
  202. data/website/static/1.13/guides/upgrading-2-0.html +193 -0
  203. data/website/static/1.13/guides/upgrading.html +314 -0
  204. data/website/static/1.13/guides/vandal.html +282 -0
  205. data/website/static/1.13/guides/why.html +1121 -0
  206. data/website/static/1.13/index.html +72 -0
  207. data/website/static/1.13/js/authentication.html +295 -0
  208. data/website/static/1.13/js/ddau.html +238 -0
  209. data/website/static/1.13/js/extra-params.html +270 -0
  210. data/website/static/1.13/js/index.html +321 -0
  211. data/website/static/1.13/js/installation.html +637 -0
  212. data/website/static/1.13/js/introduction.html +257 -0
  213. data/website/static/1.13/js/middleware.html +318 -0
  214. data/website/static/1.13/js/reads/fieldsets.html +271 -0
  215. data/website/static/1.13/js/reads/filtering.html +289 -0
  216. data/website/static/1.13/js/reads/includes.html +260 -0
  217. data/website/static/1.13/js/reads/index.html +497 -0
  218. data/website/static/1.13/js/reads/nested-queries.html +353 -0
  219. data/website/static/1.13/js/reads/pagination.html +260 -0
  220. data/website/static/1.13/js/reads/sorting.html +265 -0
  221. data/website/static/1.13/js/reads/statistics.html +289 -0
  222. data/website/static/1.13/js/state-syncing.html +340 -0
  223. data/website/static/1.13/js/writes/deferred.html +296 -0
  224. data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
  225. data/website/static/1.13/js/writes/index.html +391 -0
  226. data/website/static/1.13/js/writes/nested.html +330 -0
  227. data/website/static/1.13/js/writes/validations.html +272 -0
  228. data/website/static/1.13/quickstart.html +660 -0
  229. data/website/static/1.13/template +161 -0
  230. data/website/static/1.13/tutorial/index.html +250 -0
  231. data/website/static/1.13/tutorial/step_0.html +292 -0
  232. data/website/static/1.13/tutorial/step_1.html +517 -0
  233. data/website/static/1.13/tutorial/step_2.html +481 -0
  234. data/website/static/1.13/tutorial/step_3.html +323 -0
  235. data/website/static/1.13/tutorial/step_4.html +318 -0
  236. data/website/static/1.13/tutorial/step_5.html +265 -0
  237. data/website/static/1.13/tutorial/step_6.html +276 -0
  238. data/website/static/1.13/tutorial/step_7.html +390 -0
  239. data/website/static/1.13/tutorial/step_8.html +316 -0
  240. data/website/static/1.13/tutorial/step_9.html +365 -0
  241. data/website/static/CNAME +1 -0
  242. data/website/static/assets/img/error_payload.png +0 -0
  243. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  244. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  245. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  246. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  247. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  248. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  249. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  250. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  251. data/website/static/assets/img/meta_total_count.png +0 -0
  252. data/website/static/img/docusaurus-social-card.jpg +0 -0
  253. data/website/static/img/docusaurus.png +0 -0
  254. data/website/static/img/favicon.ico +0 -0
  255. data/website/static/img/logo.png +0 -0
  256. data/website/static/img/logo.svg +1 -0
  257. data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
  258. data/website/static/img/undraw_docusaurus_react.svg +170 -0
  259. data/website/static/img/undraw_docusaurus_tree.svg +40 -0
  260. metadata +251 -54
  261. data/gemfiles/rails_6.gemfile +0 -18
  262. data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
  263. data/gemfiles/rails_7.gemfile +0 -18
  264. data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
  265. data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
  266. data/lib/graphiti/deprecated_tasks.rb +0 -49
  267. data/lib/graphiti/railtie.rb +0 -121
  268. /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
  269. /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
  270. /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
  271. /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
  272. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
  273. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
  274. /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Graphiti
4
+ module Rails
5
+ # This Railtie exposes some configuration options:
6
+ # * `config.graphiti.handled_exception_formats`, defaulting to `[:jsonapi]`.
7
+ # Formats in this list will always have their exceptions handled by Graphiti.
8
+ # * `config.graphiti.respond_to_formats`, defaulting to `[:json, :jsonapi, :xml]`.
9
+ # When {Graphiti::Rails::Responders} is included in a controller it will respond
10
+ # to these mime types by default.
11
+ class Railtie < ::Rails::Railtie
12
+ config.graphiti = ActiveSupport::OrderedOptions.new
13
+
14
+ config.graphiti.handled_exception_formats = [:jsonapi]
15
+
16
+ old_respond_to_formats = Graphiti::DEPRECATOR.silence { Graphiti.config.try(:respond_to) }
17
+ config.graphiti.respond_to_formats = old_respond_to_formats || [:json, :jsonapi, :xml]
18
+
19
+ rake_tasks do
20
+ load File.expand_path("../../tasks/graphiti.rake", __dir__)
21
+ end
22
+
23
+ initializer "graphiti-rails.config" do |app|
24
+ Graphiti::Rails.handled_exception_formats = app.config.graphiti.handled_exception_formats
25
+ Graphiti::Rails.respond_to_formats = app.config.graphiti.respond_to_formats
26
+
27
+ root = ::Rails.root
28
+
29
+ config_file = root.join(".graphiticfg.yml")
30
+ if config_file.exist?
31
+ cfg = YAML.load_file(config_file)
32
+ Graphiti.config.schema_path = root.join("public#{cfg["namespace"]}/schema.json")
33
+ else
34
+ Graphiti.config.schema_path = root.join("public/schema.json")
35
+ end
36
+ end
37
+
38
+ initializer "graphti-rails.logger" do
39
+ config.after_initialize do
40
+ Graphiti.config.debug = ::Rails.logger.debug? && Graphiti.config.debug
41
+ Graphiti.logger = ::Rails.logger
42
+ end
43
+ end
44
+
45
+ initializer "graphiti-rails.init" do
46
+ if ::Rails.application.config.eager_load
47
+ config.after_initialize do |app|
48
+ ::Rails.application.reload_routes!
49
+ Graphiti.setup!
50
+ end
51
+ end
52
+
53
+ register_mime_type
54
+ register_parameter_parser
55
+ register_renderers
56
+ establish_concurrency
57
+ configure_endpoint_lookup
58
+ end
59
+
60
+ # from jsonapi-rails
61
+ MEDIA_TYPE = "application/vnd.api+json"
62
+
63
+ PARSER = lambda do |body|
64
+ data = JSON.parse(body)
65
+ data[:format] = :jsonapi
66
+ data.with_indifferent_access
67
+ end
68
+
69
+ def register_mime_type
70
+ Mime::Type.register(MEDIA_TYPE, :jsonapi)
71
+ end
72
+
73
+ def register_parameter_parser
74
+ ActionDispatch::Request.parameter_parsers[:jsonapi] ||= PARSER
75
+ end
76
+
77
+ def register_renderers
78
+ ActiveSupport.on_load(:action_controller) do
79
+ ::ActionController::Renderers.add(:jsonapi) do |proxy, options|
80
+ self.content_type ||= Mime[:jsonapi]
81
+
82
+ # opts = {}
83
+ # if respond_to?(:default_jsonapi_render_options)
84
+ # opts = default_jsonapi_render_options
85
+ # end
86
+
87
+ if proxy.is_a?(Hash) # for destroy
88
+ render(options.merge(json: proxy))
89
+ else
90
+ proxy.to_jsonapi(options)
91
+ end
92
+ end
93
+ end
94
+
95
+ ActiveSupport.on_load(:action_controller) do
96
+ ActionController::Renderers.add(:jsonapi_errors) do |proxy, options|
97
+ self.content_type ||= Mime[:jsonapi]
98
+
99
+ validation = Graphiti::ErrorSerializers::Validation.new \
100
+ proxy.data, proxy.payload.relationships
101
+
102
+ render \
103
+ json: {errors: validation.errors},
104
+ status: :unprocessable_entity
105
+ end
106
+ end
107
+ end
108
+
109
+ # Only run concurrently if our environment supports it
110
+ def establish_concurrency
111
+ Graphiti.config.concurrency = !::Rails.env.test? &&
112
+ ::Rails.application.config.cache_classes
113
+ end
114
+
115
+ def configure_endpoint_lookup
116
+ Graphiti.config.context_for_endpoint = ->(path, action) {
117
+ method = :GET
118
+ case action
119
+ when :show then path = "#{path}/1"
120
+ when :create then method = :POST
121
+ when :update
122
+ path = "#{path}/1"
123
+ method = :PUT
124
+ when :destroy
125
+ path = "#{path}/1"
126
+ method = :DELETE
127
+ end
128
+
129
+ route = begin
130
+ ::Rails.application.routes.recognize_path(path.to_s, method: method)
131
+ rescue
132
+ nil
133
+ end
134
+ "#{route[:controller]}_controller".classify.safe_constantize if route
135
+ }
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,42 @@
1
+ require "graphiti/rails/test_helpers"
2
+
3
+ module Graphiti
4
+ module Rails
5
+ # Rake's `namespace` takes a block, and a block does not open a new definee,
6
+ # so helpers defined inside one are defined on Object and turn up in every
7
+ # request spec in the host app. They live here to stay off that namespace.
8
+ module RakeHelpers
9
+ extend TestHelpers
10
+
11
+ module_function
12
+
13
+ # ::Rails throughout, because bare Rails resolves to Graphiti::Rails here.
14
+ def session
15
+ @session ||= ActionDispatch::Integration::Session.new(::Rails.application)
16
+ end
17
+
18
+ def setup_rails!
19
+ ::Rails.application.eager_load!
20
+ ::Rails.application.config.cache_classes = true
21
+ ::Rails.application.config.action_controller.perform_caching = false
22
+ end
23
+
24
+ def make_request(path, debug = false)
25
+ if path.split("/").length == 2
26
+ path = "#{ApplicationResource.endpoint_namespace}#{path}"
27
+ end
28
+ path << if path.include?("?")
29
+ "&cache=bust"
30
+ else
31
+ "?cache=bust"
32
+ end
33
+ path = "#{path}&debug=true" if debug
34
+ handle_request_exceptions do
35
+ headers = {Authorization: ENV["AUTHORIZATION_HEADER"]}.compact
36
+ session.get(path.to_s, headers: headers)
37
+ end
38
+ JSON.parse(session.response.body)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ # For use with the `responders` gem responders gem to get respond_with
2
+ module Graphiti
3
+ module Rails
4
+ module Responders
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ActionController::MimeResponds
9
+ respond_to(*Graphiti::Rails.respond_to_formats)
10
+ end
11
+
12
+ # Override to avoid location url generation (for now)
13
+ def respond_with(*args, &blk)
14
+ opts = args.extract_options!
15
+ opts[:location] = nil
16
+ args << opts
17
+ super(*args, &blk)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ module Graphiti
2
+ module Rails
3
+ # Wrap a request in `handle_request_exceptions` to assert on the rendered
4
+ # error payload. Exceptions propagate untouched in tests otherwise.
5
+ module TestHelpers
6
+ include RescueRegistry::RailsTestHelpers
7
+
8
+ # rescue_registry sets action_dispatch.show_exceptions to true/false.
9
+ # Rails 7.1 replaced those with :all/:rescuable/:none, and Rails 8 dropped
10
+ # boolean handling altogether. ExceptionWrapper#show? now treats
11
+ # everything that is not :none as "show", so passing false would leave
12
+ # exception handling on and silently do nothing.
13
+ def handle_request_exceptions(handle = true, &block)
14
+ super(handle ? :all : :none, &block)
15
+ end
16
+
17
+ def handle_request_exceptions?
18
+ ::Rails.application.config.action_dispatch.show_exceptions != :none
19
+ end
20
+ end
21
+ end
22
+ end
@@ -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
@@ -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] ||= {}