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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +30 -86
- data/.github/workflows/docs.yml +60 -0
- data/.github/workflows/release.yml +8 -8
- data/.gitignore +7 -0
- data/.npmrc +9 -0
- data/.standard.yml +4 -1
- data/Appraisals +33 -32
- data/CHANGELOG.md +41 -0
- data/README.md +13 -2
- data/UPGRADING.md +2 -68
- data/docs/concepts/backends-and-models.md +122 -0
- data/docs/concepts/endpoints.md +183 -0
- data/docs/concepts/links.md +212 -0
- data/docs/concepts/overview.md +80 -0
- data/docs/concepts/persisting.md +376 -0
- data/docs/concepts/relationships.md +527 -0
- data/docs/concepts/resources.md +677 -0
- data/docs/getting-started/first-api.md +289 -0
- data/docs/getting-started/installation.md +185 -0
- data/docs/intro.md +307 -0
- data/docs/js/authentication.md +63 -0
- data/docs/js/ddau.md +20 -0
- data/docs/js/extra-params.md +41 -0
- data/docs/js/index.md +112 -0
- data/docs/js/installation.md +120 -0
- data/docs/js/middleware.md +72 -0
- data/docs/js/models.md +202 -0
- data/docs/js/reads.md +494 -0
- data/docs/js/state-syncing.md +100 -0
- data/docs/js/writes.md +373 -0
- data/docs/reference/vandal.md +63 -0
- data/docs/reference/why.md +13 -0
- data/docs/topics/authorization.md +155 -0
- data/docs/topics/caching.md +55 -0
- data/docs/topics/customizing-sideloads.md +156 -0
- data/docs/topics/debugging.md +216 -0
- data/docs/topics/error-handling.md +210 -0
- data/docs/topics/etags.md +46 -0
- data/docs/topics/hopping-relationships.md +149 -0
- data/docs/topics/json-attributes.md +77 -0
- data/docs/topics/openstruct-models.md +50 -0
- data/docs/topics/remote-resources.md +291 -0
- data/docs/topics/testing.md +894 -0
- data/docs/topics/without-activerecord.md +324 -0
- data/docs/tutorial/index.md +58 -0
- data/docs/tutorial/step_0.md +107 -0
- data/docs/tutorial/step_1.md +199 -0
- data/docs/tutorial/step_2.md +312 -0
- data/docs/tutorial/step_3.md +142 -0
- data/docs/tutorial/step_4.md +135 -0
- data/docs/tutorial/step_5.md +69 -0
- data/docs/tutorial/step_6.md +82 -0
- data/docs/tutorial/step_7.md +205 -0
- data/docs/tutorial/step_8.md +128 -0
- data/docs/tutorial/step_9.md +171 -0
- data/docs/upgrading.md +265 -0
- data/gemfiles/rails_7_1.gemfile +4 -3
- data/gemfiles/{rails_7_2_graphiti_rails.gemfile → rails_7_2.gemfile} +3 -3
- data/gemfiles/{rails_8_1_graphiti_rails.gemfile → rails_8_0.gemfile} +3 -3
- data/gemfiles/{rails_8_0_graphiti_rails.gemfile → rails_8_1.gemfile} +3 -3
- data/graphiti.gemspec +7 -5
- data/{deprecated_generators → lib/generators}/graphiti/api_test_generator.rb +7 -1
- data/{deprecated_generators → lib/generators}/graphiti/generator_mixin.rb +14 -1
- data/{deprecated_generators → lib/generators}/graphiti/install_generator.rb +19 -13
- data/{deprecated_generators → lib/generators}/graphiti/resource_generator.rb +43 -6
- data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
- data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
- data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
- data/lib/graphiti/configuration.rb +2 -2
- data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
- data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
- data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
- data/lib/graphiti/error_serializers/validation.rb +143 -0
- data/lib/graphiti/errors.rb +4 -23
- data/lib/graphiti/query.rb +1 -1
- data/lib/graphiti/rails/context.rb +33 -0
- data/lib/graphiti/rails/controller.rb +41 -0
- data/lib/graphiti/rails/debugging.rb +18 -0
- data/lib/graphiti/rails/exception_handlers.rb +77 -0
- data/lib/graphiti/rails/railtie.rb +139 -0
- data/lib/graphiti/rails/responders.rb +21 -0
- data/lib/graphiti/rails/test_helpers.rb +22 -0
- data/lib/graphiti/rails.rb +47 -29
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/interface.rb +2 -2
- data/lib/graphiti/resource/persistence.rb +14 -2
- data/lib/graphiti/resource/remote.rb +2 -2
- data/lib/graphiti/resource/sideloading.rb +1 -1
- data/lib/graphiti/resource.rb +13 -1
- data/lib/graphiti/responders.rb +7 -20
- data/lib/graphiti/schema.rb +5 -1
- data/lib/graphiti/schema_diff.rb +4 -0
- data/lib/graphiti/scope.rb +45 -37
- data/lib/graphiti/serializer.rb +6 -0
- data/lib/graphiti/sideload/belongs_to.rb +38 -5
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
- data/lib/graphiti/sideload.rb +54 -35
- data/lib/graphiti/spec_helpers/errors.rb +73 -0
- data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
- data/lib/graphiti/spec_helpers/helpers.rb +107 -0
- data/lib/graphiti/spec_helpers/node.rb +88 -0
- data/lib/graphiti/spec_helpers/rspec.rb +147 -0
- data/lib/graphiti/spec_helpers.rb +53 -0
- data/lib/graphiti/util/include_params.rb +2 -2
- data/lib/graphiti/util/persistence.rb +10 -11
- data/lib/graphiti/util/serializer_relationships.rb +41 -5
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti-rails.rb +11 -0
- data/lib/graphiti.rb +34 -10
- data/lib/graphiti_errors.rb +11 -0
- data/lib/graphiti_spec_helpers/rspec.rb +3 -0
- data/lib/graphiti_spec_helpers.rb +11 -0
- data/lib/{graphiti/deprecated_tasks.rb → tasks/graphiti.rake} +6 -1
- data/package-lock.json +6199 -0
- data/package.json +5 -4
- data/website/.gitignore +20 -0
- data/website/README.md +43 -0
- data/website/docusaurus.config.js +141 -0
- data/website/package-lock.json +19474 -0
- data/website/package.json +46 -0
- data/website/sidebars.js +82 -0
- data/website/src/css/custom.css +58 -0
- data/website/src/pages/markdown-page.mdx +7 -0
- data/website/static/.nojekyll +0 -0
- data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
- data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
- data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
- data/website/static/1.13/2019/10/14/tutorial.html +198 -0
- data/website/static/1.13/CNAME +1 -0
- data/website/static/1.13/README.md +16 -0
- data/website/static/1.13/assets/css/syntax.css +60 -0
- data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
- data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
- data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
- data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
- data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
- data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
- data/website/static/1.13/assets/favicons/favicon.ico +0 -0
- data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
- data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
- data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
- data/website/static/1.13/assets/img/backend.gif +0 -0
- data/website/static/1.13/assets/img/conformity.png +0 -0
- data/website/static/1.13/assets/img/error_payload.png +0 -0
- data/website/static/1.13/assets/img/gh.png +0 -0
- data/website/static/1.13/assets/img/lifecycle.gif +0 -0
- data/website/static/1.13/assets/img/logo-500.png +0 -0
- data/website/static/1.13/assets/img/logo.png +0 -0
- data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/meta_total_count.png +0 -0
- data/website/static/1.13/assets/img/persist.jpg +0 -0
- data/website/static/1.13/assets/img/resource.gif +0 -0
- data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/rest1.gif +0 -0
- data/website/static/1.13/assets/img/rest2.gif +0 -0
- data/website/static/1.13/assets/img/rest3.gif +0 -0
- data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/why.png +0 -0
- data/website/static/1.13/assets/js/highlight.pack.js +2 -0
- data/website/static/1.13/assets/main.css +15518 -0
- data/website/static/1.13/assets/main.css.map +1 -0
- data/website/static/1.13/bin/bundle +109 -0
- data/website/static/1.13/bin/jekyll +27 -0
- data/website/static/1.13/bin/kramdown +27 -0
- data/website/static/1.13/bin/listen +27 -0
- data/website/static/1.13/bin/rake +27 -0
- data/website/static/1.13/bin/rougify +27 -0
- data/website/static/1.13/bin/safe_yaml +27 -0
- data/website/static/1.13/bin/sass +27 -0
- data/website/static/1.13/bin/sass-convert +27 -0
- data/website/static/1.13/bin/scss +27 -0
- data/website/static/1.13/blog.html +259 -0
- data/website/static/1.13/cheatsheet.html +316 -0
- data/website/static/1.13/cookbooks/authorization.md +0 -0
- data/website/static/1.13/cookbooks/caching.md +0 -0
- data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
- data/website/static/1.13/cookbooks/etags.md +0 -0
- data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
- data/website/static/1.13/cookbooks/json_attributes.md +0 -0
- data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
- data/website/static/1.13/cookbooks/remote-resources.md +0 -0
- data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
- data/website/static/1.13/features.html +249 -0
- data/website/static/1.13/feed.xml +106 -0
- data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
- data/website/static/1.13/guides/concepts/debugging.html +440 -0
- data/website/static/1.13/guides/concepts/endpoints.html +432 -0
- data/website/static/1.13/guides/concepts/error-handling.html +396 -0
- data/website/static/1.13/guides/concepts/links.html +501 -0
- data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
- data/website/static/1.13/guides/concepts/resources.html +2176 -0
- data/website/static/1.13/guides/concepts/testing.html +1469 -0
- data/website/static/1.13/guides/getting-started/installation.html +420 -0
- data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
- data/website/static/1.13/guides/index.html +269 -0
- data/website/static/1.13/guides/overview.html +325 -0
- data/website/static/1.13/guides/upgrading-2-0.html +193 -0
- data/website/static/1.13/guides/upgrading.html +314 -0
- data/website/static/1.13/guides/vandal.html +282 -0
- data/website/static/1.13/guides/why.html +1121 -0
- data/website/static/1.13/index.html +72 -0
- data/website/static/1.13/js/authentication.html +295 -0
- data/website/static/1.13/js/ddau.html +238 -0
- data/website/static/1.13/js/extra-params.html +270 -0
- data/website/static/1.13/js/index.html +321 -0
- data/website/static/1.13/js/installation.html +637 -0
- data/website/static/1.13/js/introduction.html +257 -0
- data/website/static/1.13/js/middleware.html +318 -0
- data/website/static/1.13/js/reads/fieldsets.html +271 -0
- data/website/static/1.13/js/reads/filtering.html +289 -0
- data/website/static/1.13/js/reads/includes.html +260 -0
- data/website/static/1.13/js/reads/index.html +497 -0
- data/website/static/1.13/js/reads/nested-queries.html +353 -0
- data/website/static/1.13/js/reads/pagination.html +260 -0
- data/website/static/1.13/js/reads/sorting.html +265 -0
- data/website/static/1.13/js/reads/statistics.html +289 -0
- data/website/static/1.13/js/state-syncing.html +340 -0
- data/website/static/1.13/js/writes/deferred.html +296 -0
- data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
- data/website/static/1.13/js/writes/index.html +391 -0
- data/website/static/1.13/js/writes/nested.html +330 -0
- data/website/static/1.13/js/writes/validations.html +272 -0
- data/website/static/1.13/quickstart.html +660 -0
- data/website/static/1.13/template +161 -0
- data/website/static/1.13/tutorial/index.html +250 -0
- data/website/static/1.13/tutorial/step_0.html +292 -0
- data/website/static/1.13/tutorial/step_1.html +517 -0
- data/website/static/1.13/tutorial/step_2.html +481 -0
- data/website/static/1.13/tutorial/step_3.html +323 -0
- data/website/static/1.13/tutorial/step_4.html +318 -0
- data/website/static/1.13/tutorial/step_5.html +265 -0
- data/website/static/1.13/tutorial/step_6.html +276 -0
- data/website/static/1.13/tutorial/step_7.html +390 -0
- data/website/static/1.13/tutorial/step_8.html +316 -0
- data/website/static/1.13/tutorial/step_9.html +365 -0
- data/website/static/assets/img/error_payload.png +0 -0
- data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
- data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
- data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
- data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
- data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
- data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
- data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
- data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
- data/website/static/assets/img/meta_total_count.png +0 -0
- data/website/static/img/docusaurus-social-card.jpg +0 -0
- data/website/static/img/docusaurus.png +0 -0
- data/website/static/img/favicon.ico +0 -0
- data/website/static/img/logo.png +0 -0
- data/website/static/img/logo.svg +1 -0
- data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
- data/website/static/img/undraw_docusaurus_react.svg +170 -0
- data/website/static/img/undraw_docusaurus_tree.svg +40 -0
- metadata +245 -46
- data/gemfiles/rails_6.gemfile +0 -18
- data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
- data/gemfiles/rails_7.gemfile +0 -18
- data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
- data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
- data/lib/graphiti/railtie.rb +0 -121
- /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
- /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
|
+
»
|
|
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
|
+
»
|
|
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
|
+
»
|
|
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.
|