graphiti 2.0.0.beta.7 → 2.0.0.beta.9

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 (251) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +56 -4
  4. data/.github/workflows/docs.yml +10 -7
  5. data/.github/workflows/notify-example-app.yml +18 -0
  6. data/.github/workflows/release.yml +1 -1
  7. data/.gitignore +2 -0
  8. data/.standard.yml +6 -0
  9. data/CHANGELOG.md +66 -0
  10. data/CONTRIBUTING.md +3 -1
  11. data/README.md +13 -206
  12. data/Rakefile +34 -3
  13. data/UPGRADING.md +2 -2
  14. data/graphiti.gemspec +8 -4
  15. data/lib/generators/graphiti/generator_mixin.rb +1 -1
  16. data/lib/generators/graphiti/install_generator.rb +1 -1
  17. data/lib/generators/graphiti/resource_generator.rb +6 -3
  18. data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
  19. data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
  20. data/lib/graphiti/audit/report.rb +6 -6
  21. data/lib/graphiti/audit.rb +3 -4
  22. data/lib/graphiti/configuration.rb +11 -4
  23. data/lib/graphiti/debugger.rb +18 -6
  24. data/lib/graphiti/delegates/pagination.rb +2 -2
  25. data/lib/graphiti/deserializer.rb +3 -3
  26. data/lib/graphiti/error_serializers/deprecated_constants.rb +23 -0
  27. data/lib/graphiti/error_serializers/invalid_request.rb +6 -0
  28. data/lib/graphiti/errors.rb +22 -6
  29. data/lib/graphiti/extensions/temp_id.rb +1 -1
  30. data/lib/graphiti/query.rb +5 -4
  31. data/lib/graphiti/rails/context.rb +2 -1
  32. data/lib/graphiti/rails/controller.rb +3 -0
  33. data/lib/graphiti/rails/debugging.rb +1 -1
  34. data/lib/graphiti/rails/responders.rb +2 -1
  35. data/lib/graphiti/rails.rb +14 -0
  36. data/lib/graphiti/railtie.rb +8 -0
  37. data/lib/graphiti/resource/configuration.rb +1 -0
  38. data/lib/graphiti/resource/dsl.rb +2 -2
  39. data/lib/graphiti/resource/links.rb +1 -1
  40. data/lib/graphiti/resource/persistence.rb +1 -1
  41. data/lib/graphiti/resource/polymorphism.rb +1 -1
  42. data/lib/graphiti/resource.rb +2 -2
  43. data/lib/graphiti/resource_proxy.rb +0 -1
  44. data/lib/graphiti/scope.rb +68 -21
  45. data/lib/graphiti/scoping/filter.rb +5 -3
  46. data/lib/graphiti/scoping/paginate.rb +1 -1
  47. data/lib/graphiti/scoping/sort.rb +1 -1
  48. data/lib/graphiti/sideload/has_many.rb +1 -1
  49. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +11 -4
  50. data/lib/graphiti/sideload.rb +8 -6
  51. data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
  52. data/lib/graphiti/spec_helpers/matchers.rb +1 -1
  53. data/lib/graphiti/types.rb +5 -1
  54. data/lib/graphiti/util/cache_debug.rb +3 -3
  55. data/lib/graphiti/util/hash.rb +1 -1
  56. data/lib/graphiti/util/persistence.rb +6 -6
  57. data/lib/graphiti/util/serializer_relationships.rb +11 -2
  58. data/lib/graphiti/version.rb +1 -1
  59. data/lib/graphiti.rb +1 -1
  60. data/lib/tasks/graphiti.rake +1 -1
  61. data/package-lock.json +114 -0
  62. data/package.json +16 -3
  63. metadata +33 -198
  64. data/docs/concepts/backends-and-models.md +0 -122
  65. data/docs/concepts/endpoints.md +0 -183
  66. data/docs/concepts/links.md +0 -212
  67. data/docs/concepts/overview.md +0 -80
  68. data/docs/concepts/persisting.md +0 -376
  69. data/docs/concepts/relationships.md +0 -627
  70. data/docs/concepts/resources.md +0 -677
  71. data/docs/getting-started/first-api.md +0 -289
  72. data/docs/getting-started/installation.md +0 -185
  73. data/docs/intro.md +0 -307
  74. data/docs/js/authentication.md +0 -63
  75. data/docs/js/ddau.md +0 -20
  76. data/docs/js/extra-params.md +0 -41
  77. data/docs/js/index.md +0 -112
  78. data/docs/js/installation.md +0 -120
  79. data/docs/js/middleware.md +0 -72
  80. data/docs/js/models.md +0 -202
  81. data/docs/js/reads.md +0 -494
  82. data/docs/js/state-syncing.md +0 -100
  83. data/docs/js/writes.md +0 -373
  84. data/docs/reference/vandal.md +0 -63
  85. data/docs/reference/why.md +0 -13
  86. data/docs/topics/authorization.md +0 -155
  87. data/docs/topics/caching.md +0 -55
  88. data/docs/topics/customizing-sideloads.md +0 -156
  89. data/docs/topics/debugging.md +0 -242
  90. data/docs/topics/error-handling.md +0 -210
  91. data/docs/topics/etags.md +0 -46
  92. data/docs/topics/hopping-relationships.md +0 -149
  93. data/docs/topics/json-attributes.md +0 -77
  94. data/docs/topics/openstruct-models.md +0 -50
  95. data/docs/topics/remote-resources.md +0 -291
  96. data/docs/topics/testing.md +0 -916
  97. data/docs/topics/without-activerecord.md +0 -324
  98. data/docs/tutorial/index.md +0 -58
  99. data/docs/tutorial/step_0.md +0 -107
  100. data/docs/tutorial/step_1.md +0 -199
  101. data/docs/tutorial/step_2.md +0 -312
  102. data/docs/tutorial/step_3.md +0 -142
  103. data/docs/tutorial/step_4.md +0 -135
  104. data/docs/tutorial/step_5.md +0 -69
  105. data/docs/tutorial/step_6.md +0 -82
  106. data/docs/tutorial/step_7.md +0 -205
  107. data/docs/tutorial/step_8.md +0 -128
  108. data/docs/tutorial/step_9.md +0 -171
  109. data/docs/upgrading.md +0 -267
  110. data/website/.gitignore +0 -20
  111. data/website/README.md +0 -43
  112. data/website/docusaurus.config.js +0 -197
  113. data/website/package-lock.json +0 -19474
  114. data/website/package.json +0 -46
  115. data/website/sidebars.js +0 -82
  116. data/website/src/css/custom.css +0 -85
  117. data/website/src/pages/markdown-page.mdx +0 -7
  118. data/website/static/.nojekyll +0 -0
  119. data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
  120. data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
  121. data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
  122. data/website/static/1.13/2019/10/14/tutorial.html +0 -198
  123. data/website/static/1.13/CNAME +0 -1
  124. data/website/static/1.13/README.md +0 -16
  125. data/website/static/1.13/assets/css/syntax.css +0 -60
  126. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  127. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  128. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  129. data/website/static/1.13/assets/favicons/browserconfig.xml +0 -9
  130. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  131. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  132. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  133. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  134. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +0 -1
  135. data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
  136. data/website/static/1.13/assets/img/backend.gif +0 -0
  137. data/website/static/1.13/assets/img/conformity.png +0 -0
  138. data/website/static/1.13/assets/img/error_payload.png +0 -0
  139. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  140. data/website/static/1.13/assets/img/gh.png +0 -0
  141. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  142. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  143. data/website/static/1.13/assets/img/logo-500.png +0 -0
  144. data/website/static/1.13/assets/img/logo.png +0 -0
  145. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  146. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  147. data/website/static/1.13/assets/img/persist.jpg +0 -0
  148. data/website/static/1.13/assets/img/resource.gif +0 -0
  149. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  150. data/website/static/1.13/assets/img/rest1.gif +0 -0
  151. data/website/static/1.13/assets/img/rest2.gif +0 -0
  152. data/website/static/1.13/assets/img/rest3.gif +0 -0
  153. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  154. data/website/static/1.13/assets/img/sunrise.png +0 -0
  155. data/website/static/1.13/assets/img/why.png +0 -0
  156. data/website/static/1.13/assets/js/highlight.pack.js +0 -2
  157. data/website/static/1.13/assets/main.css +0 -15518
  158. data/website/static/1.13/assets/main.css.map +0 -1
  159. data/website/static/1.13/bin/bundle +0 -109
  160. data/website/static/1.13/bin/jekyll +0 -27
  161. data/website/static/1.13/bin/kramdown +0 -27
  162. data/website/static/1.13/bin/listen +0 -27
  163. data/website/static/1.13/bin/rake +0 -27
  164. data/website/static/1.13/bin/rougify +0 -27
  165. data/website/static/1.13/bin/safe_yaml +0 -27
  166. data/website/static/1.13/bin/sass +0 -27
  167. data/website/static/1.13/bin/sass-convert +0 -27
  168. data/website/static/1.13/bin/scss +0 -27
  169. data/website/static/1.13/blog.html +0 -259
  170. data/website/static/1.13/cheatsheet.html +0 -316
  171. data/website/static/1.13/cookbooks/authorization.md +0 -0
  172. data/website/static/1.13/cookbooks/caching.md +0 -0
  173. data/website/static/1.13/cookbooks/customizing-sideloads.html +0 -325
  174. data/website/static/1.13/cookbooks/etags.md +0 -0
  175. data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
  176. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  177. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  178. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  179. data/website/static/1.13/cookbooks/without-activerecord.html +0 -510
  180. data/website/static/1.13/features.html +0 -249
  181. data/website/static/1.13/feed.xml +0 -106
  182. data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
  183. data/website/static/1.13/guides/concepts/debugging.html +0 -440
  184. data/website/static/1.13/guides/concepts/endpoints.html +0 -432
  185. data/website/static/1.13/guides/concepts/error-handling.html +0 -396
  186. data/website/static/1.13/guides/concepts/links.html +0 -501
  187. data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
  188. data/website/static/1.13/guides/concepts/resources.html +0 -2176
  189. data/website/static/1.13/guides/concepts/testing.html +0 -1469
  190. data/website/static/1.13/guides/getting-started/installation.html +0 -420
  191. data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
  192. data/website/static/1.13/guides/index.html +0 -269
  193. data/website/static/1.13/guides/overview.html +0 -325
  194. data/website/static/1.13/guides/upgrading-2-0.html +0 -193
  195. data/website/static/1.13/guides/upgrading.html +0 -314
  196. data/website/static/1.13/guides/vandal.html +0 -282
  197. data/website/static/1.13/guides/why.html +0 -1121
  198. data/website/static/1.13/index.html +0 -72
  199. data/website/static/1.13/js/authentication.html +0 -295
  200. data/website/static/1.13/js/ddau.html +0 -238
  201. data/website/static/1.13/js/extra-params.html +0 -270
  202. data/website/static/1.13/js/index.html +0 -321
  203. data/website/static/1.13/js/installation.html +0 -637
  204. data/website/static/1.13/js/introduction.html +0 -257
  205. data/website/static/1.13/js/middleware.html +0 -318
  206. data/website/static/1.13/js/reads/fieldsets.html +0 -271
  207. data/website/static/1.13/js/reads/filtering.html +0 -289
  208. data/website/static/1.13/js/reads/includes.html +0 -260
  209. data/website/static/1.13/js/reads/index.html +0 -497
  210. data/website/static/1.13/js/reads/nested-queries.html +0 -353
  211. data/website/static/1.13/js/reads/pagination.html +0 -260
  212. data/website/static/1.13/js/reads/sorting.html +0 -265
  213. data/website/static/1.13/js/reads/statistics.html +0 -289
  214. data/website/static/1.13/js/state-syncing.html +0 -340
  215. data/website/static/1.13/js/writes/deferred.html +0 -296
  216. data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
  217. data/website/static/1.13/js/writes/index.html +0 -391
  218. data/website/static/1.13/js/writes/nested.html +0 -330
  219. data/website/static/1.13/js/writes/validations.html +0 -272
  220. data/website/static/1.13/quickstart.html +0 -660
  221. data/website/static/1.13/template +0 -161
  222. data/website/static/1.13/tutorial/index.html +0 -250
  223. data/website/static/1.13/tutorial/step_0.html +0 -292
  224. data/website/static/1.13/tutorial/step_1.html +0 -517
  225. data/website/static/1.13/tutorial/step_2.html +0 -481
  226. data/website/static/1.13/tutorial/step_3.html +0 -323
  227. data/website/static/1.13/tutorial/step_4.html +0 -318
  228. data/website/static/1.13/tutorial/step_5.html +0 -265
  229. data/website/static/1.13/tutorial/step_6.html +0 -276
  230. data/website/static/1.13/tutorial/step_7.html +0 -390
  231. data/website/static/1.13/tutorial/step_8.html +0 -316
  232. data/website/static/1.13/tutorial/step_9.html +0 -365
  233. data/website/static/CNAME +0 -1
  234. data/website/static/assets/img/error_payload.png +0 -0
  235. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  236. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  237. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  238. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  239. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  240. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  241. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  242. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  243. data/website/static/assets/img/meta_total_count.png +0 -0
  244. data/website/static/img/docusaurus-social-card.jpg +0 -0
  245. data/website/static/img/docusaurus.png +0 -0
  246. data/website/static/img/favicon.ico +0 -0
  247. data/website/static/img/logo.png +0 -0
  248. data/website/static/img/logo.svg +0 -1
  249. data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
  250. data/website/static/img/undraw_docusaurus_react.svg +0 -170
  251. data/website/static/img/undraw_docusaurus_tree.svg +0 -40
