graphiti 2.0.0.beta.2 → 2.0.0.beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -86
  3. data/.github/workflows/docs.yml +60 -0
  4. data/.github/workflows/release.yml +8 -8
  5. data/.gitignore +7 -0
  6. data/.npmrc +9 -0
  7. data/.standard.yml +4 -1
  8. data/Appraisals +33 -32
  9. data/CHANGELOG.md +41 -0
  10. data/README.md +13 -2
  11. data/UPGRADING.md +2 -68
  12. data/docs/concepts/backends-and-models.md +122 -0
  13. data/docs/concepts/endpoints.md +183 -0
  14. data/docs/concepts/links.md +212 -0
  15. data/docs/concepts/overview.md +80 -0
  16. data/docs/concepts/persisting.md +376 -0
  17. data/docs/concepts/relationships.md +527 -0
  18. data/docs/concepts/resources.md +677 -0
  19. data/docs/getting-started/first-api.md +289 -0
  20. data/docs/getting-started/installation.md +185 -0
  21. data/docs/intro.md +307 -0
  22. data/docs/js/authentication.md +63 -0
  23. data/docs/js/ddau.md +20 -0
  24. data/docs/js/extra-params.md +41 -0
  25. data/docs/js/index.md +112 -0
  26. data/docs/js/installation.md +120 -0
  27. data/docs/js/middleware.md +72 -0
  28. data/docs/js/models.md +202 -0
  29. data/docs/js/reads.md +494 -0
  30. data/docs/js/state-syncing.md +100 -0
  31. data/docs/js/writes.md +373 -0
  32. data/docs/reference/vandal.md +63 -0
  33. data/docs/reference/why.md +13 -0
  34. data/docs/topics/authorization.md +155 -0
  35. data/docs/topics/caching.md +55 -0
  36. data/docs/topics/customizing-sideloads.md +156 -0
  37. data/docs/topics/debugging.md +216 -0
  38. data/docs/topics/error-handling.md +210 -0
  39. data/docs/topics/etags.md +46 -0
  40. data/docs/topics/hopping-relationships.md +149 -0
  41. data/docs/topics/json-attributes.md +77 -0
  42. data/docs/topics/openstruct-models.md +50 -0
  43. data/docs/topics/remote-resources.md +291 -0
  44. data/docs/topics/testing.md +894 -0
  45. data/docs/topics/without-activerecord.md +324 -0
  46. data/docs/tutorial/index.md +58 -0
  47. data/docs/tutorial/step_0.md +107 -0
  48. data/docs/tutorial/step_1.md +199 -0
  49. data/docs/tutorial/step_2.md +312 -0
  50. data/docs/tutorial/step_3.md +142 -0
  51. data/docs/tutorial/step_4.md +135 -0
  52. data/docs/tutorial/step_5.md +69 -0
  53. data/docs/tutorial/step_6.md +82 -0
  54. data/docs/tutorial/step_7.md +205 -0
  55. data/docs/tutorial/step_8.md +128 -0
  56. data/docs/tutorial/step_9.md +171 -0
  57. data/docs/upgrading.md +265 -0
  58. data/gemfiles/rails_7_1.gemfile +4 -3
  59. data/gemfiles/{rails_7_2_graphiti_rails.gemfile → rails_7_2.gemfile} +3 -3
  60. data/gemfiles/{rails_8_1_graphiti_rails.gemfile → rails_8_0.gemfile} +3 -3
  61. data/gemfiles/{rails_8_0_graphiti_rails.gemfile → rails_8_1.gemfile} +3 -3
  62. data/graphiti.gemspec +7 -5
  63. data/{deprecated_generators → lib/generators}/graphiti/api_test_generator.rb +7 -1
  64. data/{deprecated_generators → lib/generators}/graphiti/generator_mixin.rb +14 -1
  65. data/{deprecated_generators → lib/generators}/graphiti/install_generator.rb +19 -13
  66. data/{deprecated_generators → lib/generators}/graphiti/resource_generator.rb +43 -6
  67. data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
  68. data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
  69. data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
  70. data/lib/graphiti/configuration.rb +2 -2
  71. data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
  72. data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
  73. data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
  74. data/lib/graphiti/error_serializers/validation.rb +143 -0
  75. data/lib/graphiti/errors.rb +4 -23
  76. data/lib/graphiti/query.rb +1 -1
  77. data/lib/graphiti/rails/context.rb +33 -0
  78. data/lib/graphiti/rails/controller.rb +41 -0
  79. data/lib/graphiti/rails/debugging.rb +18 -0
  80. data/lib/graphiti/rails/exception_handlers.rb +77 -0
  81. data/lib/graphiti/rails/railtie.rb +139 -0
  82. data/lib/graphiti/rails/responders.rb +21 -0
  83. data/lib/graphiti/rails/test_helpers.rb +22 -0
  84. data/lib/graphiti/rails.rb +47 -29
  85. data/lib/graphiti/resource/configuration.rb +1 -0
  86. data/lib/graphiti/resource/interface.rb +2 -2
  87. data/lib/graphiti/resource/persistence.rb +14 -2
  88. data/lib/graphiti/resource/remote.rb +2 -2
  89. data/lib/graphiti/resource/sideloading.rb +1 -1
  90. data/lib/graphiti/resource.rb +13 -1
  91. data/lib/graphiti/responders.rb +7 -20
  92. data/lib/graphiti/schema.rb +5 -1
  93. data/lib/graphiti/schema_diff.rb +4 -0
  94. data/lib/graphiti/scope.rb +45 -37
  95. data/lib/graphiti/serializer.rb +6 -0
  96. data/lib/graphiti/sideload/belongs_to.rb +38 -5
  97. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
  98. data/lib/graphiti/sideload.rb +54 -35
  99. data/lib/graphiti/spec_helpers/errors.rb +73 -0
  100. data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
  101. data/lib/graphiti/spec_helpers/helpers.rb +107 -0
  102. data/lib/graphiti/spec_helpers/node.rb +88 -0
  103. data/lib/graphiti/spec_helpers/rspec.rb +147 -0
  104. data/lib/graphiti/spec_helpers.rb +53 -0
  105. data/lib/graphiti/util/include_params.rb +2 -2
  106. data/lib/graphiti/util/persistence.rb +10 -11
  107. data/lib/graphiti/util/serializer_relationships.rb +41 -5
  108. data/lib/graphiti/version.rb +1 -1
  109. data/lib/graphiti-rails.rb +11 -0
  110. data/lib/graphiti.rb +34 -10
  111. data/lib/graphiti_errors.rb +11 -0
  112. data/lib/graphiti_spec_helpers/rspec.rb +3 -0
  113. data/lib/graphiti_spec_helpers.rb +11 -0
  114. data/lib/{graphiti/deprecated_tasks.rb → tasks/graphiti.rake} +6 -1
  115. data/package-lock.json +6199 -0
  116. data/package.json +5 -4
  117. data/website/.gitignore +20 -0
  118. data/website/README.md +43 -0
  119. data/website/docusaurus.config.js +141 -0
  120. data/website/package-lock.json +19474 -0
  121. data/website/package.json +46 -0
  122. data/website/sidebars.js +82 -0
  123. data/website/src/css/custom.css +58 -0
  124. data/website/src/pages/markdown-page.mdx +7 -0
  125. data/website/static/.nojekyll +0 -0
  126. data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
  127. data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
  128. data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
  129. data/website/static/1.13/2019/10/14/tutorial.html +198 -0
  130. data/website/static/1.13/CNAME +1 -0
  131. data/website/static/1.13/README.md +16 -0
  132. data/website/static/1.13/assets/css/syntax.css +60 -0
  133. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  134. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  135. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  136. data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
  137. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  138. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  139. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  140. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  141. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
  142. data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
  143. data/website/static/1.13/assets/img/backend.gif +0 -0
  144. data/website/static/1.13/assets/img/conformity.png +0 -0
  145. data/website/static/1.13/assets/img/error_payload.png +0 -0
  146. data/website/static/1.13/assets/img/gh.png +0 -0
  147. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  148. data/website/static/1.13/assets/img/logo-500.png +0 -0
  149. data/website/static/1.13/assets/img/logo.png +0 -0
  150. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  151. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  152. data/website/static/1.13/assets/img/persist.jpg +0 -0
  153. data/website/static/1.13/assets/img/resource.gif +0 -0
  154. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  155. data/website/static/1.13/assets/img/rest1.gif +0 -0
  156. data/website/static/1.13/assets/img/rest2.gif +0 -0
  157. data/website/static/1.13/assets/img/rest3.gif +0 -0
  158. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  159. data/website/static/1.13/assets/img/why.png +0 -0
  160. data/website/static/1.13/assets/js/highlight.pack.js +2 -0
  161. data/website/static/1.13/assets/main.css +15518 -0
  162. data/website/static/1.13/assets/main.css.map +1 -0
  163. data/website/static/1.13/bin/bundle +109 -0
  164. data/website/static/1.13/bin/jekyll +27 -0
  165. data/website/static/1.13/bin/kramdown +27 -0
  166. data/website/static/1.13/bin/listen +27 -0
  167. data/website/static/1.13/bin/rake +27 -0
  168. data/website/static/1.13/bin/rougify +27 -0
  169. data/website/static/1.13/bin/safe_yaml +27 -0
  170. data/website/static/1.13/bin/sass +27 -0
  171. data/website/static/1.13/bin/sass-convert +27 -0
  172. data/website/static/1.13/bin/scss +27 -0
  173. data/website/static/1.13/blog.html +259 -0
  174. data/website/static/1.13/cheatsheet.html +316 -0
  175. data/website/static/1.13/cookbooks/authorization.md +0 -0
  176. data/website/static/1.13/cookbooks/caching.md +0 -0
  177. data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
  178. data/website/static/1.13/cookbooks/etags.md +0 -0
  179. data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
  180. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  181. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  182. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  183. data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
  184. data/website/static/1.13/features.html +249 -0
  185. data/website/static/1.13/feed.xml +106 -0
  186. data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
  187. data/website/static/1.13/guides/concepts/debugging.html +440 -0
  188. data/website/static/1.13/guides/concepts/endpoints.html +432 -0
  189. data/website/static/1.13/guides/concepts/error-handling.html +396 -0
  190. data/website/static/1.13/guides/concepts/links.html +501 -0
  191. data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
  192. data/website/static/1.13/guides/concepts/resources.html +2176 -0
  193. data/website/static/1.13/guides/concepts/testing.html +1469 -0
  194. data/website/static/1.13/guides/getting-started/installation.html +420 -0
  195. data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
  196. data/website/static/1.13/guides/index.html +269 -0
  197. data/website/static/1.13/guides/overview.html +325 -0
  198. data/website/static/1.13/guides/upgrading-2-0.html +193 -0
  199. data/website/static/1.13/guides/upgrading.html +314 -0
  200. data/website/static/1.13/guides/vandal.html +282 -0
  201. data/website/static/1.13/guides/why.html +1121 -0
  202. data/website/static/1.13/index.html +72 -0
  203. data/website/static/1.13/js/authentication.html +295 -0
  204. data/website/static/1.13/js/ddau.html +238 -0
  205. data/website/static/1.13/js/extra-params.html +270 -0
  206. data/website/static/1.13/js/index.html +321 -0
  207. data/website/static/1.13/js/installation.html +637 -0
  208. data/website/static/1.13/js/introduction.html +257 -0
  209. data/website/static/1.13/js/middleware.html +318 -0
  210. data/website/static/1.13/js/reads/fieldsets.html +271 -0
  211. data/website/static/1.13/js/reads/filtering.html +289 -0
  212. data/website/static/1.13/js/reads/includes.html +260 -0
  213. data/website/static/1.13/js/reads/index.html +497 -0
  214. data/website/static/1.13/js/reads/nested-queries.html +353 -0
  215. data/website/static/1.13/js/reads/pagination.html +260 -0
  216. data/website/static/1.13/js/reads/sorting.html +265 -0
  217. data/website/static/1.13/js/reads/statistics.html +289 -0
  218. data/website/static/1.13/js/state-syncing.html +340 -0
  219. data/website/static/1.13/js/writes/deferred.html +296 -0
  220. data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
  221. data/website/static/1.13/js/writes/index.html +391 -0
  222. data/website/static/1.13/js/writes/nested.html +330 -0
  223. data/website/static/1.13/js/writes/validations.html +272 -0
  224. data/website/static/1.13/quickstart.html +660 -0
  225. data/website/static/1.13/template +161 -0
  226. data/website/static/1.13/tutorial/index.html +250 -0
  227. data/website/static/1.13/tutorial/step_0.html +292 -0
  228. data/website/static/1.13/tutorial/step_1.html +517 -0
  229. data/website/static/1.13/tutorial/step_2.html +481 -0
  230. data/website/static/1.13/tutorial/step_3.html +323 -0
  231. data/website/static/1.13/tutorial/step_4.html +318 -0
  232. data/website/static/1.13/tutorial/step_5.html +265 -0
  233. data/website/static/1.13/tutorial/step_6.html +276 -0
  234. data/website/static/1.13/tutorial/step_7.html +390 -0
  235. data/website/static/1.13/tutorial/step_8.html +316 -0
  236. data/website/static/1.13/tutorial/step_9.html +365 -0
  237. data/website/static/assets/img/error_payload.png +0 -0
  238. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  239. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  240. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  241. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  242. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  243. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  244. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  245. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  246. data/website/static/assets/img/meta_total_count.png +0 -0
  247. data/website/static/img/docusaurus-social-card.jpg +0 -0
  248. data/website/static/img/docusaurus.png +0 -0
  249. data/website/static/img/favicon.ico +0 -0
  250. data/website/static/img/logo.png +0 -0
  251. data/website/static/img/logo.svg +1 -0
  252. data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
  253. data/website/static/img/undraw_docusaurus_react.svg +170 -0
  254. data/website/static/img/undraw_docusaurus_tree.svg +40 -0
  255. metadata +245 -46
  256. data/gemfiles/rails_6.gemfile +0 -18
  257. data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
  258. data/gemfiles/rails_7.gemfile +0 -18
  259. data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
  260. data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
  261. data/lib/graphiti/railtie.rb +0 -121
  262. /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
  263. /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
  264. /data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +0 -0
  265. /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
  266. /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
  267. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
  268. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
  269. /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: 'Step 6'
