graphiti 2.0.0.beta.6 → 2.0.0.beta.8

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 (246) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +55 -3
  4. data/.github/workflows/docs.yml +1 -1
  5. data/.github/workflows/notify-example-app.yml +18 -0
  6. data/.gitignore +2 -0
  7. data/.standard.yml +6 -0
  8. data/CHANGELOG.md +54 -0
  9. data/CONTRIBUTING.md +3 -1
  10. data/README.md +13 -206
  11. data/Rakefile +6 -3
  12. data/UPGRADING.md +1 -1
  13. data/graphiti.gemspec +8 -4
  14. data/lib/generators/graphiti/generator_mixin.rb +1 -1
  15. data/lib/generators/graphiti/install_generator.rb +1 -2
  16. data/lib/generators/graphiti/resource_generator.rb +6 -3
  17. data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
  18. data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
  19. data/lib/generators/graphiti/templates/index_request_spec.rb.erb +2 -2
  20. data/lib/generators/graphiti/templates/resource_reads_spec.rb.erb +4 -4
  21. data/lib/generators/graphiti/templates/show_request_spec.rb.erb +2 -2
  22. data/lib/graphiti/audit/report.rb +6 -6
  23. data/lib/graphiti/audit.rb +3 -4
  24. data/lib/graphiti/delegates/pagination.rb +2 -2
  25. data/lib/graphiti/deserializer.rb +3 -3
  26. data/lib/graphiti/errors.rb +6 -6
  27. data/lib/graphiti/extensions/temp_id.rb +1 -1
  28. data/lib/graphiti/query.rb +27 -8
  29. data/lib/graphiti/rails/context.rb +2 -1
  30. data/lib/graphiti/rails/controller.rb +3 -0
  31. data/lib/graphiti/rails/debugging.rb +1 -1
  32. data/lib/graphiti/rails/responders.rb +2 -1
  33. data/lib/graphiti/resource/configuration.rb +1 -0
  34. data/lib/graphiti/resource/dsl.rb +2 -2
  35. data/lib/graphiti/resource/interface.rb +3 -3
  36. data/lib/graphiti/resource/links.rb +1 -1
  37. data/lib/graphiti/resource/persistence.rb +1 -1
  38. data/lib/graphiti/resource/polymorphism.rb +1 -1
  39. data/lib/graphiti/resource.rb +2 -2
  40. data/lib/graphiti/resource_proxy.rb +0 -1
  41. data/lib/graphiti/runner.rb +8 -3
  42. data/lib/graphiti/scope.rb +13 -0
  43. data/lib/graphiti/scoping/filter.rb +5 -3
  44. data/lib/graphiti/scoping/paginate.rb +1 -1
  45. data/lib/graphiti/scoping/sort.rb +1 -1
  46. data/lib/graphiti/sideload/has_many.rb +1 -1
  47. data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
  48. data/lib/graphiti/spec_helpers/matchers.rb +1 -1
  49. data/lib/graphiti/spec_helpers.rb +5 -0
  50. data/lib/graphiti/types.rb +5 -1
  51. data/lib/graphiti/util/cache_debug.rb +3 -3
  52. data/lib/graphiti/util/hash.rb +1 -1
  53. data/lib/graphiti/util/persistence.rb +6 -6
  54. data/lib/graphiti/util/serializer_relationships.rb +1 -1
  55. data/lib/graphiti/version.rb +1 -1
  56. data/lib/tasks/graphiti.rake +1 -1
  57. data/package.json +1 -0
  58. metadata +31 -197
  59. data/docs/concepts/backends-and-models.md +0 -122
  60. data/docs/concepts/endpoints.md +0 -183
  61. data/docs/concepts/links.md +0 -212
  62. data/docs/concepts/overview.md +0 -80
  63. data/docs/concepts/persisting.md +0 -376
  64. data/docs/concepts/relationships.md +0 -627
  65. data/docs/concepts/resources.md +0 -677
  66. data/docs/getting-started/first-api.md +0 -289
  67. data/docs/getting-started/installation.md +0 -185
  68. data/docs/intro.md +0 -307
  69. data/docs/js/authentication.md +0 -63
  70. data/docs/js/ddau.md +0 -20
  71. data/docs/js/extra-params.md +0 -41
  72. data/docs/js/index.md +0 -112
  73. data/docs/js/installation.md +0 -120
  74. data/docs/js/middleware.md +0 -72
  75. data/docs/js/models.md +0 -202
  76. data/docs/js/reads.md +0 -494
  77. data/docs/js/state-syncing.md +0 -100
  78. data/docs/js/writes.md +0 -373
  79. data/docs/reference/vandal.md +0 -63
  80. data/docs/reference/why.md +0 -13
  81. data/docs/topics/authorization.md +0 -155
  82. data/docs/topics/caching.md +0 -55
  83. data/docs/topics/customizing-sideloads.md +0 -156
  84. data/docs/topics/debugging.md +0 -242
  85. data/docs/topics/error-handling.md +0 -210
  86. data/docs/topics/etags.md +0 -46
  87. data/docs/topics/hopping-relationships.md +0 -149
  88. data/docs/topics/json-attributes.md +0 -77
  89. data/docs/topics/openstruct-models.md +0 -50
  90. data/docs/topics/remote-resources.md +0 -291
  91. data/docs/topics/testing.md +0 -894
  92. data/docs/topics/without-activerecord.md +0 -324
  93. data/docs/tutorial/index.md +0 -58
  94. data/docs/tutorial/step_0.md +0 -107
  95. data/docs/tutorial/step_1.md +0 -199
  96. data/docs/tutorial/step_2.md +0 -312
  97. data/docs/tutorial/step_3.md +0 -142
  98. data/docs/tutorial/step_4.md +0 -135
  99. data/docs/tutorial/step_5.md +0 -69
  100. data/docs/tutorial/step_6.md +0 -82
  101. data/docs/tutorial/step_7.md +0 -205
  102. data/docs/tutorial/step_8.md +0 -128
  103. data/docs/tutorial/step_9.md +0 -171
  104. data/docs/upgrading.md +0 -266
  105. data/website/.gitignore +0 -20
  106. data/website/README.md +0 -43
  107. data/website/docusaurus.config.js +0 -197
  108. data/website/package-lock.json +0 -19474
  109. data/website/package.json +0 -46
  110. data/website/sidebars.js +0 -82
  111. data/website/src/css/custom.css +0 -85
  112. data/website/src/pages/markdown-page.mdx +0 -7
  113. data/website/static/.nojekyll +0 -0
  114. data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
  115. data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
  116. data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
  117. data/website/static/1.13/2019/10/14/tutorial.html +0 -198
  118. data/website/static/1.13/CNAME +0 -1
  119. data/website/static/1.13/README.md +0 -16
  120. data/website/static/1.13/assets/css/syntax.css +0 -60
  121. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  122. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  123. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  124. data/website/static/1.13/assets/favicons/browserconfig.xml +0 -9
  125. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  126. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  127. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  128. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  129. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +0 -1
  130. data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
  131. data/website/static/1.13/assets/img/backend.gif +0 -0
  132. data/website/static/1.13/assets/img/conformity.png +0 -0
  133. data/website/static/1.13/assets/img/error_payload.png +0 -0
  134. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  135. data/website/static/1.13/assets/img/gh.png +0 -0
  136. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  137. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  138. data/website/static/1.13/assets/img/logo-500.png +0 -0
  139. data/website/static/1.13/assets/img/logo.png +0 -0
  140. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  141. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  142. data/website/static/1.13/assets/img/persist.jpg +0 -0
  143. data/website/static/1.13/assets/img/resource.gif +0 -0
  144. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  145. data/website/static/1.13/assets/img/rest1.gif +0 -0
  146. data/website/static/1.13/assets/img/rest2.gif +0 -0
  147. data/website/static/1.13/assets/img/rest3.gif +0 -0
  148. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  149. data/website/static/1.13/assets/img/sunrise.png +0 -0
  150. data/website/static/1.13/assets/img/why.png +0 -0
  151. data/website/static/1.13/assets/js/highlight.pack.js +0 -2
  152. data/website/static/1.13/assets/main.css +0 -15518
  153. data/website/static/1.13/assets/main.css.map +0 -1
  154. data/website/static/1.13/bin/bundle +0 -109
  155. data/website/static/1.13/bin/jekyll +0 -27
  156. data/website/static/1.13/bin/kramdown +0 -27
  157. data/website/static/1.13/bin/listen +0 -27
  158. data/website/static/1.13/bin/rake +0 -27
  159. data/website/static/1.13/bin/rougify +0 -27
  160. data/website/static/1.13/bin/safe_yaml +0 -27
  161. data/website/static/1.13/bin/sass +0 -27
  162. data/website/static/1.13/bin/sass-convert +0 -27
  163. data/website/static/1.13/bin/scss +0 -27
  164. data/website/static/1.13/blog.html +0 -259
  165. data/website/static/1.13/cheatsheet.html +0 -316
  166. data/website/static/1.13/cookbooks/authorization.md +0 -0
  167. data/website/static/1.13/cookbooks/caching.md +0 -0
  168. data/website/static/1.13/cookbooks/customizing-sideloads.html +0 -325
  169. data/website/static/1.13/cookbooks/etags.md +0 -0
  170. data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
  171. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  172. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  173. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  174. data/website/static/1.13/cookbooks/without-activerecord.html +0 -510
  175. data/website/static/1.13/features.html +0 -249
  176. data/website/static/1.13/feed.xml +0 -106
  177. data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
  178. data/website/static/1.13/guides/concepts/debugging.html +0 -440
  179. data/website/static/1.13/guides/concepts/endpoints.html +0 -432
  180. data/website/static/1.13/guides/concepts/error-handling.html +0 -396
  181. data/website/static/1.13/guides/concepts/links.html +0 -501
  182. data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
  183. data/website/static/1.13/guides/concepts/resources.html +0 -2176
  184. data/website/static/1.13/guides/concepts/testing.html +0 -1469
  185. data/website/static/1.13/guides/getting-started/installation.html +0 -420
  186. data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
  187. data/website/static/1.13/guides/index.html +0 -269
  188. data/website/static/1.13/guides/overview.html +0 -325
  189. data/website/static/1.13/guides/upgrading-2-0.html +0 -193
  190. data/website/static/1.13/guides/upgrading.html +0 -314
  191. data/website/static/1.13/guides/vandal.html +0 -282
  192. data/website/static/1.13/guides/why.html +0 -1121
  193. data/website/static/1.13/index.html +0 -72
  194. data/website/static/1.13/js/authentication.html +0 -295
  195. data/website/static/1.13/js/ddau.html +0 -238
  196. data/website/static/1.13/js/extra-params.html +0 -270
  197. data/website/static/1.13/js/index.html +0 -321
  198. data/website/static/1.13/js/installation.html +0 -637
  199. data/website/static/1.13/js/introduction.html +0 -257
  200. data/website/static/1.13/js/middleware.html +0 -318
  201. data/website/static/1.13/js/reads/fieldsets.html +0 -271
  202. data/website/static/1.13/js/reads/filtering.html +0 -289
  203. data/website/static/1.13/js/reads/includes.html +0 -260
  204. data/website/static/1.13/js/reads/index.html +0 -497
  205. data/website/static/1.13/js/reads/nested-queries.html +0 -353
  206. data/website/static/1.13/js/reads/pagination.html +0 -260
  207. data/website/static/1.13/js/reads/sorting.html +0 -265
  208. data/website/static/1.13/js/reads/statistics.html +0 -289
  209. data/website/static/1.13/js/state-syncing.html +0 -340
  210. data/website/static/1.13/js/writes/deferred.html +0 -296
  211. data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
  212. data/website/static/1.13/js/writes/index.html +0 -391
  213. data/website/static/1.13/js/writes/nested.html +0 -330
  214. data/website/static/1.13/js/writes/validations.html +0 -272
  215. data/website/static/1.13/quickstart.html +0 -660
  216. data/website/static/1.13/template +0 -161
  217. data/website/static/1.13/tutorial/index.html +0 -250
  218. data/website/static/1.13/tutorial/step_0.html +0 -292
  219. data/website/static/1.13/tutorial/step_1.html +0 -517
  220. data/website/static/1.13/tutorial/step_2.html +0 -481
  221. data/website/static/1.13/tutorial/step_3.html +0 -323
  222. data/website/static/1.13/tutorial/step_4.html +0 -318
  223. data/website/static/1.13/tutorial/step_5.html +0 -265
  224. data/website/static/1.13/tutorial/step_6.html +0 -276
  225. data/website/static/1.13/tutorial/step_7.html +0 -390
  226. data/website/static/1.13/tutorial/step_8.html +0 -316
  227. data/website/static/1.13/tutorial/step_9.html +0 -365
  228. data/website/static/CNAME +0 -1
  229. data/website/static/assets/img/error_payload.png +0 -0
  230. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  231. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  232. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  233. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  234. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  235. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  236. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  237. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  238. data/website/static/assets/img/meta_total_count.png +0 -0
  239. data/website/static/img/docusaurus-social-card.jpg +0 -0
  240. data/website/static/img/docusaurus.png +0 -0
  241. data/website/static/img/favicon.ico +0 -0
  242. data/website/static/img/logo.png +0 -0
  243. data/website/static/img/logo.svg +0 -1
  244. data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
  245. data/website/static/img/undraw_docusaurus_react.svg +0 -170
  246. data/website/static/img/undraw_docusaurus_tree.svg +0 -40
@@ -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
- ```