@@ -1,212 +0,0 @@
1
- ---
2
- title: 'Links'
3
- ---
4
-
5
- # Links
6
-
7
- ## Overview {#overview}
8
-
9
- A [Link](http://jsonapi.org/format/#document-links) is a URL Graphiti puts in a relationship, pointing at the data so a client can fetch it separately. Every relationship gets one automatically:
10
-
11
- ```ruby
12
- class PostResource < ApplicationResource
13
- has_many :comments
14
- end
15
- ```
16
-
17
- `GET /posts/123` renders the `comments` relationship with a `links.related` of `/comments?filter[post_id]=123`. The client follows that URL when it wants the comments, rather than asking for them up front with `?include=comments`.
18
-
19
- ### Why links {#why-links}
20
-
21
- The URL matters most when the relationship means something more specific than "all the comments". Say `top_comments` is defined as 100 upvotes or more. A [`params` block](#linking-relationships) puts that into the generated Link, and the client still follows a URL.
22
-
23
- The alternative is for clients to build that query themselves, which means every client (desktop, mobile, third-party) has to know what a "Top Comment" is and ship an update whenever the definition changes. Hiding it behind a dedicated `/top_comments` endpoint moves the problem rather than solving it: clients still have to know to hit a special endpoint, and nothing keeps its definition in sync with the eager-loaded one.
24
-
25
- With a Link, the definition lives in one place. Change it to 500 upvotes, factor in recency, subtract downvotes: clients keep following the same URL.
26
-
27
-
28
- ## Linking Relationships {#linking-relationships}
29
-
30
- When defining a relationship, we get a Link for free:
31
-
32
- ```ruby
33
- class PostResource < ApplicationResource
34
- has_many :comments
35
- end
36
- ```
37
-
38
- > `/comments?filter[post_id]=123`
39
-
40
- And when customizing a relationship with `params`, our Link will be
41
- updated:
42
-
43
- ```ruby
44
- has_many :comments do
45
- params do |hash|
46
- hash[:filter][:upvotes] = { gte: 100 }
47
- end
48
- end
49
- ```
50
-
51
- > `/comments?filter[post_id]=123&filter[upvotes][gte]=100`
52
-
53
- Note: if you use the `scope` block directly, it may cause incorrect links. Avoid using `scope` directly and instead use `params` and `pre_load` if possible.
54
-
55
- To manually generate a Link:
56
-
57
- ```ruby
58
- has_many :comments do
59
- link do |post|
60
- helpers = Rails.application.routes.url_helpers
61
- helpers.comments_url(params: { filter: { post_id: post.id } })
62
- # or
63
- # http://example.com/api/v1/comments?filter[post_id]=123
64
- end
65
- end
66
- ```
67
-
68
- To avoid a Relationship Link altogether:
69
-
70
- ```ruby
71
- has_many :comments, link: false
72
- ```
73
-
74
- ## Resource Endpoints {#resource-endpoints}
75
-
76
- To generate links, we need to associate a Resource to a URL. By default,
77
- this happens automatically:
78
-
79
- ```ruby
80
- class ApplicationResource < Graphiti::Resource
81
- # ... code ...
82
- self.endpoint_namespace = '/api/v1'
83
- end
84
-
85
- class PostResource < ApplicationResource
86
- # under the hood:
87
- primary_endpoint 'posts',
88
- [:index, :show, :create, :update, :destroy]
89
- end
90
- ```
91
-
92
- Which would generate links to `/api/v1/posts`.
93
-
94
- ### Validation {#validation}
95
-
96
- Associating a Resource to an Endpoint serves two purposes. We've gone
97
- over link generation. But we also want to make sure we're not linking to
98
- something that doesn't actually exist. That's why we perform **Endpoint
99
- Validation**.
100
-
101
- If we tried to access the above resource at a `/comments` endpoint:
102
-
103
- ```ruby
104
- class CommentsController < ApplicationController
105
- def index
106
- PostResource.all(params)
107
- # ...
108
- end
109
- end
110
- ```
111
-
112
- We'd get a `Graphiti::Errors::InvalidEndpoint` error. Endpoint
113
- validation ensures that our auto-generated Links are actually valid.
114
-
115
- To change the endpoint associated to a Resource:
116
-
117
- ```ruby
118
- primary_endpoint 'special_posts', [:index, :show]
119
- ```
120
-
121
- Or to alter only the **path**:
122
-
123
- ```ruby
124
- self.endpoint[:path] = 'special_posts'
125
- ```
126
-
127
- Or to alter only the **actions** supported:
128
-
129
- ```ruby
130
- self.endpoint[:actions] = [:index, :show]
131
- ```
132
-
133
- A resource may be accessible by multiple endpoints. Maybe `PostResource` is also used at `/top_posts`. We want to keep all auto-generated links pointing to `/posts` (the primary endpoint), but *allow* accessing `PostResource` from the `/top_posts` endpoint:
134
-
135
- ```ruby
136
- secondary_endpoint '/top_posts', [:index]
137
- ```
138
-
139
- ## Configuration {#configuration}
140
-
141
- ### Autolinking {#autolinking}
142
-
143
- To turn off automatically generated links:
144
-
145
- ```ruby
146
- class ApplicationResource < Graphiti::Resource
147
- self.autolink = false
148
- end
149
- ```
150
-
151
- ### Endpoint Validation {#endpoint-validation}
152
-
153
- To turn off Endpoint Validation:
154
-
155
- ```ruby
156
- class ApplicationResource < Graphiti::Resource
157
- self.validate_endpoints = false
158
- end
159
- ```
160
-
161
- ### Links-on-Demand {#links-on-demand}
162
-
163
- To only render links when requested in the URL with `?links=true`:
164
-
165
- ```ruby
166
- Graphiti.configure do |c|
167
- c.links_on_demand = true
168
- end
169
- ```
170
-
171
- ### Pagination Links {#pagination-links}
172
-
173
- Requesting large collections can make for slow responses. [Pagination](https://jsonapi.org/format/#fetching-pagination) breaks the response into smaller pieces, and pagination links tell the client how to walk them. They can appear in a response two ways.
174
-
175
- #### Showing by default {#pagination-links-showing-by-default}
176
-
177
- Every collection response returns pagination links:
178
-
179
- ```ruby
180
- Graphiti.configure do |c|
181
- c.pagination_links = true
182
- end
183
- ```
184
-
185
- #### When requested {#pagination-links-when-requested}
186
-
187
- Links are rendered only when the request asks for them with `?pagination_links=true`. Worth doing when the collection is large: the `last` link needs a total count, so rendering links costs a `stat(:total, :count)` on every request that gets them.
188
-
189
- ```ruby
190
- Graphiti.configure do |c|
191
- c.pagination_links_on_demand = true
192
- end
193
- ```
194
-
195
- Pagination links won't show up for *#show* actions.
196
-
197
- ### Custom Endpoint URLs {#custom-endpoint-urls}
198
-
199
- To change the URL associated with a Resource:
200
-
201
- ```ruby
202
- class PostResource < ApplicationResource
203
- # Most commonly seen in ApplicationResource
204
- self.endpoint_namespace = '/api/v1'
205
-
206
- primary_endpoint '/posts', [:index, :show]
207
- # OR
208
- self.endpoint[:path] = '/posts'
209
- # OR
210
- self.endpoint[:actions] = [:index, :show]
211
- end
212
- ```
@@ -1,80 +0,0 @@
1
- ---
2
- title: 'Lifecycle of a Request'
3
- ---
4
-
5
- # Lifecycle of a Request
6
-
7
- A request goes down through a Resource to your data, and comes back up as a serialized response.
8
-
9
- <figure>
10
- <svg viewBox="0 0 790 300" role="img" aria-label="A request reaches an Endpoint (your Rails controller), then a Resource. The Resource builds a scope from the request, queries your Backend through an Adapter, resolves the results into Models, and serializes them into the JSON:API response that goes back to the client. The Resource and the JSON:API response are Graphiti. The Endpoint and Backend are yours." style={{width: '100%', height: 'auto'}}>
11
- <defs>
12
- <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
13
- <path d="M0 0 L10 5 L0 10 z" fill="var(--ifm-color-emphasis-500)"/>
14
- </marker>
15
- <marker id="arrowStart" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
16
- <path d="M10 0 L0 5 L10 10 z" fill="var(--ifm-color-emphasis-500)"/>
17
- </marker>
18
- </defs>
19
-
20
- <g fill="none" stroke="var(--ifm-color-emphasis-500)" strokeWidth="1.5">
21
- <path d="M150 64 H176" markerEnd="url(#arrow)"/>
22
- <path d="M310 64 H336" markerEnd="url(#arrow)"/>
23
- <path d="M574 96 H626" markerEnd="url(#arrow)" markerStart="url(#arrowStart)"/>
24
- <path d="M336 230 H156" markerEnd="url(#arrow)"/>
25
- <path d="M85 206 V92" markerEnd="url(#arrow)"/>
26
- </g>
27
-
28
- <g stroke="var(--ifm-color-emphasis-300)" fill="var(--ifm-background-surface-color)">
29
- <rect x="20" y="40" width="130" height="48" rx="6"/>
30
- <rect x="180" y="40" width="130" height="48" rx="6"/>
31
- <rect x="630" y="70" width="140" height="52" rx="6"/>
32
- </g>
33
- <g stroke="var(--ifm-color-primary)" strokeWidth="1.5" fill="none">
34
- <rect x="340" y="30" width="230" height="230" rx="8"/>
35
- <rect x="20" y="206" width="130" height="48" rx="6"/>
36
- </g>
37
-
38
- <g fontFamily="var(--ifm-font-family-base)" fontSize="15" fill="var(--ifm-font-color-base)" textAnchor="middle">
39
- <text x="85" y="70">Request</text>
40
- <text x="245" y="70">Endpoint</text>
41
- <text x="700" y="102">Backend</text>
42
- <text x="85" y="236">JSON:API</text>
43
- </g>
44
- <g fontFamily="var(--ifm-font-family-base)" fontSize="15" fill="var(--ifm-color-primary)" textAnchor="middle">
45
- <text x="455" y="62">Resource</text>
46
- </g>
47
-
48
- <g fontFamily="var(--ifm-font-family-base)" fontSize="12.5" fill="var(--ifm-color-emphasis-700)" textAnchor="middle">
49
- <text x="455" y="104">base_scope + filters,</text>
50
- <text x="455" y="122">sorts, pagination</text>
51
- <text x="455" y="163">resolve(scope)</text>
52
- <text x="455" y="181">to your Models</text>
53
- <text x="455" y="222">serialize</text>
54
- <text x="600" y="86">Adapter</text>
55
- </g>
56
- <g fontFamily="var(--ifm-font-family-base)" fontSize="12.5" fill="var(--ifm-color-emphasis-700)">
57
- <text x="95" y="152">response</text>
58
- </g>
59
- </svg>
60
- </figure>
61
-
62
- Graphiti is the highlighted part: the Resource, and the JSON:API it renders. The Endpoint is your Rails controller, which handles routing, response codes and MIME types. The Backend is yours too.
63
-
64
- | Piece | What it does |
65
- | --- | --- |
66
- | [Endpoint](/concepts/endpoints) | Your controller. Graphiti registers its path and actions, which drives link generation and endpoint validation, and lets you vary a Resource's behavior per route. |
67
- | [Resource](/concepts/resources) | Turns request params into a **scope**, resolves that scope into Models, and serializes them on the way back out. |
68
- | Adapter | Reusable glue between a Resource and a Backend. Defaults to `Graphiti::Adapters::ActiveRecord`. |
69
- | [Backend](/concepts/backends-and-models) | Whatever you query: a database, a search index, an HTTP service. |
70
- | [Model](/concepts/backends-and-models) | What you return and serialize. With ActiveRecord, the same object as the Backend. |
71
-
72
- ## The graph
73
-
74
- Resources connect to other Resources:
75
-
76
- * **Sideloading**: fetch an employee, her positions, and those positions' departments in one request
77
- * **Sideposting**: *save* an employee and her positions in one request
78
- * **[Links](/concepts/links)**: a URL to lazy-load positions in a separate request
79
-
80
- Query logic written for one Resource applies at every level of that graph, so you can ask for an employee and her last three positions ordered by `created_at`. That's [deep querying](/concepts/relationships#deep-queries).
@@ -1,376 +0,0 @@
1
- ---
2
- title: 'Persisting'
3
- ---
4
-
5
- # Persisting {#persisting}
6
-
7
- This page covers how Graphiti writes data: the persistence lifecycle, sideposting a graph of resources in one request, validation errors, and reading data back after a write.
8
-
9
- Graphiti allows writing a graph of data in a single request. We'll do
10
- the work of parsing the graph and ordering operations, so you can focus
11
- on the part you care about: the logic for actually persisting an object.
12
-
13
- By default, persistence operations are handled by your adapter, and the flow breaks into three steps: build or find the model, assign attributes to it, then save it.
14
-
15
- Attributes are assigned up front, before the persistence hooks run. That means the model exists (populated but unwritten) before anything touches the database, and **the model you inspect is the model that saves**:
16
-
17
- ```ruby
18
- employee = EmployeeResource.build(payload)
19
-
20
- employee.data # the model, attributes already assigned, nothing written yet
21
- employee.data.valid? # inspect it, or modify it
22
- employee.save # persists that same instance
23
- ```
24
-
25
- Reading `data` repeatedly returns the same instance, and the attribute callbacks run only once no matter how often you read it. For an update, the proxy reads the persisted record until you apply the payload:
26
-
27
- ```ruby
28
- proxy = EmployeeResource.find(payload)
29
- proxy.data.first_name # => "asdf", straight from the database
30
- proxy.assign_attributes(payload)
31
- proxy.data.first_name # => "Jane", assigned but not yet persisted
32
- proxy.save(action: :update)
33
- ```
34
-
35
- `assign_attributes` validates the payload and runs your writable guards, but writes nothing. `#save` will not re-validate a payload it already validated, so inspecting the model costs no extra guard evaluations. `ResourceProxy#update` is the Rails-style shorthand that assigns and saves in one call.
36
-
37
- You can override `#create`, `#update` and `#destroy` on a Resource, but you are encouraged **not** to. Use the hooks below instead. If you do override them, `#create` and `#update` receive an attributes hash while `#destroy` receives an id, and all three **must return the Model instance**. Graphiti processes any `writable: false` or guarded attributes before these methods run, and checks the returned Model for validation errors afterward, rolling back the transaction if any Model in the graph is invalid.
38
-
39
- ## Persistence Lifecycle Hooks {#persistence-lifecycle-hooks}
40
-
41
- Let's dive into a persistence request. If you look at the code snippets in
42
- the prior section, the flow breaks down into 3 steps:
43
-
44
- * Build or find the model
45
- * Assign attributes to the model
46
- * Save
47
-
48
- You can hook into each step:
49
-
50
- ```ruby
51
- class PostResource < ApplicationResource
52
- before_attributes do |attributes|
53
- # Before attributes have been assigned to the model
54
- end
55
-
56
- after_attributes do |model|
57
- # After attributes have been assigned to the model
58
- end
59
-
60
- around_attributes :do_around_attributes
61
-
62
- def do_around_attributes(attributes)
63
- # before
64
- model_instance = yield attributes
65
- # after
66
- end
67
-
68
- before_save do |model|
69
- # After attributes assigned, but before persisting
70
- end
71
-
72
- after_save do |model|
73
- # After model has been saved
74
- end
75
-
76
- around_save :do_around_save
77
-
78
- def do_around_save(model)
79
- # before
80
- yield model
81
- # after
82
- end
83
-
84
- # This is an *override*
85
- # During #create, build a blank model instance
86
- # By default, we'd call adapter.build(model_class)
87
- def build(model_class)
88
- model_class.new
89
- end
90
-
91
- # This is an *override*
92
- # During #create/#update, assign new attributes to the model instance
93
- # By default, we'd call adapter.assign_attributes(model_instance, attributes)
94
- def assign_attributes(model_instance, attributes)
95
- attributes.each_pair do |key, value|
96
- model_instance.send(:"#{key}=", value)
97
- end
98
- end
99
-
100
- # This is an *override*
101
- # During #create/#update, actually save the model instance
102
- # By default, we'd call adapter.save(model_instance)
103
- def save(model_instance)
104
- model_instance.save
105
- model_instance
106
- end
107
-
108
-
109
- # This is an *override*
110
- # During #destroy, actually save the model instance
111
- # By default, we'd call adapter.destroy(model_instance)
112
- def delete(model_instance)
113
- model_instance.destroy
114
- model_instance
115
- end
116
-
117
- # Finally, you may want to hook around *all* the above steps:
118
- # Only applies to #create/#update
119
- around_persistence :do_around_persistence
120
-
121
- def do_around_persistence(attributes)
122
- attributes[:foo] = 'bar'
123
- model = yield # build/find, assign attrs, save
124
- model.update_counter_cache
125
- end
126
- end
127
- ```
128
-
129
- * All hooks have `only/except` options, e.g. `before_attributes only: [:update]`
130
- * Most hooks can be called with an in-line block, or by passing a method
131
- name (e.g. `before_attributes :do_something`). The exception is `around_*` hooks, which *must* be called with a method name.
132
-
133
- When persisting multiple objects at once, we'll open a database
134
- transaction, process each model individually, ensure all models pass
135
- validation, then close the transaction. This means that if you raise an
136
- error at any point, or any model does not pass validations, the
137
- transaction will be rolled back.
138
-
139
- You may want to perform an operation after all models have been
140
- processed and validated, but before the transaction is closed. One
141
- example is sending an email - you don't want to send if the models were
142
- invalid, so `after_save` wouldn't work. And you still want to do it
143
- *within* the transaction, so if your email server is down and an error
144
- is raised the transaction gets rolled back.
145
-
146
- For this scenario, use `before_commit`:
147
-
148
- ```ruby
149
- before_commit do |model|
150
- PostMailer.with(post: model).some_email.deliver
151
- end
152
- ```
153
-
154
- ## Sideposting {#sideposting}
155
-
156
- The act of persisting multiple Resources in a single request is called
157
- **Sideposting**. The payload mirrors the **sideloading** payload for
158
- read operations, with minor additions.
159
-
160
- Let's create a Post and associate it to an existing Blog in a single
161
- request:
162
-
163
- ```ruby
164
- # POST /api/v1/posts
165
- {
166
- type: 'posts',
167
- attributes: { title: 'My post' },
168
- relationships: {
169
- blog: {
170
- data: {
171
- id: '1',
172
- type: 'blogs',
173
- method: 'update'
174
- }
175
- }
176
- }
177
- }
178
- ```
179
-
180
- The critical addition here is the `method` key. When we persist RESTful
181
- Resources, we send a corresponding HTTP verb. This follows the same
182
- pattern, adding a verb for each Resource in the graph. `method` can be
183
- one of:
184
-
185
- * `create`
186
- * `update`
187
- * `destroy`
188
- * `disassociate` (e.g. `null` foreign key)
189
-
190
- When we sidepost, all objects will be persisted within the same database
191
- transaction, which rolls back if an error is raised or any objects are invalid.
192
-
193
- ### Create {#create}
194
-
195
- Let's say we want to create a Post and its Blog in a single request.
196
- You'll note that we don't have the `id` key to generate a [Resource Identifier](http://jsonapi.org/format/#document-resource-identifier-objects) (combination of `id` and `type`
197
- that uniquely identifies a Resource).
198
-
199
- To accomodate this, send an ephemeral `temp-id` (any UUID):
200
-
201
- ```ruby
202
- {
203
- # POST /api/v1/posts
204
- {
205
- type: 'posts',
206
- attributes: { title: 'My post' },
207
- relationships: {
208
- blog: {
209
- data: {
210
- :'temp-id' => 'abc123',
211
- type: 'blogs',
212
- method: 'create'
213
- }
214
- }
215
- },
216
- included: [
217
- {
218
- :'temp-id' => 'abc123'
219
- type: 'blogs',
220
- attributes: { name: 'New Blog' }
221
- }
222
- ]
223
- }
224
- }
225
- ```
226
-
227
- This random UUID:
228
-
229
- * Connects relevant sections of the payload.
230
- * Tells clients how to associate their in-memory objects with the ids returned from the server.
231
-
232
- ### Expanded Example {#expanded-example}
233
-
234
- Here we're updating a Post, changing the name of its associated Blog, creating a Tag, deleting one Comment, and disassociating (`null` foreign key) a different Comment, all in a single request:
235
-
236
- ```ruby
237
- {
238
- data: {
239
- type: 'posts',
240
- id: 123,
241
- attributes: { title: 'Updated!' },
242
- relationships: {
243
- blog: {
244
- data: {
245
- type: 'blogs',
246
- id: 123,
247
- method: 'update'
248
- }
249
- },
250
- tags: {
251
- data: [{
252
- type: 'tags',
253
- temp-id: 's0m3uu1d',
254
- method: 'create'
255
- }]
256
- },
257
- comments: {
258
- data: [
259
- {
260
- type: 'comments',
261
- id: '123',
262
- method: 'destroy'
263
- },
264
- {
265
- type: 'comments',
266
- id: '456',
267
- method: 'disassociate'
268
- }
269
- ]
270
- }
271
- }
272
- },
273
- included: [
274
- {
275
- type: 'tags',
276
- :'temp-id' => 's0m3uu1d',
277
- attributes: { name: 'Important' }
278
- },
279
- {
280
- type: 'blogs',
281
- id: => '123',
282
- attributes: { name: 'Updated!' }
283
- }
284
- ]
285
- }
286
- ```
287
-
288
- ## Validation Errors {#validation-errors}
289
-
290
- When a persistence operation is attempted but the corresponding Resource
291
- is invalid, the transaction will be rolled back and an [errors payload](http://jsonapi.org/format/#errors) will be returned
292
- with a `422` response code:
293
-
294
- ```ruby
295
- {
296
- errors: [{
297
- code: 'unprocessable_entity',
298
- status: '422',
299
- title: "Validation Error",
300
- detail: "Title can't be blank",
301
- source: { pointer: '/data/attributes/title' },
302
- meta: {
303
- attribute: :title,
304
- message: "can't be blank",
305
- code: :blank
306
- }
307
- }]
308
- }
309
- ```
310
-
311
- To get this functionality, your Model must adhere to the
312
- [ActiveModel::Validations API](https://api.rubyonrails.org/classes/ActiveModel/Validations.html).
313
-
314
- You get this for free with ActiveRecord, or it can be mixed in to any
315
- PORO:
316
-
317
- ```ruby
318
- class Post
319
- include ActiveModel::Validations
320
- validates :title, presence: true
321
- end
322
- ```
323
-
324
- Errors on associations will have a slightly expanded payload:
325
-
326
- ```ruby
327
- {
328
- errors: [{
329
- code: 'unprocessable_entity',
330
- status: '422',
331
- title: 'Validation Error',
332
- detail: "Name can't be blank",
333
- source: { pointer: '/data/attributes/name' },
334
- meta: {
335
- relationship: {
336
- attribute: :name,
337
- message: "can't be blank",
338
- code: :blank,
339
- name: :pets,
340
- id: '444',
341
- type: 'pets'
342
- }
343
- }
344
- }]
345
- }
346
- ```
347
-
348
- When [Sideposting](#sideposting), the errors payload will contain all
349
- invalid Resources in the graph.
350
-
351
- ## Read on Write {#read-on-write}
352
-
353
- By default, the response of a persistence operation will mirror your
354
- request. But sometimes you need control over the response. The most
355
- common scenario is sideloading an additional entity - imagine creating
356
- an order, and wanting the order's shipping information to come back in
357
- the response.
358
-
359
- You can do this by POSTing the payload as normal, but adding query
360
- parameters to the URL:
361
-
362
- ```ruby
363
- # POST /api/v1/orders?include=shipping_information
364
-
365
- {
366
- type: 'orders',
367
- attributes: { ... }
368
- }
369
- ```
370
-
371
- This will sideload the shipping information in the response. When using
372
- [Spraypaint](/js/), do this with:
373
-
374
- ```typescript
375
- order.save({ returnScope: Order.includes('shipping_information') })
376
- ```