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
@@ -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
@@ -0,0 +1,48 @@
1
+ # The graphiti_errors gem was absorbed into graphiti in 2.0. Only its
2
+ # serializers survived, as Graphiti::ErrorSerializers. The rest of it (the
3
+ # GraphitiErrors controller mixin and its exception handlers) is replaced by
4
+ # rescue_registry rather than renamed, so there is deliberately nothing here to
5
+ # alias it to. A custom error renderer reaching for one of the serializers gets
6
+ # pointed at the new name instead of a bare NameError. Remove in 3.0.
7
+ module GraphitiErrors
8
+ # `include GraphitiErrors` in ApplicationController was this gem's headline
9
+ # usage. Including an empty module succeeds, so without this an upgraded app
10
+ # boots clean and then serves Rails' default HTML error pages instead of
11
+ # JSON:API ones, with nothing to indicate why.
12
+ def self.included(klass)
13
+ raise <<~MSG
14
+ GraphitiErrors was merged into graphiti as of 2.0 and `include GraphitiErrors`
15
+ no longer does anything.
16
+
17
+ Exception handling now goes through rescue_registry. Replace it with:
18
+
19
+ include Graphiti::Rails::Controller
20
+
21
+ which registers Graphiti's own handlers. Your own go alongside them with
22
+ `register_exception`, available on every controller.
23
+ MSG
24
+ end
25
+
26
+ module Validation; end
27
+
28
+ module InvalidRequest; end
29
+
30
+ module ConflictRequest; end
31
+
32
+ module Serializers; end
33
+ end
34
+
35
+ {
36
+ "GraphitiErrors::Validation::Serializer" => "Graphiti::ErrorSerializers::Validation",
37
+ "GraphitiErrors::InvalidRequest::Serializer" => "Graphiti::ErrorSerializers::InvalidRequest",
38
+ "GraphitiErrors::ConflictRequest::Serializer" => "Graphiti::ErrorSerializers::ConflictRequest",
39
+ # Graphiti 1.0.x referenced this spelling
40
+ "GraphitiErrors::Serializers::Validation" => "Graphiti::ErrorSerializers::Validation"
41
+ }.each do |old_name, new_name|
42
+ namespace, _, constant = old_name.rpartition("::")
43
+
44
+ Object.const_get(namespace).const_set(
45
+ constant,
46
+ ActiveSupport::Deprecation::DeprecatedConstantProxy.new(old_name, new_name, Graphiti::DEPRECATOR)
47
+ )
48
+ end
@@ -0,0 +1,56 @@
1
+ module Graphiti
2
+ module ErrorSerializers
3
+ class InvalidRequest
4
+ STATUS = 400
5
+
6
+ attr_reader :source
7
+
8
+ def initialize(source)
9
+ @source = source
10
+ end
11
+
12
+ def status
13
+ self.class::STATUS
14
+ end
15
+
16
+ def errors
17
+ [].tap do |payload|
18
+ source.details.each_pair do |attribute, attribute_errors|
19
+ attribute_errors.each_with_index do |error, index|
20
+ message = source.messages[attribute][index]
21
+
22
+ payload << {
23
+ code: code,
24
+ status: status.to_s,
25
+ title: title,
26
+ detail: source.full_message(attribute, message),
27
+ source: {pointer: pointer_for(attribute)},
28
+ meta: {
29
+ attribute: attribute,
30
+ message: message,
31
+ code: error[:error]
32
+ }
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def code
42
+ "bad_request"
43
+ end
44
+
45
+ def title
46
+ "Request Error"
47
+ end
48
+
49
+ # "filter.foo[0]" describes a path into the payload, not an attribute
50
+ # name, so it becomes a JSON pointer.
51
+ def pointer_for(attribute)
52
+ attribute.to_s.tr(".", "/").gsub(/\[(\d+)\]/, '/\1')
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,143 @@
1
+ module Graphiti
2
+ module ErrorSerializers
3
+ # Walks into related objects so nested writes report against the record
4
+ # that actually failed.
5
+ class Validation
6
+ attr_reader :object
7
+
8
+ def initialize(object, relationship_payloads = {}, relationship_meta = {})
9
+ @object = object
10
+ @relationship_payloads = relationship_payloads
11
+ @relationship_meta = relationship_meta
12
+ end
13
+
14
+ def attribute_errors
15
+ [].tap do |errors|
16
+ each_error do |attribute, message, code|
17
+ errors << {
18
+ code: "unprocessable_entity",
19
+ status: "422",
20
+ title: "Validation Error",
21
+ detail: detail_for(attribute, message),
22
+ source: {pointer: pointer_for(object, attribute)},
23
+ meta: meta_for(attribute, message, code, @relationship_meta)
24
+ }
25
+ end
26
+ end
27
+ end
28
+
29
+ def errors
30
+ return [] unless object.respond_to?(:errors)
31
+
32
+ attribute_errors | relationship_errors(object, @relationship_payloads)
33
+ end
34
+
35
+ private
36
+
37
+ def each_error
38
+ object.errors.messages.each_pair do |attribute, messages|
39
+ details = object.errors.details.find { |k, _| k == attribute }[1]
40
+
41
+ messages.each_with_index do |message, index|
42
+ yield attribute, message, details[index][:error]
43
+ end
44
+ end
45
+ end
46
+
47
+ def relationship?(name)
48
+ relationship_names = []
49
+ if activerecord?
50
+ relationship_names = object.class
51
+ .reflect_on_all_associations.map(&:name)
52
+ elsif object.respond_to?(:relationship_names)
53
+ relationship_names = object.relationship_names
54
+ end
55
+
56
+ relationship_names.include?(name)
57
+ end
58
+
59
+ def attribute?(name)
60
+ object.respond_to?(name)
61
+ end
62
+
63
+ def meta_for(attribute, message, code, relationship_meta)
64
+ meta = {
65
+ attribute: attribute,
66
+ message: message,
67
+ code: code
68
+ }
69
+
70
+ unless relationship_meta.empty?
71
+ meta = {relationship: meta.merge(relationship_meta)}
72
+ end
73
+
74
+ meta
75
+ end
76
+
77
+ def detail_for(attribute, message)
78
+ detail = object.errors.full_message(attribute, message)
79
+ detail = message if attribute.to_s.downcase == "base"
80
+ detail
81
+ end
82
+
83
+ # @richmolj: Keeping this to support ember-data, but I hate the concept.
84
+ def pointer_for(object, name)
85
+ if relationship?(name)
86
+ "/data/relationships/#{name}"
87
+ elsif attribute?(name)
88
+ "/data/attributes/#{name}"
89
+ elsif name == :base
90
+ nil
91
+ else
92
+ # Probably a nested relation, like post.comments
93
+ "/data/relationships/#{name}"
94
+ end
95
+ end
96
+
97
+ def activerecord?
98
+ object.class.respond_to?(:reflect_on_all_associations)
99
+ end
100
+
101
+ def traverse_relationships(model, relationship_params)
102
+ return unless relationship_params
103
+
104
+ relationship_params.each_pair do |name, payload|
105
+ relationship_objects = Array(model.send(name))
106
+
107
+ relationship_objects.each do |relationship_object|
108
+ related_payload = payload
109
+ if payload.is_a?(Array)
110
+ related_payload = payload.find { |p|
111
+ temp_id = relationship_object
112
+ .instance_variable_get(:@_jsonapi_temp_id)
113
+ p[:meta][:temp_id] === temp_id ||
114
+ p[:meta][:id] == relationship_object.id.to_s
115
+ }
116
+ end
117
+
118
+ yield name, relationship_object, related_payload
119
+ relationship_errors(relationship_object, related_payload[:relationships])
120
+ end
121
+ end
122
+ end
123
+
124
+ def relationship_errors(model, relationship_payloads)
125
+ errors = []
126
+ traverse_relationships(model, relationship_payloads) do |name, related, payload|
127
+ meta = {}.tap do |hash|
128
+ hash[:name] = name
129
+ hash[:type] = payload[:meta][:jsonapi_type]
130
+ if (temp_id = related.instance_variable_get(:@_jsonapi_temp_id))
131
+ hash[:"temp-id"] = temp_id
132
+ else
133
+ hash[:id] = related.id
134
+ end
135
+ end
136
+
137
+ errors |= self.class.new(related, payload[:relationships], meta).errors
138
+ end
139
+ errors
140
+ end
141
+ end
142
+ end
143
+ end
@@ -270,7 +270,7 @@ module Graphiti
270
270
  #{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
271
271
 
272
272
  The error was raised while attempting to build query parameters for the associated Resource.
273
- Read more about sideload scoping here: www.graphiti.dev/guides/concepts/resources#customizing-scope
273
+ Read more about sideload scoping here: www.graphiti.dev/concepts/relationships#customizing-scope
274
274
 
275
275
  A good way to debug is to put a debugger within the 'params' block.
276
276
 
@@ -294,7 +294,7 @@ module Graphiti
294
294
 
295
295
  The error was raised while attempting to build the scope for the associated Resource.
296
296
 
297
- Read more about sideload scoping here: www.graphiti.dev/guides/concepts/resources#customizing-scope
297
+ Read more about sideload scoping here: www.graphiti.dev/concepts/relationships#customizing-scope
298
298
 
299
299
  Here's the original, underlying error:
300
300
 
@@ -315,7 +315,7 @@ module Graphiti
315
315
  #{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
316
316
 
317
317
  The error was raised while attempting to assign relevant model instances. Read
318
- more about sideload assignment here: www.graphiti.dev/guides/concepts/resources#customizing-assignment
318
+ more about sideload assignment here: www.graphiti.dev/concepts/relationships#customizing-assignment
319
319
 
320
320
  A good way to debug is to put a debugger within the 'assign' block.
321
321
 
@@ -449,7 +449,7 @@ module Graphiti
449
449
 
450
450
  self.validate_endpoints = false
451
451
 
452
- See https://www.graphiti.dev/guides/concepts/links for more information.
452
+ See https://www.graphiti.dev/concepts/links for more information.
453
453
 
454
454
  The current endpoints allowed for this resource are: #{@resource_class.endpoints.inspect}
455
455
  MSG
@@ -209,7 +209,7 @@ module Graphiti
209
209
  allowlist = nil
210
210
  if @resource.context&.respond_to?(:sideload_allowlist)
211
211
  allowlist = @resource.context.sideload_allowlist
212
- allowlist = allowlist[@resource.context_namespace] if allowlist
212
+ allowlist = allowlist[@resource.current_action] if allowlist
213
213
  end
214
214
 
215
215
  scrubbed = allowlist ? Util::IncludeParams.scrub(requested, allowlist) : requested
@@ -0,0 +1,33 @@
1
+ module Graphiti
2
+ module Rails
3
+ # Wraps controller actions in a [Graphiti Context](https://www.graphiti.dev/concepts/resources#context) which points to the
4
+ # controller instance by default.
5
+ module Context
6
+ def self.included(klass)
7
+ klass.class_eval do
8
+ include Graphiti::Context
9
+ around_action :wrap_graphiti_context
10
+ end
11
+ end
12
+
13
+ # Called by [`#around_action`](https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-around_action)
14
+ # to wrap the current action in a Graphiti context defined by {#graphiti_context}.
15
+ def wrap_graphiti_context
16
+ Graphiti.with_context(graphiti_context, action_name.to_sym) do
17
+ yield
18
+ end
19
+ end
20
+
21
+ # The context to use for Graphiti Resources. Defaults to the controller instance.
22
+ # Can be redefined for different behavior.
23
+ def graphiti_context
24
+ if respond_to?(:jsonapi_context)
25
+ DEPRECATOR.deprecation_warning("Overriding jsonapi_context", "Override #graphiti_context instead")
26
+ jsonapi_context
27
+ else
28
+ self
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ module Graphiti
2
+ module Rails
3
+ # Everything a controller needs to serve Graphiti resources. Include it in
4
+ # the controller your API inherits from:
5
+ #
6
+ # class ApplicationController < ActionController::Base
7
+ # include Graphiti::Rails::Controller
8
+ # end
9
+ #
10
+ # Graphiti applied all of this to every controller in the application until
11
+ # 2.0, which meant wrapping unrelated actions in a Graphiti context and
12
+ # registering a catch-all exception handler on controllers that never touch
13
+ # Graphiti. Scoping it is now up to you.
14
+ module Controller
15
+ extend ActiveSupport::Concern
16
+
17
+ included do
18
+ include Graphiti::Rails::Context
19
+ include Graphiti::Rails::Debugging
20
+
21
+ # Broad, but the handler only renders formats listed in
22
+ # config.graphiti.handled_exception_formats and hands everything else
23
+ # back to Rails.
24
+ register_exception Exception,
25
+ status: :passthrough,
26
+ handler: Graphiti::Rails::FallbackHandler
27
+
28
+ register_exception Graphiti::Errors::InvalidRequest,
29
+ status: 400, handler: Graphiti::Rails::InvalidRequestHandler
30
+ register_exception Graphiti::Errors::ConflictRequest,
31
+ status: 409, handler: Graphiti::Rails::ConflictRequestHandler
32
+ register_exception Graphiti::Errors::RecordNotFound,
33
+ status: 404, handler: Graphiti::Rails::ExceptionHandler
34
+ register_exception Graphiti::Errors::RemoteWrite,
35
+ status: 400, handler: Graphiti::Rails::ExceptionHandler
36
+ register_exception Graphiti::Errors::SingularSideload,
37
+ status: 400, handler: Graphiti::Rails::ExceptionHandler
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ module Graphiti
2
+ module Rails
3
+ # Wraps controller actions in a [Graphiti Debugger](https://www.graphiti.dev/topics/debugging#debugger).
4
+ module Debugging
5
+ def self.included(klass)
6
+ klass.around_action :debug_graphiti
7
+ end
8
+
9
+ # Called by [`#around_action`](https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-around_action)
10
+ # to wrap the current action in a Graphiti Debugger.
11
+ def debug_graphiti
12
+ Debugger.debug do
13
+ yield
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,77 @@
1
+ module Graphiti
2
+ module Rails
3
+ class ExceptionHandler < RescueRegistry::ExceptionHandler
4
+ # We've actually changed the signature here which is somewhat risky...
5
+ def build_payload(show_details: false, traces: nil, style: :rails)
6
+ case style
7
+ when :standard
8
+ super(show_details: show_details, traces: traces).tap do |payload|
9
+ if show_details
10
+ # For Vandal and Request Responses
11
+ payload[:__raw_error__] = {
12
+ message: exception.message,
13
+ debug: exception.instance_variable_get(:@__graphiti_debug),
14
+ backtrace: exception.backtrace
15
+ }
16
+ end
17
+ end
18
+ when :rails
19
+ # TODO: Find way to not duplicate RailsExceptionHandler
20
+ body = {
21
+ status: status_code,
22
+ error: title
23
+ }
24
+
25
+ if show_details
26
+ body[:exception] = exception.inspect
27
+ body[:traces] = traces if traces
28
+ end
29
+
30
+ body
31
+ else
32
+ raise ArgumentError, "unknown style #{style}"
33
+ end
34
+ end
35
+
36
+ def formatted_response(content_type, **options)
37
+ # We're relying on the fact that `formatted_response` passes through unknown options to `build_payload`
38
+ if Graphiti::Rails.handled_exception_formats.include?(content_type.to_sym)
39
+ options[:style] = :standard
40
+ end
41
+ super
42
+ end
43
+ end
44
+
45
+ class FallbackHandler < ExceptionHandler
46
+ # A nil response is what hands the exception back to the default Rails
47
+ # handler, so formats Graphiti does not claim render as they always did.
48
+ def formatted_response(content_type, **options)
49
+ return unless Graphiti::Rails.handled_exception_formats.include?(content_type.to_sym)
50
+
51
+ super
52
+ end
53
+ end
54
+
55
+ class InvalidRequestHandler < ExceptionHandler
56
+ # NOTE: `style` is ignored. A rejected request always reports which
57
+ # parameters were rejected and why.
58
+ def build_payload(show_details: false, traces: nil, style: nil)
59
+ {errors: error_serializer.new(exception.errors).errors}
60
+ end
61
+
62
+ private
63
+
64
+ def error_serializer
65
+ Graphiti::ErrorSerializers::InvalidRequest
66
+ end
67
+ end
68
+
69
+ class ConflictRequestHandler < InvalidRequestHandler
70
+ private
71
+
72
+ def error_serializer
73
+ Graphiti::ErrorSerializers::ConflictRequest
74
+ end
75
+ end
76
+ end
77
+ end