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