3
+ ---
4
+
5
+ ### Step 6: Customizing Writes
6
+
7
+ > [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_5_has_one...step_6_write_customization)
8
+
9
+ When we ran the generators (and created a blank Resource class), we got the ability to create, update, and destroy resources for free. You can turn off this behavior with `self.read_only = true`. Or for relationships: `has_many :positions, writable: false`.
10
+
11
+ But in RESTful APIs, it's super common for persistence operations to
12
+ have side effects - that's how we avoid extraneous verbs and
13
+ inconsistent patterns.
14
+
15
+ In a prior step, we updated `position` Factory to automatically reorder the `historical_index`: when a new record comes in, all the prior
16
+ values need to change. This step will show how to add that behavior to
17
+ our API, using hooks that work for a variety of scenarios: sending
18
+ emails, checking authorization roles, queuing delayed jobs, and more.
19
+
20
+ ### The Rails Stuff 🚂
21
+
22
+ Previously, we put the logic that re-ordered the `historical_index` column in the `position` factory. Let's move that to the model so our
23
+ tests and API can share the same logic:
24
+
25
+ ```ruby
26
+ # app/models/position.rb
27
+ def self.reorder!(employee_id)
28
+ scope = Position.where(employee_id: employee_id).order(created_at: :desc)
29
+ scope.each_with_index do |p, index|
30
+ p.update_attribute(:historical_index, index + 1)
31
+ end
32
+ end
33
+ ```
34
+
35
+ ```ruby
36
+ # spec/factories/positions.rb
37
+ # ... code ...
38
+ after(:create) do |position|
39
+ unless position.historical_index
40
+ Position.reorder!(position.employee.id)
41
+ end
42
+ end
43
+ ```
44
+
45
+ ### The Graphiti Stuff 🎨
46
+
47
+ All Graphiti updates happen within a transaction. We want to insert our
48
+ code right before that transaction closes - after the graph of objects
49
+ has been persisted and validations have passed. To do that, we'll use
50
+ the `before_commit` hook:
51
+
52
+ ```ruby
53
+ before_commit only: [:create, :destroy] do |position|
54
+ Position.reorder!(position.employee_id)
55
+ end
56
+ ```
57
+
58
+ Again, the `Position.reorder!` code existed independent of our
59
+ API, and was re-used in our factory.
60
+
61
+ #### Digging Deeper 🧐
62
+
63
+ Resources come with [Lifecycle Hooks](https://www.graphiti.dev/guides/concepts/persisting#persistence-lifecycle-hooks), similar to ActiveRecord [Callbacks](https://guides.rubyonrails.org/active_record_callbacks.html).
64
+
65
+ Those callbacks have gotten a bad reputation. This is because your Model
66
+ can be - is supposed to be - used in a variety of contexts across your
67
+ application. Some of those contexts will want a given callback to fire,
68
+ others will not, and accomodating the conditionals gets hairy. This is
69
+ why many developers move that functionality into [Service Objects](https://engineering.gusto.com/the-rails-callbacks-best-practices-used-at-gusto/).
70
+
71
+ But Resource callbacks don't have the same problem - they only fire in
72
+ the context of your API, and can be associated to a single endpoint. You
73
+ can still use Service Objects if you'd like. Graphiti callbacks wire them up.
74
+
75
+
76
+ <h2 id="next">
77
+ <a href="/tutorial/step_7">
78
+ NEXT -
79
+ <small>Step 7: Many to Many</small>
80
+ &raquo;
81
+ </a>
82
+ </h2>
@@ -0,0 +1,205 @@
1
+ ---
2
+ title: 'Step 7'
3
+ ---
4
+
5
+ ### Step 7: Many to Many
6
+
7
+ > [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_6_write_customization...step_7_many_to_many)
8
+
9
+ Let's add a `Team` relationship: a `Team` can have many `Employee`s, an `Employee` can have many `Team`s. Let's also say a `Team` belongs to a `Department`.
10
+
11
+ <table class="table table-small text-center">
12
+ <thead>
13
+ <tr>
14
+ <th class="text-center">id</th>
15
+ <th class="text-center">department_id</th>
16
+ <th class="text-center">name</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <tr>
21
+ <td>1</td>
22
+ <td>1</td>
23
+ <td>The A Team</td>
24
+ </tr>
25
+ <tr>
26
+ <td>2</td>
27
+ <td>1</td>
28
+ <td>The B Team</td>
29
+ </tr>
30
+ <tr>
31
+ <td>3</td>
32
+ <td>2</td>
33
+ <td>The C Team</td>
34
+ </tr>
35
+ </tbody>
36
+ </table>
37
+
38
+ To satisfy this many-to-many use case, we'll need a join model,
39
+ `TeamMembership`:
40
+
41
+ <table class="table table-small text-center">
42
+ <thead>
43
+ <tr>
44
+ <th class="text-center">id</th>
45
+ <th class="text-center">team_id</th>
46
+ <th class="text-center">employee_id</th>
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ <tr>
51
+ <td>1</td>
52
+ <td>1</td>
53
+ <td>1</td>
54
+ </tr>
55
+ <tr>
56
+ <td>2</td>
57
+ <td>2</td>
58
+ <td>1</td>
59
+ </tr>
60
+ <tr>
61
+ <td>3</td>
62
+ <td>3</td>
63
+ <td>2</td>
64
+ </tr>
65
+ </tbody>
66
+ </table>
67
+
68
+ ### The Rails Stuff 🚂
69
+
70
+ ```bash
71
+ $ bin/rails g model Team name:string department:belongs_to
72
+ $ bin/rails g model TeamMembership employee:belongs_to team:belongs_to
73
+ $ bin/rails db:migrate
74
+ ```
75
+
76
+ Graphiti supports `has_many :through`:
77
+
78
+ ```ruby
79
+ # app/models/employee.rb
80
+ has_many :team_memberships
81
+ has_many :teams, through: :team_memberships
82
+ ```
83
+
84
+ ```ruby
85
+ # app/models/department.rb
86
+ has_many :teams
87
+ ```
88
+
89
+ ```ruby
90
+ class Team < ApplicationRecord
91
+ belongs_to :department
92
+ has_many :team_memberships
93
+ has_many :employees, through: :team_memberships
94
+ end
95
+ ```
96
+
97
+ ```ruby
98
+ class TeamMembership < ApplicationRecord
99
+ belongs_to :team
100
+ belongs_to :employee
101
+ end
102
+ ```
103
+
104
+ Finally, we'll need a new seed file to handle these new associations:
105
+
106
+ ```ruby
107
+ [
108
+ Employee,
109
+ Position,
110
+ Department,
111
+ TeamMembership,
112
+ Team
113
+ ].each(&:delete_all)
114
+
115
+ departments = []
116
+ def create_department(name)
117
+ dept = Department.create! name: name
118
+ dept.teams.create!(name: 'Engineering Team B')
119
+ dept.teams.create!(name: 'Engineering Team C')
120
+ dept
121
+ end
122
+
123
+ departments << create_department('Engineering')
124
+ departments << create_department('Safety')
125
+ departments << create_department('QA')
126
+
127
+ 100.times do
128
+ employee = Employee.create! first_name: Faker::Name.first_name,
129
+ last_name: Faker::Name.last_name,
130
+ age: rand(20..80)
131
+
132
+ (1..2).each do |i|
133
+ employee.positions.create! title: Faker::Job.title,
134
+ historical_index: i,
135
+ active: i == 1,
136
+ department: departments.sample
137
+ end
138
+
139
+ employee.teams << employee.positions[0].department.teams.sample
140
+ end
141
+ ```
142
+
143
+ ### The Graphiti Stuff 🎨
144
+
145
+ ```bash
146
+ $ bin/rails g graphiti:resource Team name:string
147
+ ```
148
+
149
+ Let's flesh out our `TeamResource`:
150
+
151
+ ```ruby
152
+ # app/resources/team_resource.rb
153
+ class TeamResource < ApplicationResource
154
+ attribute :department_id, :integer, only: [:filterable]
155
+ attribute :name, :string
156
+
157
+ belongs_to :department
158
+ many_to_many :employees
159
+ end
160
+ ```
161
+
162
+ The trick here is the `many_to_many` relationship. Let's add the reverse
163
+ as well:
164
+
165
+ ```ruby
166
+ # app/resources/employee_resource.rb
167
+ many_to_many :teams
168
+ ```
169
+
170
+ And for good measure:
171
+
172
+ ```ruby
173
+ # app/resources/department_resource.rb
174
+ has_many :teams
175
+ ```
176
+
177
+ We can now get all the usual functionality: fetch Employees and their
178
+ Teams in a single request (or vice versa).
179
+
180
+ #### Digging Deeper 🧐
181
+
182
+ The `many_to_many` relationship is the only one where Graphiti modifies a separate Resource "under the hood". When we said `many_to_many
183
+ :employees`, the `EmployeeResource` got a `team_id` filter, and `many_to_many :teams` created an `employee_id` filter on `TeamResource`.
184
+
185
+ This is because the logic is more complex than the default use case. We
186
+ don't have a simple `WHERE` clause. We need to join tables and look at
187
+ the appropriate primary/foreign keys. If the name of your API
188
+ association doesn't match the name of your ActiveRecord association, try
189
+ `has_many :things, as: :my_activerecord_relationship` to make the
190
+ introspection work correctly - or, write your own filter.
191
+
192
+ Sometimes you'll have multiple levels of `has_many :through`. In this case, a simple `many_to_many` isn't enough - check out the [Hopping
193
+ Relationships](/topics/hopping-relationships) recipe.
194
+
195
+ Think hard before reaching for `many_to_many`. Imagine one Team is the "primary" Team for an Employee. We'd add a `primary` boolean column to the `team_memberships` table...but that table isn't exposed to the API!
196
+ Consider if there's a hidden domain concept there.
197
+
198
+
199
+ <h2 id="next">
200
+ <a href="/tutorial/step_8">
201
+ NEXT -
202
+ <small>Step 8: Polymorphic Relationships</small>
203
+ &raquo;
204
+ </a>
205
+ </h2>
@@ -0,0 +1,128 @@
1
+ ---
2
+ title: 'Step 8'
3
+ ---
4
+
5
+ ### Step 8: Polymorphic Relationships
6
+
7
+ > [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_7_many_to_many...step_8_polymorphic_belongs_to)
8
+
9
+ Let's introduce the concept of a Note. A Note can belong to a
10
+ Department, an Employee, or a Team. For this, we'll need to introduce
11
+ the concept of [polymorphism](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations).
12
+
13
+ <table class="table table-small text-center">
14
+ <thead>
15
+ <tr>
16
+ <th class="text-center">id</th>
17
+ <th class="text-center">notable_id</th>
18
+ <th class="text-center">notable_type</th>
19
+ <th class="text-center">body</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <tr>
24
+ <td>1</td>
25
+ <td>1</td>
26
+ <td>Employee</td>
27
+ <td>A Sample Note!</td>
28
+ </tr>
29
+ <tr>
30
+ <td>2</td>
31
+ <td>1</td>
32
+ <td>Department</td>
33
+ <td>Another Sample Note!</td>
34
+ </tr>
35
+ <tr>
36
+ <td>3</td>
37
+ <td>1</td>
38
+ <td>Team</td>
39
+ <td>A Third Sample Note!</td>
40
+ </tr>
41
+ </tbody>
42
+ </table>
43
+
44
+ ### The Rails Stuff 🚂
45
+
46
+ ```bash
47
+ $ rails generate model Note notable:references{polymorphic}:index
48
+ $ bin/rails db:migrate
49
+ ```
50
+
51
+ Make sure to add the corresponding model relationships:
52
+
53
+ ```ruby
54
+ # app/models/employee.rb
55
+ has_many :notes, as: :notable
56
+ # app/models/team.rb
57
+ has_many :notes, as: :notable
58
+ # app/models/department.rb
59
+ has_many :notes, as: :notable
60
+
61
+ # app/models/note.rb
62
+ belongs_to :notable, polymorphic: true
63
+ ```
64
+
65
+ Finally, make sure to edit your seed file - check out the [diff](https://github.com/graphiti-api/employee_directory/compare/step_7_many_to_many...step_8_polymorphic_belongs_to) to see the necessary adjustments.
66
+
67
+ ### The Graphiti Stuff 🎨
68
+
69
+ ```bash
70
+ $ bin/rails g graphiti:resource Note body:string
71
+ ```
72
+
73
+ Let's create our `NoteResource`:
74
+
75
+ ```ruby
76
+ class NoteResource < ApplicationResource
77
+ attribute :body, :string
78
+
79
+ filter :notable_id, :integer
80
+ filter :notable_type, :string, allow: %w(Employee Department Team)
81
+
82
+ polymorphic_belongs_to :notable do
83
+ group_by(:notable_type) do
84
+ on(:Employee)
85
+ on(:Team)
86
+ on(:Department)
87
+ end
88
+ end
89
+ end
90
+ ```
91
+
92
+ And corresponding associations:
93
+
94
+ ```ruby
95
+ # app/resources/employee_resource.rb
96
+ polymorphic_has_many :notes, as: :notable
97
+ # app/resources/team_resource.rb
98
+ polymorphic_has_many :notes, as: :notable
99
+ # app/resources/department_resource.rb
100
+ polymorphic_has_many :notes, as: :notable
101
+ ```
102
+
103
+ #### Digging Deeper 🧐
104
+
105
+ When defining a polymorphic relationship for our API, we're saying "grab
106
+ all the parent records, group them by a `type` column, and execute different queries for each type". This way records with `notable_type ==
107
+ 'Employee'` can hit the `employees` table, but records with `notable_type == 'Department'` could in theory load from a different API
108
+ altogether.
109
+
110
+ Each of the `on` lines defines a new `belongs_to` association. That
111
+ means you can customize just like always:
112
+
113
+ ```ruby
114
+ on(:Team).belongs_to :team, resource: SomeCustomTeamResource do
115
+ # assign {}
116
+ # link {}
117
+ # ... etc ...
118
+ end
119
+ ```
120
+
121
+
122
+ <h2 id="next">
123
+ <a href="/tutorial/step_9">
124
+ NEXT -
125
+ <small>Step 9: Polymorphic Resources</small>
126
+ &raquo;
127
+ </a>
128
+ </h2>
@@ -0,0 +1,171 @@
1
+ ---
2
+ title: 'Step 9'
3
+ ---
4
+
5
+ ### Step 9: Polymorphic Resources
6
+
7
+ > [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_8_polymorphic_belongs_to...step_9_polymorphic_resource)
8
+
9
+ In the last step, we covered polymorphic relationships: a single
10
+ relationship can point to many different Resources. Polymorphic
11
+ Resources are the same concept, without an association: a single
12
+ Resource can resolve to many different sub-Resources. It's a very similar
13
+ to [Single-Table Inheritance in ActiveRecord](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html).
14
+
15
+ To illustrate this, we'll add a `tasks` table and corresponding `Task` superclass. Each record in this table will resolve to one of `Bug`, `Epic`, or `Feature`.
16
+
17
+ <table class="table table-small text-center">
18
+ <thead>
19
+ <tr>
20
+ <th class="text-center">id</th>
21
+ <th class="text-center">milestone_id</th>
22
+ <th class="text-center">type</th>
23
+ <th class="text-center">title</th>
24
+ </tr>
25
+ </thead>
26
+ <tbody>
27
+ <tr>
28
+ <td>1</td>
29
+ <td>null</td>
30
+ <td>Bug</td>
31
+ <td>Incorrect Value!</td>
32
+ </tr>
33
+ <tr>
34
+ <td>2</td>
35
+ <td>null</td>
36
+ <td>Feature</td>
37
+ <td>Build great stuff!</td>
38
+ </tr>
39
+ <tr>
40
+ <td>3</td>
41
+ <td>1</td>
42
+ <td>Epic</td>
43
+ <td>Build TONS of great stuff!</td>
44
+ </tr>
45
+ </tbody>
46
+ </table>
47
+
48
+ Why not just stick with a single `Task` model? Because each of these types has specific behavior: only `Feature`s have a `points` attribute, and only `Epic`s have a `milestones` relationship.
49
+
50
+ ### The Rails Stuff 🚂
51
+
52
+ Let's create our `Task` model:
53
+
54
+ ```bash
55
+ $ bin/rails g model Task employee:belongs_to team:belongs_to type:string
56
+ title:string
57
+ $ bin/rails db:migrate
58
+ ```
59
+
60
+ And create models to reflect our STI logic:
61
+
62
+ ```ruby
63
+ # app/models/task.rb
64
+ class Task < ApplicationRecord
65
+ TYPES = %w(Bug Feature Epic)
66
+
67
+ belongs_to :team, optional: true
68
+ belongs_to :employee, optional: true
69
+ end
70
+
71
+ # app/models/bug.rb
72
+ class Bug < Task
73
+ end
74
+
75
+ # app/models/feature.rb
76
+ class Feature < Task
77
+ end
78
+
79
+ # Only Epics have Milestones
80
+ # app/models/epic.rb
81
+ class Epic < Task
82
+ has_many :milestones
83
+ end
84
+
85
+ # app/models/milestone.rb
86
+ class Milestone < ApplicationRecord
87
+ belongs_to :epic
88
+ end
89
+ ```
90
+
91
+ Add the association:
92
+
93
+ ```ruby
94
+ # app/models/team.rb
95
+ has_many :tasks
96
+ has_many :bugs
97
+ has_many :features
98
+ has_many :epics
99
+
100
+ # app/models/employee.rb
101
+ has_many :tasks
102
+ has_many :bugs
103
+ has_many :features
104
+ has_many :epics
105
+ ```
106
+
107
+ Finally [view the diff](https://github.com/graphiti-api/employee_directory/compare/step_8_polymorphic_belongs_to...step_9_polymorphic_resource) to edit your `seeds.rb` file.
108
+
109
+ ### The Graphiti Stuff 🎨
110
+
111
+ Start by creating our Resource as normal:
112
+
113
+ ```bash
114
+ $ bin/rails g graphiti:resource Task title:string
115
+ ```
116
+
117
+ Now edit to support polymorphism and associations:
118
+
119
+ ```ruby
120
+ class TaskResource < ApplicationResource
121
+ self.polymorphic = %w(FeatureResource BugResource EpicResource)
122
+
123
+ attribute :employee_id, :integer, only: [:filterable]
124
+ attribute :team_id, :integer, only: [:filterable]
125
+ attribute :title, :string
126
+
127
+ belongs_to :employee
128
+ belongs_to :team
129
+ end
130
+ ```
131
+
132
+ The point of this was to show how responses could be specific to type,
133
+ so let's customize `Features`:
134
+
135
+ ```ruby
136
+ class FeatureResource < TaskResource
137
+ attribute :points, :integer do
138
+ rand(20)
139
+ end
140
+ end
141
+ ```
142
+
143
+
144
+ Only Epics have milestones, but let's support those as well:
145
+
146
+ ```bash
147
+ $ bin/rails g graphiti:resource Milestone name:string
148
+ ```
149
+
150
+ ```ruby
151
+ class MilestoneResource < ApplicationResource
152
+ attribute :epic_id, :integer, only: [:filterable]
153
+ attribute :name, :string
154
+
155
+ # Customize the link to the Tasks endpoint, as we
156
+ # didn't create an Epics endpoint
157
+ belongs_to :epic do
158
+ link do |milestone|
159
+ helpers = Rails.application.routes.url_helpers
160
+ helpers.task_url(milestone.epic_id)
161
+ end
162
+ end
163
+ end
164
+ ```
165
+
166
+ #### Digging Deeper 🧐
167
+
168
+ We can now resolve `Tasks`, either as a relationship or through the `/tasks` endpoint directly. When `Task` is type `'Feature'` it will have an extra attribute of `points`. When it's an `Epic`, it will have an additional relationship `Milestone`.
169
+
170
+ Graphiti is smart enough to fetch the appropriate relationships. A hit
171
+ to `/tasks?include=milestones` will only query for milestones when the resulting `Task` records are `Epic`s.