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,627 +0,0 @@
1
- ---
2
- title: 'Relationships'
3
- ---
4
-
5
- # Relationships {#relationships}
6
-
7
- Resources rarely stand alone. This page covers how to connect them together for sideloading, sideposting, and links.
8
-
9
- Resources can connect to other Resources via **relationships**.
10
- Each relationship determines behavior for:
11
-
12
- * Sideloading (load both Resources in a single request)
13
- * Links (URL to lazy-load in separate request)
14
- * Sideposting (save both in single request)
15
-
16
- When connecting resources, you can imagine the logic similar to
17
- `ActiveRecord`'s `.includes`:
18
-
19
- ```ruby
20
- class PostResource < ApplicationResource
21
- has_many :comments
22
- end
23
-
24
- class CommentResource < ApplicationResource
25
- attribute :post_id, :integer, only: [:filterable]
26
- belongs_to :post
27
- end
28
-
29
- PostResource.all(include: 'comments')
30
- # Under the hood:
31
- # CommentResource.all(filter: { post_id: array_of_post_ids })
32
-
33
- CommentResource.all(include: 'post')
34
- # Under the hood:
35
- # PostResource.all(filter: { id: array_of_comment_ids })
36
- ```
37
-
38
- > Note the explicit `post_id` filter on `CommentResource`
39
-
40
- ## Deep Queries {#deep-queries}
41
-
42
- A query that applies to a relationship is referred to as a **deep
43
- query**. Use the dot-syntax to deep query:
44
-
45
- `/employees?include=positions&filter[positions.title]=Manager`
46
-
47
- `/employees?include=positions.department&filter[positions.department.name]=Engineering`
48
-
49
- The above references the **relationship name**. For simplicity, you can
50
- also pass the JSONAPI type in brackets:
51
-
52
- `/employees?include=positions.department&filter[departments][name]=Engineering`
53
-
54
- Sorting and pagination currently only support the JSONAPI type:
55
-
56
- `/employees?include=positions.department&sort=departments.name`
57
-
58
- `/employees?include=positions.department&page[departments][size]=10`
59
-
60
- ## Customizing Relationships {#customizing-relationships}
61
-
62
- The default options you can override are:
63
-
64
- ```ruby
65
- has_many :positions,
66
- foreign_key: :employee_id,
67
- primary_key: :id,
68
- resource: EmployeeResource,
69
- readable: true,
70
- writable: true,
71
- link: self.autolink, # default true
72
- single: false, # only allow this sideload when one employee
73
- resource_ids: false
74
- ```
75
-
76
- `resource_ids` is the one whose default depends on the relationship type:
77
-
78
- | type | renders resource ids by default |
79
- |---|---|
80
- | `belongs_to` | yes, when its foreign key already holds the related id |
81
- | `has_one` | no |
82
- | `has_many` | no |
83
- | `many_to_many` | no |
84
- | `polymorphic_belongs_to` | no |
85
-
86
- `belongs_to` renders them so a client can see which record a relationship points at without following the link:
87
-
88
- ```json
89
- "employee": {
90
- "data": { "type": "employees", "id": "1" },
91
- "links": { "related": "/employees?filter[id]=1" }
92
- }
93
- ```
94
-
95
- That costs nothing, because the id is already on the parent as its foreign key.
96
-
97
- No other relationship type has a free source for its ids. A collection accepts `resource_ids: true`, but that reads the association on every render of every parent record, whether or not the request wants the relationship. That is the N+1 from [#167](https://github.com/graphiti-api/graphiti/issues/167#issuecomment-686866646) on every response. Leave collections off and let clients `?include=` them.
98
-
99
- Not every `belongs_to` can use its foreign key. A `scope` or `params` block or a `base_scope` can filter out the record the key points at, a polymorphic target's type varies per record while rendered ids carry one type for the whole relationship, a remote resource has no local key to read, and a custom `primary_key` points the relationship at some other column. Those load the association instead, so they stay off by default too.
100
-
101
- <details>
102
- <summary>Which `belongs_to` declarations render resource ids, and which do not</summary>
103
-
104
- ```ruby
105
- # yes. employee_id is the employee's id, so the payload already has it
106
- belongs_to :employee
107
-
108
- # no. nothing renders at all, ids included
109
- belongs_to :employee, readable: false
110
-
111
- # no. employee_id holds a name, not the related id
112
- belongs_to :employee, primary_key: :first_name
113
-
114
- # no. the base scope can exclude the employee the key points at, and
115
- # graphiti cannot know whether it does without running it
116
- belongs_to :employee, base_scope: -> { Employee.all }
117
-
118
- # no. a remote resource has no local foreign key to read
119
- belongs_to :employee, remote: "http://foo.com/employees"
120
-
121
- # no. the record's own class decides its type, so the key gives an id
122
- # with no type to pair it with
123
- belongs_to :employee, resource: CreditCardResource
124
-
125
- # no. the scope can exclude the employee the key points at, and graphiti
126
- # cannot know whether it does without running it
127
- belongs_to :employee do
128
- scope { |ids| {type: :employees, conditions: {id: ids}} }
129
- end
130
-
131
- # no. same, a params filter can exclude the employee the key points at
132
- belongs_to :employee do
133
- params { |hash, positions| hash[:filter][:active] = true }
134
- end
135
-
136
- # no. credit_card_type is local, but rendered ids carry one type for the
137
- # whole relationship and this one's varies per record
138
- polymorphic_belongs_to :credit_card do
139
- group_by(:credit_card_type) do
140
- on(:Visa).belongs_to :visa, resource: VisaResource
141
- end
142
- end
143
- ```
144
-
145
- Watch for the `scope`, `params` and `base_scope` cases. Nothing about those declarations looks like it concerns resource ids, so adding a scope block to filter a relationship also stops its ids from rendering.
146
-
147
- If you keep a `schema.json`, the schema check catches that. A relationship that renders resource ids is marked `linkage: true`, and one that stops rendering them is reported as a breaking change. Gaining them is additive and passes.
148
-
149
- To render ids anyway, opt in on the relationship and accept the query:
150
-
151
- ```ruby
152
- belongs_to :employee, resource_ids: true do
153
- scope { |ids| {type: :employees, conditions: {id: ids}} }
154
- end
155
- ```
156
-
157
- Know what that buys for the `scope`, `params` and `base_scope` cases. Rendering reads the association off the model, which does not apply the block, so if the block narrows what sideloading returns, the ids will disagree with it. Opting in there says you know the two agree. A `primary_key`, polymorphic or remote relationship does resolve to the right id this way.
158
-
159
- </details>
160
-
161
- ### belongs_to_resource_ids_by_default {#belongs-to-resource-ids}
162
-
163
- To change how far a `belongs_to` goes, across a whole API, set it on the resource everything inherits from:
164
-
165
- ```ruby
166
- class ApplicationResource < Graphiti::Resource
167
- self.belongs_to_resource_ids_by_default = :foreign_key
168
- end
169
- ```
170
-
171
- | | |
172
- |---|---|
173
- | `:foreign_key` | Default. Render resource ids wherever the foreign key already holds the related id, and never run an extra query. |
174
- | `:always` | Render them for every `belongs_to`, loading the association when the foreign key cannot answer. A query per record, per relationship, on every render. |
175
- | `:never` | Render none. This is the 1.x payload. |
176
-
177
- Subclasses inherit it, and a relationship passing `resource_ids` explicitly still wins.
178
-
179
- All three describe requests that do not include the relationship. A relationship the request does include renders its ids whatever this is set to, `:never` included, because the records are already loaded and sitting in `included`.
180
-
181
- Before flipping the setting, [`bin/rake graphiti:audit`](/topics/debugging#graphiti-audit) reports how every relationship renders resource ids today and which would start loading the association.
182
-
183
- #### What a client sees {#relationship-payload-shapes}
184
-
185
- A client never has to work out which rule applied. The relationship object says what it knows:
186
-
187
- ```json
188
- "employee": { "data": { "type": "employees", "id": "1" } } // here is the id
189
- "employee": { "links": { "related": "..." } } // fetch it yourself
190
- "employee": { "meta": { "included": false } } // neither
191
- ```
192
-
193
- The last shape appears only when a relationship has no ids **and** no link, which usually means `link: false`. It is not a general "was this sideloaded" flag. Relationships are autolinked by default, so the link shape is the one you normally see.
194
-
195
- The setting covers `belongs_to` and `polymorphic_belongs_to`, and no collection, deliberately. An API-wide `:always` on collections would be the N+1 from [#167](https://github.com/graphiti-api/graphiti/issues/167#issuecomment-686866646) applied everywhere at once.
196
-
197
- `:always` renders ids by loading the association, so a relationship naming a method the model does not have raises on every render once you set it.
198
-
199
- ### Conditional Relationships {#conditional-relationships}
200
-
201
- Like attributes, the `readable` and `writable` flags on a relationship accept more than a boolean: pass a symbol, string, or proc and the relationship becomes conditional, evaluated per-request.
202
-
203
- ```ruby
204
- class EmployeeResource < ApplicationResource
205
- has_many :salary_histories, readable: :admin?, writable: :admin?
206
-
207
- def admin?
208
- context.current_user.admin?
209
- end
210
- end
211
- ```
212
-
213
- When a readable guard returns `false`, the relationship is omitted from the serialized output and any attempt to sideload it via `?include=` is silently scrubbed from the request. When a writable guard returns `false`, sideposting to that relationship is rejected with an `unwritable_relationship` validation error.
214
-
215
- Unlike attribute guards, relationship guards take no arguments. Include scrubbing happens before any records have been fetched, so there is no model to hand them. Base the decision on `context` alone.
216
-
217
- The guard can live on either side of the relationship. Graphiti first looks for the method on the resource declaring the relationship. If it isn't defined there but is defined on the related resource, the related resource's method is used. Defining the guard on the related resource lets a single guard cover every relationship pointing at it:
218
-
219
- ```ruby
220
- class SalaryHistoryResource < ApplicationResource
221
- # Any resource declaring a relationship to SalaryHistoryResource with
222
- # readable: :admin? will use this method, unless it defines its own.
223
- def admin?
224
- context.current_user.admin?
225
- end
226
- end
227
- ```
228
-
229
- > **Upgrading to 1.12:** relationship guards are new enforcement, not a new
230
- > option. Before 1.12, a symbol, string, or proc passed to a relationship's
231
- > `readable`/`writable` was accepted and silently treated as `true`. The guard
232
- > was never called. Those guards now run. If your app already passes one of
233
- > these, a relationship that has been serialized all along may start
234
- > disappearing from responses.
235
- >
236
- > To list every guarded relationship in your app before deploying, run
237
- > `bin/rails runner 'puts Graphiti.guarded_relationships'`.
238
- >
239
- > Apps using `schema.json` also get this for free: guarded relationships are
240
- > flagged in the schema, and the schema check reports them as
241
- > `became guarded`.
242
-
243
- ### Customizing Scope {#customizing-scope}
244
-
245
- Use `params` to change the query parameters that will be passed to the
246
- associated Resource:
247
-
248
- ```ruby
249
- has_many :active_positions, resource: PositionResource do
250
- params do |hash, employees|
251
- hash[:filter][:active] = true
252
- end
253
- end
254
-
255
- # Would cause the underlying query:
256
- #
257
- # PositionResource.all({
258
- # filter: {
259
- # employee_id: array_of_employee_ids
260
- # active: true
261
- # }
262
- # })
263
- ```
264
-
265
- If there is no existing AR association for this we would also need to make it a getter/setter on the model.
266
-
267
- ```ruby
268
- # app/models/position.rb
269
- attr_accessor :active_positions
270
- ```
271
-
272
- ### Customizing Assignment {#customizing-assignment}
273
-
274
- Once we've fetched primary data and its relationship (e.g. we have an
275
- `employees` array and `positions` array), we need to associate these
276
- objects:
277
-
278
- ```ruby
279
- employees.each do |e|
280
- e.positions = positions.select { |p| p.employee_id == e.id }
281
- end
282
- ```
283
-
284
- Occasionally this logic will be non-standard or more complex. Use
285
- `assign_each` to customize, returning all relevant children for the
286
- given parent:
287
-
288
- ```ruby
289
- has_many :positions do
290
- assign_each do |employee, positions|
291
- positions.select { |p| p.belongs_to?(employee) }
292
- end
293
- end
294
- ```
295
-
296
- Or if all else fails, use `#assign` to control all the logic:
297
-
298
- ```ruby
299
- has_many :positions do
300
- assign do |employees, positions|
301
- employees.each do |employee|
302
- positions.select { |p| p.belongs_to?(employee) }
303
- end
304
- end
305
- end
306
- ```
307
-
308
- **Note**: ActiveRecord will sometimes cause unexpected queries when
309
- assigning. If you're overriding `#assign`, make sure to keep an eye on this. If using `#assign_each`, you're fine because the adapter will take
310
- care of this for you.
311
-
312
- ## has_many {#has-many}
313
-
314
- ```ruby
315
- has_many :positions
316
- ```
317
-
318
- Defaults to these common options:
319
-
320
- ```ruby
321
- has_many :positions,
322
- foreign_key: :employee_id,
323
- primary_key: :id,
324
- resource_ids: false,
325
- resource: PositionResource
326
- ```
327
-
328
- Which would cause the following query when sideloading:
329
-
330
- ```ruby
331
- PositionResource.all({ filter: { employee_id => employee_ids } })
332
- ```
333
-
334
- This means **we need to make sure that filter is supported**:
335
-
336
- ```ruby
337
- class PositionResource < ApplicationResource
338
- attribute :employee_id, :integer, only: [:filterable]
339
- # ... code ...
340
- end
341
- ```
342
-
343
- Once we've resolved `employees` and `positions` the resulting objects
344
- would be associated with logic similar to:
345
-
346
- ```ruby
347
- employees.each do |e|
348
- e.positions = positions.select { |p| p.employee_id == e.id }
349
- end
350
- ```
351
-
352
- And generate a Link:
353
-
354
- `/positions?filter[employee_id]=1,2,3`
355
-
356
- ## belongs_to {#belongs-to}
357
-
358
- ```ruby
359
- belongs_to :employee
360
- ```
361
-
362
- Defaults to these common options:
363
-
364
- ```ruby
365
- belongs_to :employee,
366
- foreign_key: :employee_id,
367
- primary_key: :id,
368
- resource_ids: true,
369
- resource: EmployeeResource
370
- ```
371
-
372
- Which would cause the following query when sideloading:
373
-
374
- ```ruby
375
- EmployeeResource.all({ filter: { id => position_ids } })
376
- ```
377
-
378
- And assign the resulting objects with logic similar to:
379
-
380
- ```ruby
381
- positions.each do |p|
382
- p.employee = employees.find { |e| p.employee_id == e.id }
383
- end
384
- ```
385
-
386
- And generate a Link:
387
-
388
- `/employees?filter[id]=1,2,3`
389
-
390
- ## has_one {#has-one}
391
-
392
- `has_one` works exactly like `has_many`, but only one record will be
393
- returned. When sideloading this will be a single element, much like
394
- `belongs_to`.
395
-
396
- There is one small caveat: Links always point to an `index` action, so we can apply filters. That means following *`has_one` Link will lead to
397
- an array*, and you should select the first record.
398
-
399
- ### Faux has_one {#faux-has-one}
400
-
401
- A "Faux Has One" occurs when there is more than one record of
402
- associated data, but we only want to return the *first* record in that
403
- array. Consider this `ActiveRecord` relationship:
404
-
405
- ```ruby
406
- # app/models/employee.rb
407
- has_many :positions
408
- has_one :current_position, -> { where(created_at: :desc) }, class_name: 'Position'
409
-
410
- Employee.includes('current_position').to_a
411
-
412
- # SELECT * FROM employees
413
- # SELECT * FROM positions WHERE employee_id IN (?) ORDER BY created_at DESC
414
- ```
415
-
416
- When we eager load, *more than one Position is returned from the
417
- database query*. Assigning only the first record and dropping the rest
418
- occurs in ruby, not the database query.
419
-
420
- The same thing happens in Graphiti:
421
-
422
- ```ruby
423
- # app/resources/employee_resource.rb
424
- has_many :positions
425
- has_one :current_position, resource: PositionResource do
426
- params do |hash|
427
- hash[:sort] = '-created_at'
428
- end
429
- end
430
-
431
- EmployeeResource.all(include: 'current_position')
432
- # PositionResource.all({
433
- # filter: { employee_id: employee_ids },
434
- # sort: '-created_at'
435
- # })
436
- ```
437
-
438
- Though everything works as expected, a large number of Position records
439
- can incur a performance penalty (as we'd be instantiating a large number
440
- of ActiveRecord objects).
441
-
442
- For this reason, you are encouraged to model Faux Has One's in such a
443
- way that the underlying database query only returns the relevant single
444
- record. Imagine if we had a `historical_index` column on `positions`, where a value of `1` meant "most recent":
445
-
446
- ```ruby
447
- # app/models/employee.rb
448
- has_many :positions
449
- has_one :current_position, -> { where(historical_index: 1) }, class_name: 'Position'
450
-
451
- Employee.includes('current_position').to_a
452
-
453
- # SELECT * FROM employees
454
- # SELECT * FROM positions WHERE employee_id IN (?) AND historical_index = 1
455
- ```
456
-
457
- We've ensured the *query itself* only returns a single record.
458
- Optimizing a Graphiti API is the same as optimizing queries.
459
-
460
- ## many_to_many {#many-to-many}
461
-
462
- > This relationship is specific to relational databases that use a "join
463
- > table" between two tables.
464
-
465
- Though you can make this work for other ORMs/clients, it's easiest to
466
- explain by focusing on `ActiveRecord`.
467
-
468
- First, **you must use [has_many :through](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association) and not has_and_belongs_to_many**:
469
-
470
- ```ruby
471
- class Employee < ApplicationRecord
472
- has_many :team_memberships
473
- has_many :teams, through: :team_memberships
474
- end
475
-
476
- class TeamMembership < ApplicationRecord
477
- belongs_to :employee
478
- belongs_to :team
479
- end
480
-
481
- class Team < ApplicationRecord
482
- has_many :team_memberships
483
- has_many :employees, through: :team_memberships
484
- end
485
- ```
486
-
487
- You can always expose `team_memberships` to your API - particularly
488
- useful if that table holds metadata about the relationship.
489
-
490
- Other times, however, clients of the API should not have knowledge of
491
- this implementation detail. In these cases, use `many_to_many`:
492
-
493
- ```ruby
494
- class EmployeeResource < ApplicationResource
495
- many_to_many :teams
496
- end
497
- # Generates the Link
498
- # /teams?filter[employee_id]=1,2,3
499
-
500
- class TeamResource < ApplicationResource
501
- many_to_many :employees
502
- end
503
- # Generates the Link
504
- # /teams?filter[team_id]=1,2,3
505
- ```
506
-
507
- The `many_to_many` call will automatically add a Filter to the associated resource. The logic for that filter, in the case of `ActiveRecord`:
508
-
509
- ```ruby
510
- # app/resources/employee_resource.rb
511
-
512
- filter :team_id, :integer do
513
- eq do |scope, value|
514
- scope
515
- .includes(:team_memberships)
516
- .where(team_memberships: { team_id: value }
517
- end
518
- end
519
- ```
520
-
521
- To customize the foreign key, you will need to specify a hash rather
522
- than a symbol. The hash key is the relationship name, so the above is
523
- equivalent to
524
-
525
- ```ruby
526
- # app/resources/employee_resource.rb
527
-
528
- many_to_many :teams, foreign_key: { team_memberships: :team_id }
529
- ```
530
-
531
- If using ActiveRecord, and the API relationship name does not match your
532
- Model relationship name, use `:as` to specify the model relationship
533
- that should be used to derive the query:
534
-
535
- ```ruby
536
- # The API relationship is "teams", ActiveRecord has "groups"
537
- many_to_many :teams, as: :groups
538
- ```
539
-
540
- ## polymorphic_belongs_to {#polymorphic-belongs-to}
541
-
542
- With polymorphic associations, a Resource can belong to more than one other Resource, on a single association. Though these relationships are not specific to `ActiveRecord`, we'll use `ActiveRecord` conventions to describe the use case.
543
-
544
- Given the following [polymorphic ActiveRecords](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations):
545
-
546
- ```ruby
547
- class Note < ApplicationRecord
548
- belongs_to :notable, polymorphic: true
549
- end
550
-
551
- class Employee < ApplicationRecord
552
- has_many :notes, as: :notable
553
- end
554
-
555
- class Department < ApplicationRecord
556
- has_many :notes, as: :notable
557
- end
558
-
559
- class Team < ApplicationRecord
560
- has_many :notes, as: :notable
561
- end
562
- ```
563
-
564
- By `ActiveRecord` convention, the `notes` table would have columns `notable_id` and `notable_type`.
565
-
566
- Graphiti has the same concept. In this case we would group all the notes
567
- by a given `notable_type`, and follow a different `belongs_to`
568
- association for each group:
569
-
570
- ```ruby
571
- # app/resources/note_resource.rb
572
- polymorphic_belongs_to :notable do
573
- group_by(:notable_type) do
574
- on(:Employee)
575
- on(:Department)
576
- on(:Team)
577
- end
578
- end
579
- ```
580
-
581
- The `on` DSL is shorthand for a `belongs_to` relationship that accepts
582
- all the usual options and customizations:
583
-
584
- ```ruby
585
- on(:Employee).belongs_to :employee,
586
- resource: EmployeeResource
587
- # ... etc ...
588
- ```
589
-
590
- In other words: group all Notes by `notable_type`, and for all that have the value of `"Employee"` use the `belongs_to :employee` relationship
591
- for further querying.
592
-
593
- ## polymorphic_has_many {#polymorphic-has-many}
594
-
595
- Continuing from the prior section, the corresponding association of a
596
- `polymorphic_belongs_to` is a `polymorphic_has_many`:
597
-
598
- ```ruby
599
- class EmployeeResource < ApplicationResource
600
- polymorphic_has_many :notes, as: :notable
601
- end
602
- ```
603
-
604
- Predictably, this causes the query:
605
-
606
- ```ruby
607
- NoteResource.all({
608
- filter: {
609
- notable_type: 'Employee',
610
- notable_id: employee_ids
611
- }
612
- })
613
- ```
614
-
615
- And the Link
616
-
617
- `/notes?filter[notable_id]=1,2,3&filter[notable_type]=Employee`
618
-
619
- Which means the following filters are required:
620
-
621
- ```ruby
622
- class NoteResource < ApplicationResource
623
- attribute :notable_id, :integer, only: [:filterable]
624
- attribute :notable_type, :string, only: [:filterable]
625
- # ... code ...
626
- end
627
- ```