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
@@ -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
@@ -84,25 +84,6 @@ module Graphiti
84
84
  end
85
85
  end
86
86
 
87
- class AssignedModelNotSupported < Base
88
- def initialize(resource_class, method_name)
89
- @resource_class = resource_class
90
- @method_name = method_name
91
- end
92
-
93
- def message
94
- <<~MSG
95
- #{@resource_class}: the model was already assigned via #assign_attributes (or #data before save), but ##{@method_name} is overridden without a parameter to receive it, so changes made to the unsaved model would be silently lost.
96
-
97
- Accept and use the assigned model in your override:
98
-
99
- def #{@method_name}(attributes, meta = nil, assigned_model: nil)
100
-
101
- or persist with #save alone, without inspecting the model first. See UPGRADING.md.
102
- MSG
103
- end
104
- end
105
-
106
87
  class RemoteWrite < Base
107
88
  def initialize(resource_class)
108
89
  @resource_class = resource_class
@@ -289,7 +270,7 @@ module Graphiti
289
270
  #{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
290
271
 
291
272
  The error was raised while attempting to build query parameters for the associated Resource.
292
- 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
293
274
 
294
275
  A good way to debug is to put a debugger within the 'params' block.
295
276
 
@@ -313,7 +294,7 @@ module Graphiti
313
294
 
314
295
  The error was raised while attempting to build the scope for the associated Resource.
315
296
 
316
- 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
317
298
 
318
299
  Here's the original, underlying error:
319
300
 
@@ -334,7 +315,7 @@ module Graphiti
334
315
  #{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
335
316
 
336
317
  The error was raised while attempting to assign relevant model instances. Read
337
- 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
338
319
 
339
320
  A good way to debug is to put a debugger within the 'assign' block.
340
321
 
@@ -468,7 +449,7 @@ module Graphiti
468
449
 
469
450
  self.validate_endpoints = false
470
451
 
471
- See https://www.graphiti.dev/guides/concepts/links for more information.
452
+ See https://www.graphiti.dev/concepts/links for more information.
472
453
 
473
454
  The current endpoints allowed for this resource are: #{@resource_class.endpoints.inspect}
474
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
@@ -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,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