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
data/docs/upgrading.md ADDED
@@ -0,0 +1,265 @@
1
+ ---
2
+ title: 'Upgrading to Graphiti 2.0'
3
+ slug: /upgrading
4
+ ---
5
+
6
+ # Upgrading to Graphiti 2.0
7
+
8
+ Graphiti 2.0 requires **Ruby 3.2+** and **ActiveSupport 7.1+**. Rails is not a dependency, but if you use it, 7.1+. Ruby 3.1 and earlier are past end of life, and Rails 6.1 and 7.0 do not support Ruby 3.2. Apps that cannot move yet should stay on the 1.x branch, which remains open for hotfixes.
9
+
10
+ ## What you have to change {#what-you-have-to-change}
11
+
12
+ Four things, and three of them fail loudly if you skip them.
13
+
14
+ **1. Drop three gems.** `graphiti-rails`, `graphiti_spec_helpers` and `graphiti_errors` are now part of `graphiti` itself.
15
+
16
+ ```diff
17
+ # Gemfile
18
+ + gem "graphiti"
19
+ - gem "graphiti-rails"
20
+ - gem "graphiti_spec_helpers"
21
+ - gem "graphiti_errors"
22
+ ```
23
+
24
+ Graphiti raises at load if one is still installed, because they ship files that collide with Graphiti's own, so leaving them in place means load order decides which copy you get.
25
+
26
+ **2. Include the Rails integration in your controllers.**
27
+
28
+ ```ruby
29
+ class ApplicationController < ActionController::Base
30
+ include Graphiti::Rails::Controller
31
+ end
32
+ ```
33
+
34
+ Skipping this fails at boot rather than at request time: a controller calling `sideload_allowlist` raises `NoMethodError` while its class body loads, so the app stops starting.
35
+
36
+ If the controller already has `include Graphiti::Rails`, replace that line rather than adding to it. The old spelling still sets the controller up, so nothing breaks mid-upgrade, but it warns and goes away in the next major version.
37
+
38
+ <details>
39
+ <summary>What the include actually brings, and what a controller without it loses</summary>
40
+
41
+ Until 2.0, Graphiti added itself to **every** controller in the application: an `around_action` wrapping each request in a Graphiti context, another wrapping it in the debugger, and a catch-all exception handler, on Devise controllers, admin controllers, HTML pages, everything.
42
+
43
+ `Graphiti::Rails::Controller` now bundles all of it, and where you include it decides the blast radius. `ApplicationController` matches 1.x behavior. An API base class scopes it and leaves the rest of the app alone.
44
+
45
+ A controller without it gets no Graphiti context, no debugger, and none of Graphiti's exception handlers, so if a resource action sees an empty `Graphiti.context`, this include is what is missing.
46
+
47
+ The class-level DSL travels with it, which is the one failure you see before a request is ever served:
48
+
49
+ ```ruby
50
+ class PostsController < ApplicationController
51
+ self.sideload_allowlist = {index: [:comments]} # NoMethodError without the include
52
+ end
53
+ ```
54
+
55
+ `sideload_allowlist` comes from `Graphiti::Context`, so a controller that never includes `Graphiti::Rails::Controller` raises `NoMethodError` while the class body is being loaded. Watch for base classes that were given `Graphiti::Rails::Responders` alone. Responders declares formats and nothing else, and does not carry the context.
56
+
57
+ `Graphiti::Rails::Responders` is separate and most apps do not need it. It exists for the [`responders`](https://github.com/heartcombo/responders) gem's `respond_with`, and depends on that gem, which is why it is not part of `Graphiti::Rails::Controller`.
58
+
59
+ </details>
60
+
61
+ **3. Update `around_persistence` hooks**, if you have any.
62
+
63
+ They now receive the already-assigned model where they used to receive the attributes hash, so a hook doing `attributes[:tenant_id] = current_tenant.id` raises. Move that to `before_attributes`, or set it on the model.
64
+
65
+ <details>
66
+ <summary>Before and after, and what else moved with it</summary>
67
+
68
+ Attributes are now assigned to the model once, up front, before the persistence hooks run, which is what lets `build` and `find` hand you the model before anything is written. See the [lifecycle hooks guide](/concepts/persisting#persistence-lifecycle-hooks) for what that enables.
69
+
70
+ That changes one hook.
71
+
72
+ #### around_persistence receives the model, not the attributes hash
73
+
74
+ It now wraps the save of an already-assigned model, and gets that model:
75
+
76
+ ```ruby
77
+ # 1.x
78
+ def do_around_persistence(attributes)
79
+ attributes[:tenant_id] = current_tenant.id
80
+ model = yield
81
+ model.log_saved!
82
+ end
83
+
84
+ # 2.0
85
+ def do_around_persistence(model)
86
+ model.tenant_id = current_tenant.id # last chance to touch the model before save, inside the transaction
87
+ saved = yield
88
+ saved.log_saved!
89
+ end
90
+ ```
91
+
92
+ To migrate, move attribute-hash modifications to `before_attributes` (which still receives the mutable hash, before assignment), or set the value on the model as above. Hooks that only wrap their yield, such as transactions, timing and post-save side effects, need no changes. Graphiti 1.x releases warn at runtime when a hook would be affected.
93
+
94
+ `before/around/after_attributes` and `before/around/after_save` are unchanged. Custom `create`/`update` adapter overrides keep their 1.x signatures.
95
+
96
+ #### Fine print
97
+
98
+ - If you inspect the model before saving, the attributes callbacks run at inspection time (in your controller, outside the save transaction). On the plain `save` path they run inside the transaction, at the same point as 1.x.
99
+ - Writable guards judge persisted state: a guard asking for the model gets a fresh build/find, never the current request's unsaved changes. A payload cannot influence its own authorization.
100
+ - Sideposted child models are still built and assigned during save, and `data` exposes the pre-assigned root model only.
101
+
102
+ </details>
103
+
104
+ **4. Wrap specs that assert on error payloads.**
105
+
106
+ ```ruby
107
+ RSpec.configure do |config|
108
+ config.include Graphiti::Rails::TestHelpers, type: :request
109
+ end
110
+
111
+ it "renders a 404" do
112
+ handle_request_exceptions { get "/posts/999" }
113
+
114
+ expect(response.status).to eq(404)
115
+ end
116
+ ```
117
+
118
+ Exceptions now propagate untouched in tests rather than rendering, so a spec expecting a 404 body sees the exception raised instead. This is the one that breaks the suite that would otherwise have told you the app was fine.
119
+
120
+ <details>
121
+ <summary>Why it has to be a request spec</summary>
122
+
123
+ Exceptions propagate untouched in tests as of 2.0. A spec that asserts on a rendered error payload sees the exception raised instead, so wrap the request in `handle_request_exceptions`. The setup is [step 4 of the migration](/upgrading#what-you-have-to-change).
124
+
125
+ It has to be a request spec. Exceptions are rendered in Rack middleware, which controller specs bypass, so the same assertion in a controller spec never sees a rendered payload no matter how it is wrapped.
126
+
127
+ `handle_request_exceptions` replaces `GraphitiErrors.enable!` and `.disable!`, which toggled rendering globally. Wrapping the request scopes it to the example instead.
128
+
129
+ </details>
130
+
131
+ ## Behavior changes to be aware of {#behavior-changes}
132
+
133
+ Nothing to do here. These change what a client receives or when a callback runs, and none of them warns you, because none of them is a rename.
134
+
135
+ <details>
136
+ <summary>A `belongs_to` renders resource linkage in every payload, where 1.x sent only a link</summary>
137
+
138
+ A `belongs_to` now includes resource linkage in the payload by default, where 1.x sent only a link:
139
+
140
+ ```json
141
+ "employee": { "data": { "type": "employees", "id": "1" }, "links": { "related": "..." } }
142
+ ```
143
+
144
+ The id comes from the foreign key already on the parent, so this costs no extra queries, and clients can resolve the relationship against data they already hold instead of following the link. `has_many` is unchanged, since answering there means a query per record.
145
+
146
+ Not every `belongs_to` qualifies. A `scope` or `params` block, a `base_scope`, a polymorphic or remote target or a custom `primary_key` all mean the foreign key is not the related id, and those keep loading the association, so they stay opt-in as in 1.x.
147
+
148
+ Relationships are marked `linkage: true` in `schema.json`, and the schema check reports a relationship that stops including it.
149
+
150
+ To go back to the old payload for one relationship:
151
+
152
+ ```ruby
153
+ belongs_to :employee, always_include_resource_ids: false
154
+ ```
155
+
156
+ Or for the whole API, on the resource everything inherits from. The same setting with `true` is the 2.0 replacement for the `Sideload::BelongsTo` monkey patch that [#167](https://github.com/graphiti-api/graphiti/issues/167) has been recommending, and it now covers every relationship type rather than only `belongs_to`:
157
+
158
+ ```ruby
159
+ class ApplicationResource < Graphiti::Resource
160
+ self.abstract_class = true
161
+
162
+ self.always_include_resource_ids_by_default = false
163
+ end
164
+ ```
165
+
166
+ How linkage is configured, and when a `belongs_to` cannot use its foreign key, is covered in [Customizing Relationships](/concepts/relationships#customizing-relationships).
167
+
168
+ </details>
169
+
170
+ <details>
171
+ <summary>`ConflictRequest` renders `code: "conflict"` at 409, where `graphiti-rails` surfaced it as a 500</summary>
172
+
173
+ Graphiti 1.x shipped two exception systems, `graphiti_errors` in core and `rescue_registry` in `graphiti-rails`, and both loaded in every Rails app. `rescue_registry` is now the only one, and installs automatically as a dependency.
174
+
175
+ Graphiti registers handlers for `InvalidRequest` (400), `ConflictRequest` (409), `RecordNotFound` (404), `RemoteWrite` (400) and `SingularSideload` (400), plus a fallback that renders anything else as JSON:API. Register your own on any controller:
176
+
177
+ ```ruby
178
+ register_exception MyApp::Forbidden, status: 403
179
+ register_exception MyApp::Throttled, status: 429, handler: MyApp::ThrottleHandler
180
+ ```
181
+
182
+ `register_exception` comes from `rescue_registry`, which adds it to every controller, so you do not need `Graphiti::Rails::Controller` to register your own exceptions or to have them rendered. What the include adds is Graphiti's own registrations above, plus the fallback that renders anything unregistered as JSON:API.
183
+
184
+ Only formats in `config.graphiti.handled_exception_formats` (default `[:jsonapi]`) are rendered by Graphiti. Everything else falls through to Rails.
185
+
186
+ If you subclassed `GraphitiErrors::ExceptionHandler`, note the interface changed with the gem: it is now `build_payload` / `formatted_response` / `status_code`, not `error_payload` / `status_code(error)`.
187
+
188
+ Registering and customizing handlers is covered in [Error Handling](/topics/error-handling).
189
+
190
+ **Conflicts now report as conflicts.** `Graphiti::Errors::ConflictRequest`, raised when a `PATCH` payload's id does not match the URL, used to render a 409 whose body said `code: "bad_request"`, `title: "Request Error"`. It now says `code: "conflict"`, `title: "Conflict Error"`. Under `graphiti-rails` this exception had no registered handler at all and surfaced as a 500, so for most apps this payload is new rather than changed.
191
+
192
+ </details>
193
+
194
+ <details>
195
+ <summary>`Node#respond_to?` answers `true` for any attribute present in the payload</summary>
196
+
197
+ `Node#respond_to?` is now a proper `respond_to_missing?`, so `node.respond_to?(:first_name)` returns `true` for attributes present in the payload where it used to return `false`. Nothing to do unless a spec asserted on the old `false`.
198
+
199
+ The node helpers are covered in [#jsonapi_data](/topics/testing#jsonapi-data).
200
+
201
+ </details>
202
+
203
+ <details>
204
+ <summary>Attributes are assigned before the persistence hooks run, so inspecting a model first moves the attributes callbacks outside the save transaction</summary>
205
+
206
+ If you inspect the model before saving, the attributes callbacks run at inspection time, in your controller and outside the save transaction. On the plain `save` path they run inside the transaction, at the same point as 1.x.
207
+
208
+ The hooks and their order are covered in [Persistence Lifecycle Hooks](/concepts/persisting#persistence-lifecycle-hooks).
209
+
210
+ </details>
211
+
212
+ ## Deprecations you should fix {#deprecations-you-should-fix}
213
+
214
+ Every name below still works, warns, and will be removed in the next major. They're easy fixes though, so why not now?
215
+
216
+ | 1.x | 2.0 |
217
+ | --- | --- |
218
+ | `require "graphiti_spec_helpers/rspec"` | `require "graphiti/spec_helpers/rspec"` |
219
+ | `GraphitiSpecHelpers::RSpec` / `::Sugar` / `::Errors::*` | `Graphiti::SpecHelpers::*` |
220
+ | `require "graphiti-rails"` | remove / no longer needed |
221
+ | `include Graphiti::Rails` | `include Graphiti::Rails::Controller`|
222
+ | `include Graphiti::Responders` | `include Graphiti::Rails::Responders` |
223
+ | `jsonapi_context` | `graphiti_context` |
224
+ | `GraphitiErrors::Validation::Serializer` | `Graphiti::ErrorSerializers::Validation` |
225
+ | `GraphitiErrors::InvalidRequest::Serializer` | `Graphiti::ErrorSerializers::InvalidRequest` |
226
+ | `GraphitiErrors::ConflictRequest::Serializer` | `Graphiti::ErrorSerializers::ConflictRequest` |
227
+ | rspec shared contexts `"resource testing"`, `"remote api"` | `"graphiti resource testing"`, `"graphiti remote api"` |
228
+ | `GraphitiContextProxy` | `Graphiti::SpecHelpers::ContextProxy` |
229
+ | `context_namespace` | `current_action` |
230
+ | `Graphiti::Rails::DEPRECATOR` | `Graphiti::DEPRECATOR` (the old name still resolves) |
231
+ | `require "graphiti_errors"`, `require "graphiti/responders"` | remove / no longer needed |
232
+
233
+ `RSpec.describe PostResource, type: :resource` still picks up the resource-testing context automatically. That has not changed.
234
+
235
+ ## Removed outright {#removed-outright}
236
+
237
+ | 1.x | 2.0 |
238
+ | --- | --- |
239
+ | `include GraphitiErrors` | `register_exception` is available on every controller |
240
+ | `GraphitiErrors::ExceptionHandler` | subclass `Graphiti::Rails::ExceptionHandler` |
241
+ | `GraphitiErrors.enable!` / `.disable!` | `handle_request_exceptions` |
242
+
243
+ ## Without Rails {#without-rails}
244
+
245
+ <details>
246
+ <summary>Using the error serializers and exception handling outside Rails</summary>
247
+
248
+ The serializers move but keep working: `Graphiti::ErrorSerializers::Validation`, `::InvalidRequest` and `::ConflictRequest` load with core and need no Rails.
249
+
250
+ `GraphitiErrors::ExceptionHandler`, which turned any exception into a JSON:API errors payload, is replaced by `RescueRegistry::ExceptionHandler`, a runtime dependency now, and usable outside Rails:
251
+
252
+ ```ruby
253
+ require "rack" # or RescueRegistry::ExceptionHandler raises NameError on Rack
254
+ require "rescue_registry"
255
+
256
+ handler = RescueRegistry::ExceptionHandler.new(exception, status: 404)
257
+ handler.build_payload # => {errors: [{code: :not_found, status: "404", ...}]}
258
+ handler.formatted_response(:json) # => [404, "{\"errors\":[...]}", :json]
259
+ ```
260
+
261
+ `register_exception` and the rendering are Rails-only, but rescue_registry ships `RescueRegistry::ShowExceptions`, a Rack middleware for exactly this case. See its README.
262
+
263
+ `GraphitiErrors.logger` has no replacement. `Graphiti.logger` is the nearest thing.
264
+
265
+ </details>
@@ -2,12 +2,13 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 7.1"
5
+ gem "rails", "~> 7.1.0"
6
6
  gem "rspec-rails"
7
- gem "sqlite3", "~> 2.1"
8
- gem "database_cleaner"
7
+ gem "responders"
8
+ gem "sqlite3", "~> 1.4"
9
9
 
10
10
  group :test do
11
+ gem "database_cleaner"
11
12
  gem "pry"
12
13
  gem "pry-byebug", platform: [:mri]
13
14
  gem "appraisal"
@@ -2,13 +2,13 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 7.2"
5
+ gem "rails", "~> 7.2.0"
6
6
  gem "rspec-rails"
7
+ gem "responders"
7
8
  gem "sqlite3", "~> 2.1"
8
- gem "database_cleaner"
9
- gem "graphiti-rails", "~> 0.4.0"
10
9
 
11
10
  group :test do
11
+ gem "database_cleaner"
12
12
  gem "pry"
13
13
  gem "pry-byebug", platform: [:mri]
14
14
  gem "appraisal"
@@ -2,13 +2,13 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 8.1"
5
+ gem "rails", "~> 8.0.0"
6
6
  gem "rspec-rails"
7
+ gem "responders"
7
8
  gem "sqlite3", "~> 2.1"
8
- gem "database_cleaner"
9
- gem "graphiti-rails", "~> 0.4.0"
10
9
 
11
10
  group :test do
11
+ gem "database_cleaner"
12
12
  gem "pry"
13
13
  gem "pry-byebug", platform: [:mri]
14
14
  gem "appraisal"
@@ -2,13 +2,13 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 8.0"
5
+ gem "rails", "~> 8.1.0"
6
6
  gem "rspec-rails"
7
+ gem "responders"
7
8
  gem "sqlite3", "~> 2.1"
8
- gem "database_cleaner"
9
- gem "graphiti-rails", "~> 0.4.0"
10
9
 
11
10
  group :test do
11
+ gem "database_cleaner"
12
12
  gem "pry"
13
13
  gem "pry-byebug", platform: [:mri]
14
14
  gem "appraisal"
data/graphiti.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.bindir = "exe"
17
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
18
  spec.require_paths = ["lib"]
19
- spec.required_ruby_version = ">= 3.0"
19
+ spec.required_ruby_version = ">= 3.2"
20
20
 
21
21
  # TODO: remove once the 1.12 upgrade window has passed
22
22
  spec.post_install_message = <<~MSG
@@ -39,9 +39,9 @@ Gem::Specification.new do |spec|
39
39
  spec.add_dependency "jsonapi-serializable", "~> 0.3.0"
40
40
  spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2"
41
41
  spec.add_dependency "dry-types", ">= 0.15.0", "< 2.0"
42
- spec.add_dependency "graphiti_errors", "~> 1.1.0"
42
+ spec.add_dependency "rescue_registry", "~> 1.0"
43
43
  spec.add_dependency "concurrent-ruby", ">= 1.2", "< 2.0"
44
- spec.add_dependency "activesupport", ">= 5.2"
44
+ spec.add_dependency "activesupport", ">= 7.1"
45
45
  # Bundled (no longer default) as of Ruby 3.5; graphiti uses OpenStruct in lib/
46
46
  spec.add_dependency "ostruct", ">= 0.5"
47
47
 
@@ -50,6 +50,8 @@ Gem::Specification.new do |spec|
50
50
  spec.add_development_dependency "bundler"
51
51
  spec.add_development_dependency "rake", ">= 10.0"
52
52
  spec.add_development_dependency "standard", "~> 1.4.0"
53
- spec.add_development_dependency "activemodel", ">= 5.2"
54
- spec.add_development_dependency "graphiti_spec_helpers", "1.0.beta.4"
53
+ spec.add_development_dependency "activemodel", ">= 7.1"
54
+ # Was reaching the suite transitively through graphiti_spec_helpers, which is
55
+ # now part of this gem.
56
+ spec.add_development_dependency "rspec", "~> 3.0"
55
57
  end
@@ -4,7 +4,7 @@ module Graphiti
4
4
  class ApiTestGenerator < ::Rails::Generators::Base
5
5
  include GeneratorMixin
6
6
 
7
- source_root File.expand_path("templates", __dir__)
7
+ source_root File.expand_path("../templates", __FILE__)
8
8
 
9
9
  argument :resource, type: :string
10
10
  class_option :actions,
@@ -13,6 +13,12 @@ module Graphiti
13
13
  aliases: ["--actions", "-a"],
14
14
  desc: 'Array of controller actions, e.g. "index show destroy"'
15
15
 
16
+ class_option :rawid,
17
+ type: :boolean,
18
+ default: false,
19
+ aliases: ["--rawid", "-r"],
20
+ desc: "Generate tests using rawid"
21
+
16
22
  desc "Generates rspec request specs at spec/api"
17
23
  def generate
18
24
  generate_api_specs
@@ -12,7 +12,6 @@ module Graphiti
12
12
  def api_namespace
13
13
  @api_namespace ||= begin
14
14
  ns = graphiti_config["namespace"]
15
- ns.delete_suffix("/")
16
15
 
17
16
  if ns.blank?
18
17
  ns = prompt \
@@ -42,5 +41,19 @@ module Graphiti
42
41
  config = graphiti_config.merge(attrs)
43
42
  File.open(".graphiticfg.yml", "w") { |f| f.write(config.to_yaml) }
44
43
  end
44
+
45
+ def id_or_rawid
46
+ @options["rawid"] ? "rawid" : "id"
47
+ end
48
+
49
+ def sort_raw_ids
50
+ return unless @options["rawid"]
51
+ ".sort"
52
+ end
53
+
54
+ def sort_raw_ids_descending
55
+ return unless @options["rawid"]
56
+ ".sort.reverse"
57
+ end
45
58
  end
46
59
  end
@@ -4,7 +4,7 @@ module Graphiti
4
4
  class InstallGenerator < ::Rails::Generators::Base
5
5
  include GeneratorMixin
6
6
 
7
- source_root File.expand_path("../templates", __FILE__)
7
+ source_root File.expand_path("templates", __dir__)
8
8
 
9
9
  class_option :'omit-comments',
10
10
  type: :boolean,
@@ -41,8 +41,20 @@ module Graphiti
41
41
  RUBY
42
42
  end
43
43
 
44
- inject_into_file "spec/rails_helper.rb", after: /RSpec.configure.+^end$/m do
45
- "\n\nGraphitiSpecHelpers::RSpec.schema!"
44
+ if defined?(RSpec)
45
+ inject_into_file "spec/rails_helper.rb", after: /RSpec.configure.+^end$/m do
46
+ <<~RUBY
47
+
48
+ require "graphiti/spec_helpers/rspec"
49
+
50
+ RSpec.configure do |config|
51
+ config.include Graphiti::SpecHelpers::RSpec
52
+ config.include Graphiti::SpecHelpers::Sugar
53
+ end
54
+
55
+ Graphiti::SpecHelpers::RSpec.schema!
56
+ RUBY
57
+ end
46
58
  end
47
59
 
48
60
  insert_into_file "config/routes.rb", after: "Rails.application.routes.draw do\n" do
@@ -70,16 +82,10 @@ module Graphiti
70
82
  end
71
83
 
72
84
  def app_controller_code
73
- str = ""
74
- str << " include Graphiti::Rails\n"
75
- str << " include Graphiti::Responders\n"
76
- str << "\n"
77
- str << " register_exception Graphiti::Errors::RecordNotFound,\n"
78
- str << " status: 404\n"
79
- str << "\n"
80
- str << " rescue_from Exception do |e|\n"
81
- str << " handle_exception(e)\n"
82
- str << " end\n"
85
+ str = +" include Graphiti::Rails::Controller\n"
86
+ if defined?(::Responders)
87
+ str << " include Graphiti::Rails::Responders\n"
88
+ end
83
89
  str
84
90
  end
85
91
  end
@@ -14,6 +14,12 @@ module Graphiti
14
14
  aliases: ["--omit-comments", "-c"],
15
15
  desc: "Generate without documentation comments"
16
16
 
17
+ class_option :rawid,
18
+ type: :boolean,
19
+ default: false,
20
+ aliases: ["--rawid", "-r"],
21
+ desc: "Generate tests using rawid"
22
+
17
23
  class_option :actions,
18
24
  type: :array,
19
25
  default: nil,
@@ -94,7 +100,10 @@ module Graphiti
94
100
  end
95
101
  if attributes_class.table_exists?
96
102
  attributes_class.columns.map do |c|
97
- OpenStruct.new({name: c.name.to_sym, type: c.type})
103
+ OpenStruct.new({
104
+ name: c.name.to_sym,
105
+ type: convert_column_type_to_graphiti_resource_type(c.type)
106
+ })
98
107
  end
99
108
  else
100
109
  raise "#{attributes_class} table must exist. Please run migrations."
@@ -113,7 +122,7 @@ module Graphiti
113
122
  end
114
123
 
115
124
  def responders?
116
- defined?(Responders)
125
+ defined?(::Responders)
117
126
  end
118
127
 
119
128
  def generate_controller
@@ -132,10 +141,7 @@ module Graphiti
132
141
  end
133
142
 
134
143
  def generate_route
135
- # Rails 5.2 adds `plural_route_name`, fallback to `plural_table_name`
136
- plural_name = try(:plural_route_name) || plural_table_name
137
-
138
- code = "resources :#{plural_name}"
144
+ code = "resources :#{file_name.pluralize}"
139
145
  code << %(, only: [#{actions.map { |a| ":#{a}" }.join(", ")}]) if actions.length < 5
140
146
  code << "\n"
141
147
  inject_into_file "config/routes.rb", after: /ApplicationResource.*$\n/ do
@@ -146,12 +152,14 @@ module Graphiti
146
152
  def generate_resource_specs
147
153
  opts = {}
148
154
  opts[:actions] = @options[:actions] if @options[:actions]
155
+ opts[:rawid] = @options[:rawid] if @options[:rawid]
149
156
  invoke "graphiti:resource_test", [resource_klass], opts
150
157
  end
151
158
 
152
159
  def generate_api_specs
153
160
  opts = {}
154
161
  opts[:actions] = @options[:actions] if @options[:actions]
162
+ opts[:rawid] = @options[:rawid] if @options[:rawid]
155
163
  invoke "graphiti:api_test", [resource_klass], opts
156
164
  end
157
165
 
@@ -176,5 +184,34 @@ module Graphiti
176
184
  def type
177
185
  model_klass.name.underscore.pluralize
178
186
  end
187
+
188
+ def convert_column_type_to_graphiti_resource_type(type)
189
+ # TODO: Support database specific types.
190
+ case type
191
+ when :string, :text
192
+ :string
193
+ when :float, :decimal
194
+ :integer
195
+ when :integer, :bigint
196
+ :integer
197
+ when :datetime, :time
198
+ :datetime
199
+ when :date
200
+ :date
201
+ when :boolean
202
+ :boolean
203
+ when :numeric
204
+ # TODO: Return type.
205
+ type
206
+ when :primary_key
207
+ # TODO: Return type.
208
+ type
209
+ when :binary
210
+ # TODO: Return type.
211
+ type
212
+ else
213
+ type
214
+ end
215
+ end
179
216
  end
180
217
  end
@@ -16,7 +16,7 @@ RSpec.describe "<%= type %>#index", type: :request do
16
16
  make_request
17
17
  expect(response.status).to eq(200), response.body
18
18
  expect(d.map(&:jsonapi_type).uniq).to match_array(['<%= type %>'])
19
- expect(d.map(&:id)).to match_array([<%= var %>1.id, <%= var %>2.id])
19
+ expect(d.map(&:<%= id_or_rawid %>)).to match_array([<%= var %>1.id, <%= var %>2.id])
20
20
  end
21
21
  end
22
22
  end
@@ -7,7 +7,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
7
7
  it 'works' do
8
8
  render
9
9
  data = jsonapi_data[0]
10
- expect(data.id).to eq(<%= var %>.id)
10
+ expect(data.<%= id_or_rawid %>).to eq(<%= var %>.id)
11
11
  expect(data.jsonapi_type).to eq('<%= type %>')
12
12
  <%- attributes.each do |a| -%>
13
13
  <%- if [:created_at, :updated_at].include?(a.name.to_sym) -%>
@@ -31,7 +31,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
31
31
 
32
32
  it 'works' do
33
33
  render
34
- expect(d.map(&:id)).to eq([<%= var %>2.id])
34
+ expect(d.map(&:<%= id_or_rawid %>)).to eq([<%= var %>2.id])
35
35
  end
36
36
  end
37
37
  end
@@ -48,10 +48,10 @@ RSpec.describe <%= resource_class %>, type: :resource do
48
48
 
49
49
  it 'works' do
50
50
  render
51
- expect(d.map(&:id)).to eq([
51
+ expect(d.map(&:<%= id_or_rawid %>)).to eq([
52
52
  <%= var %>1.id,
53
53
  <%= var %>2.id
54
- ])
54
+ ]<%= sort_raw_ids %>)
55
55
  end
56
56
  end
57
57
 
@@ -62,10 +62,10 @@ RSpec.describe <%= resource_class %>, type: :resource do
62
62
 
63
63
  it 'works' do
64
64
  render
65
- expect(d.map(&:id)).to eq([
65
+ expect(d.map(&:<%= id_or_rawid %>)).to eq([
66
66
  <%= var %>2.id,
67
67
  <%= var %>1.id
68
- ])
68
+ ]<%= sort_raw_ids_descending %>)
69
69
  end
70
70
  end
71
71
  end
@@ -15,7 +15,7 @@ RSpec.describe "<%= type %>#show", type: :request do
15
15
  make_request
16
16
  expect(response.status).to eq(200)
17
17
  expect(d.jsonapi_type).to eq('<%= type %>')
18
- expect(d.id).to eq(<%= var %>.id)
18
+ expect(d.<%= id_or_rawid %>).to eq(<%= var %>.id)
19
19
  end
20
20
  end
21
21
  end
@@ -55,7 +55,7 @@ module Graphiti
55
55
 
56
56
  @uri_decoder = infer_uri_decoder
57
57
 
58
- # FIXME: Don't duplicate graphiti-rails efforts
58
+ # FIXME: Don't duplicate the Railtie's initializer
59
59
  if defined?(::Rails.root) && (root = ::Rails.root)
60
60
  config_file = root.join(".graphiticfg.yml")
61
61
  if config_file.exist?
@@ -132,6 +132,6 @@ module Graphiti
132
132
  end
133
133
  end
134
134
 
135
- msg = "Use graphiti-rails's `config.graphiti.respond_to_formats`"
135
+ msg = "Use `config.graphiti.respond_to_formats`"
136
136
  DEPRECATOR.deprecate_methods(Configuration, respond_to: msg, "respond_to=": msg)
137
137
  end
@@ -0,0 +1,19 @@
1
+ module Graphiti
2
+ module ErrorSerializers
3
+ # graphiti_errors overrode only the status here, and so reported a 409
4
+ # alongside code "bad_request".
5
+ class ConflictRequest < InvalidRequest
6
+ STATUS = 409
7
+
8
+ private
9
+
10
+ def code
11
+ "conflict"
12
+ end
13
+
14
+ def title
15
+ "Conflict Error"
16
+ end
17
+ end
18
+ end
19
+ end