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.
- checksums.yaml +4 -4
- data/.git-blame-ignore-revs +2 -0
- data/.github/workflows/ci.yml +55 -3
- data/.github/workflows/docs.yml +1 -1
- data/.github/workflows/notify-example-app.yml +18 -0
- data/.gitignore +2 -0
- data/.standard.yml +6 -0
- data/CHANGELOG.md +45 -0
- data/CONTRIBUTING.md +3 -1
- data/README.md +13 -206
- data/Rakefile +6 -3
- data/UPGRADING.md +1 -1
- data/graphiti.gemspec +8 -4
- data/lib/generators/graphiti/generator_mixin.rb +1 -1
- data/lib/generators/graphiti/install_generator.rb +1 -1
- data/lib/generators/graphiti/resource_generator.rb +6 -3
- data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
- data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
- data/lib/graphiti/audit/report.rb +6 -6
- data/lib/graphiti/audit.rb +3 -4
- data/lib/graphiti/delegates/pagination.rb +2 -2
- data/lib/graphiti/deserializer.rb +3 -3
- data/lib/graphiti/errors.rb +6 -6
- data/lib/graphiti/extensions/temp_id.rb +1 -1
- data/lib/graphiti/query.rb +3 -3
- data/lib/graphiti/rails/context.rb +2 -1
- data/lib/graphiti/rails/controller.rb +3 -0
- data/lib/graphiti/rails/debugging.rb +1 -1
- data/lib/graphiti/rails/responders.rb +2 -1
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/dsl.rb +2 -2
- data/lib/graphiti/resource/links.rb +1 -1
- data/lib/graphiti/resource/persistence.rb +1 -1
- data/lib/graphiti/resource/polymorphism.rb +1 -1
- data/lib/graphiti/resource.rb +2 -2
- data/lib/graphiti/resource_proxy.rb +0 -1
- data/lib/graphiti/scoping/filter.rb +5 -3
- data/lib/graphiti/scoping/paginate.rb +1 -1
- data/lib/graphiti/scoping/sort.rb +1 -1
- data/lib/graphiti/sideload/has_many.rb +1 -1
- data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
- data/lib/graphiti/spec_helpers/matchers.rb +1 -1
- data/lib/graphiti/types.rb +5 -1
- data/lib/graphiti/util/cache_debug.rb +3 -3
- data/lib/graphiti/util/hash.rb +1 -1
- data/lib/graphiti/util/persistence.rb +6 -6
- data/lib/graphiti/util/serializer_relationships.rb +1 -1
- data/lib/graphiti/version.rb +1 -1
- data/lib/tasks/graphiti.rake +1 -1
- metadata +31 -197
- data/docs/concepts/backends-and-models.md +0 -122
- data/docs/concepts/endpoints.md +0 -183
- data/docs/concepts/links.md +0 -212
- data/docs/concepts/overview.md +0 -80
- data/docs/concepts/persisting.md +0 -376
- data/docs/concepts/relationships.md +0 -627
- data/docs/concepts/resources.md +0 -677
- data/docs/getting-started/first-api.md +0 -289
- data/docs/getting-started/installation.md +0 -185
- data/docs/intro.md +0 -307
- data/docs/js/authentication.md +0 -63
- data/docs/js/ddau.md +0 -20
- data/docs/js/extra-params.md +0 -41
- data/docs/js/index.md +0 -112
- data/docs/js/installation.md +0 -120
- data/docs/js/middleware.md +0 -72
- data/docs/js/models.md +0 -202
- data/docs/js/reads.md +0 -494
- data/docs/js/state-syncing.md +0 -100
- data/docs/js/writes.md +0 -373
- data/docs/reference/vandal.md +0 -63
- data/docs/reference/why.md +0 -13
- data/docs/topics/authorization.md +0 -155
- data/docs/topics/caching.md +0 -55
- data/docs/topics/customizing-sideloads.md +0 -156
- data/docs/topics/debugging.md +0 -242
- data/docs/topics/error-handling.md +0 -210
- data/docs/topics/etags.md +0 -46
- data/docs/topics/hopping-relationships.md +0 -149
- data/docs/topics/json-attributes.md +0 -77
- data/docs/topics/openstruct-models.md +0 -50
- data/docs/topics/remote-resources.md +0 -291
- data/docs/topics/testing.md +0 -916
- data/docs/topics/without-activerecord.md +0 -324
- data/docs/tutorial/index.md +0 -58
- data/docs/tutorial/step_0.md +0 -107
- data/docs/tutorial/step_1.md +0 -199
- data/docs/tutorial/step_2.md +0 -312
- data/docs/tutorial/step_3.md +0 -142
- data/docs/tutorial/step_4.md +0 -135
- data/docs/tutorial/step_5.md +0 -69
- data/docs/tutorial/step_6.md +0 -82
- data/docs/tutorial/step_7.md +0 -205
- data/docs/tutorial/step_8.md +0 -128
- data/docs/tutorial/step_9.md +0 -171
- data/docs/upgrading.md +0 -267
- data/website/.gitignore +0 -20
- data/website/README.md +0 -43
- data/website/docusaurus.config.js +0 -197
- data/website/package-lock.json +0 -19474
- data/website/package.json +0 -46
- data/website/sidebars.js +0 -82
- data/website/src/css/custom.css +0 -85
- data/website/src/pages/markdown-page.mdx +0 -7
- data/website/static/.nojekyll +0 -0
- data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
- data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
- data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
- data/website/static/1.13/2019/10/14/tutorial.html +0 -198
- data/website/static/1.13/CNAME +0 -1
- data/website/static/1.13/README.md +0 -16
- data/website/static/1.13/assets/css/syntax.css +0 -60
- 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 +0 -9
- 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 +0 -1
- data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
- 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/fancy-cushion.png +0 -0
- data/website/static/1.13/assets/img/gh.png +0 -0
- data/website/static/1.13/assets/img/home-bg.jpg +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/sunrise.png +0 -0
- data/website/static/1.13/assets/img/why.png +0 -0
- data/website/static/1.13/assets/js/highlight.pack.js +0 -2
- data/website/static/1.13/assets/main.css +0 -15518
- data/website/static/1.13/assets/main.css.map +0 -1
- data/website/static/1.13/bin/bundle +0 -109
- data/website/static/1.13/bin/jekyll +0 -27
- data/website/static/1.13/bin/kramdown +0 -27
- data/website/static/1.13/bin/listen +0 -27
- data/website/static/1.13/bin/rake +0 -27
- data/website/static/1.13/bin/rougify +0 -27
- data/website/static/1.13/bin/safe_yaml +0 -27
- data/website/static/1.13/bin/sass +0 -27
- data/website/static/1.13/bin/sass-convert +0 -27
- data/website/static/1.13/bin/scss +0 -27
- data/website/static/1.13/blog.html +0 -259
- data/website/static/1.13/cheatsheet.html +0 -316
- 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 +0 -325
- data/website/static/1.13/cookbooks/etags.md +0 -0
- data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
- 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 +0 -510
- data/website/static/1.13/features.html +0 -249
- data/website/static/1.13/feed.xml +0 -106
- data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
- data/website/static/1.13/guides/concepts/debugging.html +0 -440
- data/website/static/1.13/guides/concepts/endpoints.html +0 -432
- data/website/static/1.13/guides/concepts/error-handling.html +0 -396
- data/website/static/1.13/guides/concepts/links.html +0 -501
- data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
- data/website/static/1.13/guides/concepts/resources.html +0 -2176
- data/website/static/1.13/guides/concepts/testing.html +0 -1469
- data/website/static/1.13/guides/getting-started/installation.html +0 -420
- data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
- data/website/static/1.13/guides/index.html +0 -269
- data/website/static/1.13/guides/overview.html +0 -325
- data/website/static/1.13/guides/upgrading-2-0.html +0 -193
- data/website/static/1.13/guides/upgrading.html +0 -314
- data/website/static/1.13/guides/vandal.html +0 -282
- data/website/static/1.13/guides/why.html +0 -1121
- data/website/static/1.13/index.html +0 -72
- data/website/static/1.13/js/authentication.html +0 -295
- data/website/static/1.13/js/ddau.html +0 -238
- data/website/static/1.13/js/extra-params.html +0 -270
- data/website/static/1.13/js/index.html +0 -321
- data/website/static/1.13/js/installation.html +0 -637
- data/website/static/1.13/js/introduction.html +0 -257
- data/website/static/1.13/js/middleware.html +0 -318
- data/website/static/1.13/js/reads/fieldsets.html +0 -271
- data/website/static/1.13/js/reads/filtering.html +0 -289
- data/website/static/1.13/js/reads/includes.html +0 -260
- data/website/static/1.13/js/reads/index.html +0 -497
- data/website/static/1.13/js/reads/nested-queries.html +0 -353
- data/website/static/1.13/js/reads/pagination.html +0 -260
- data/website/static/1.13/js/reads/sorting.html +0 -265
- data/website/static/1.13/js/reads/statistics.html +0 -289
- data/website/static/1.13/js/state-syncing.html +0 -340
- data/website/static/1.13/js/writes/deferred.html +0 -296
- data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
- data/website/static/1.13/js/writes/index.html +0 -391
- data/website/static/1.13/js/writes/nested.html +0 -330
- data/website/static/1.13/js/writes/validations.html +0 -272
- data/website/static/1.13/quickstart.html +0 -660
- data/website/static/1.13/template +0 -161
- data/website/static/1.13/tutorial/index.html +0 -250
- data/website/static/1.13/tutorial/step_0.html +0 -292
- data/website/static/1.13/tutorial/step_1.html +0 -517
- data/website/static/1.13/tutorial/step_2.html +0 -481
- data/website/static/1.13/tutorial/step_3.html +0 -323
- data/website/static/1.13/tutorial/step_4.html +0 -318
- data/website/static/1.13/tutorial/step_5.html +0 -265
- data/website/static/1.13/tutorial/step_6.html +0 -276
- data/website/static/1.13/tutorial/step_7.html +0 -390
- data/website/static/1.13/tutorial/step_8.html +0 -316
- data/website/static/1.13/tutorial/step_9.html +0 -365
- data/website/static/CNAME +0 -1
- 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 +0 -1
- data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
- data/website/static/img/undraw_docusaurus_react.svg +0 -170
- data/website/static/img/undraw_docusaurus_tree.svg +0 -40
data/docs/tutorial/step_2.md
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Step 2'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## Step 2: Has Many
|
|
6
|
-
|
|
7
|
-
> [View the Code](https://github.com/graphiti-api/employee_directory/compare/step_1_employees...step_2_positions)
|
|
8
|
-
|
|
9
|
-
We'll be adding the database table `positions`:
|
|
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">employee_id</th>
|
|
16
|
-
<th class="text-center">title</th>
|
|
17
|
-
<th class="text-center">active</th>
|
|
18
|
-
<th class="text-center">historical_index</th>
|
|
19
|
-
<th class="text-center">created_at</th>
|
|
20
|
-
<th class="text-center">updated_at</th>
|
|
21
|
-
</tr>
|
|
22
|
-
</thead>
|
|
23
|
-
<tbody>
|
|
24
|
-
<tr>
|
|
25
|
-
<td>1</td>
|
|
26
|
-
<td>900</td>
|
|
27
|
-
<td>Engineer</td>
|
|
28
|
-
<td>true</td>
|
|
29
|
-
<td>1</td>
|
|
30
|
-
<td>2018-09-04</td>
|
|
31
|
-
<td>2018-09-04</td>
|
|
32
|
-
</tr>
|
|
33
|
-
<tr>
|
|
34
|
-
<td>2</td>
|
|
35
|
-
<td>900</td>
|
|
36
|
-
<td>Intern</td>
|
|
37
|
-
<td>true</td>
|
|
38
|
-
<td>2</td>
|
|
39
|
-
<td>2018-09-04</td>
|
|
40
|
-
<td>2018-09-04</td>
|
|
41
|
-
</tr>
|
|
42
|
-
<tr>
|
|
43
|
-
<td>3</td>
|
|
44
|
-
<td>800</td>
|
|
45
|
-
<td>Manager</td>
|
|
46
|
-
<td>true</td>
|
|
47
|
-
<td>1</td>
|
|
48
|
-
<td>2018-09-04</td>
|
|
49
|
-
<td>2018-09-04</td>
|
|
50
|
-
</tr>
|
|
51
|
-
</tbody>
|
|
52
|
-
</table>
|
|
53
|
-
|
|
54
|
-
Because this table tracks all historical positions, we have the
|
|
55
|
-
`historical_index` column. This tells the order the employee moved through each position, where `1` is most recent.
|
|
56
|
-
|
|
57
|
-
### The Rails Stuff 🚂
|
|
58
|
-
|
|
59
|
-
Generate the `Position` model:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
$ bin/rails g model Position title:string active:boolean historical_index:integer employee:belongs_to
|
|
63
|
-
$ bin/rails db:migrate
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Update the `Employee` model with the association, too:
|
|
67
|
-
|
|
68
|
-
```ruby
|
|
69
|
-
# app/models/employee.rb
|
|
70
|
-
has_many :positions
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
And update our seed data:
|
|
74
|
-
|
|
75
|
-
```ruby
|
|
76
|
-
# db/seeds.rb
|
|
77
|
-
[Employee, Position].each(&:delete_all)
|
|
78
|
-
|
|
79
|
-
100.times do
|
|
80
|
-
employee = Employee.create! first_name: Faker::Name.first_name,
|
|
81
|
-
last_name: Faker::Name.last_name,
|
|
82
|
-
age: rand(20..80)
|
|
83
|
-
|
|
84
|
-
(1..2).each do |i|
|
|
85
|
-
employee.positions.create! title: Faker::Job.title,
|
|
86
|
-
historical_index: i,
|
|
87
|
-
active: i == 1
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
$ bin/rails db:seed
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### The Graphiti Stuff 🎨
|
|
97
|
-
|
|
98
|
-
Let's start by running the same command as before to create
|
|
99
|
-
`PositionResource`:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
$ bin/rails g graphiti:resource Position title:string active:boolean
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
We'll need to add the association, just like ActiveRecord:
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
# app/resources/employee_resource.rb
|
|
109
|
-
has_many :positions
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
...and a corresponding filter:
|
|
113
|
-
|
|
114
|
-
```ruby
|
|
115
|
-
# app/resources/position_resource.rb
|
|
116
|
-
filter :employee_id, :integer
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
If you visit `/api/v1/employees`, you'll see a number of HTTP
|
|
120
|
-
[Links](https://www.graphiti.dev/guides/concepts/links)
|
|
121
|
-
that allow lazy-loading positions. Or, if you visit
|
|
122
|
-
`/api/v1/employees?include=positions`, you'll load the employees and
|
|
123
|
-
positions in a single request. We'll dig a bit deeper into this logic
|
|
124
|
-
in the section below.
|
|
125
|
-
|
|
126
|
-
Before we get there, let's revisit the `historical_index` column. For now, let's
|
|
127
|
-
treat this as an implementation detail that the API should not expose -
|
|
128
|
-
let's say we want to support sorting on this attribute but nothing else:
|
|
129
|
-
|
|
130
|
-
```ruby
|
|
131
|
-
attribute :historical_index, :integer, only: [:sortable]
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
We're almost done, but if you run your tests you'll see two outstanding
|
|
135
|
-
errors. This is because Rails requires `belongs_to` associations by default. We can't save a `Position` without its corresponding `Employee`.
|
|
136
|
-
|
|
137
|
-
We can solve this in three ways:
|
|
138
|
-
|
|
139
|
-
* Turn this off globally, with [config.active_record.belongs_to_required_by_default](https://edgeguides.rubyonrails.org/configuring.html#configuring-active-record). You may want to do this in test-mode only.
|
|
140
|
-
* Turn this off for the specific association: `belongs_to :employee, optional: true`.
|
|
141
|
-
* Associate an `Employee` as part of the API request.
|
|
142
|
-
|
|
143
|
-
We'll take for the last option. Look at
|
|
144
|
-
`spec/resources/position/writes_spec.rb`:
|
|
145
|
-
|
|
146
|
-
```ruby
|
|
147
|
-
RSpec.describe PositionResource, type: :resource do
|
|
148
|
-
describe 'creating' do
|
|
149
|
-
let(:payload) do
|
|
150
|
-
{
|
|
151
|
-
data: {
|
|
152
|
-
type: 'positions',
|
|
153
|
-
attributes: { }
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
let(:instance) do
|
|
159
|
-
PositionResource.build(payload)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
it 'works' do
|
|
163
|
-
expect {
|
|
164
|
-
expect(instance.save).to eq(true)
|
|
165
|
-
}.to change { Position.count }.by(1)
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
When running our tests, let's make sure the `historical_index` column
|
|
172
|
-
reflects the order we created the positions. This code recalculates
|
|
173
|
-
everything after a record is saved:
|
|
174
|
-
|
|
175
|
-
```ruby
|
|
176
|
-
# spec/factories/position.rb
|
|
177
|
-
FactoryBot.define do
|
|
178
|
-
factory :position do
|
|
179
|
-
employee
|
|
180
|
-
|
|
181
|
-
title { Faker::Job.title }
|
|
182
|
-
|
|
183
|
-
after(:create) do |position|
|
|
184
|
-
unless position.historical_index
|
|
185
|
-
scope = Position
|
|
186
|
-
.where(employee_id: position.employee.id)
|
|
187
|
-
.order(created_at: :desc)
|
|
188
|
-
scope.each_with_index do |p, index|
|
|
189
|
-
p.update_attribute(:historical_index, index + 1)
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Let's associate an `Employee`. Start by seeding the data:
|
|
198
|
-
|
|
199
|
-
```ruby
|
|
200
|
-
let!(:employee) { create(:employee) }
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
And associate via `relationships`:
|
|
204
|
-
|
|
205
|
-
```ruby
|
|
206
|
-
let(:payload) do
|
|
207
|
-
{
|
|
208
|
-
data: {
|
|
209
|
-
type: 'positions',
|
|
210
|
-
attributes: { },
|
|
211
|
-
relationships: {
|
|
212
|
-
employee: {
|
|
213
|
-
data: {
|
|
214
|
-
id: employee.id.to_s,
|
|
215
|
-
type: 'employees'
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
end
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
To ensure the `PositionResource` will process this relationship, the
|
|
225
|
-
last step is to add it:
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
# app/resources/position_resource.rb
|
|
229
|
-
belongs_to :employee
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
This will associate the `Position` to the `Employee` as part of the
|
|
233
|
-
creation process. The test should now pass - make the same change to
|
|
234
|
-
`spec/api/v1/positions/create_spec.rb` to get a fully-passing test
|
|
235
|
-
suite.
|
|
236
|
-
|
|
237
|
-
#### Digging Deeper 🧐
|
|
238
|
-
|
|
239
|
-
Why did we need the `employee_id` filter above? To explain that, let's dive deeper into the logic connecting Resources.
|
|
240
|
-
|
|
241
|
-
If you hit `/api/v1/employees`, you'll see a number of
|
|
242
|
-
[Links](https://www.graphiti.dev/guides/concepts/links) in the
|
|
243
|
-
response. These are useful for lazy-loading, but the same logic
|
|
244
|
-
applies to eager loading. Let's take a look at a Link to see how these
|
|
245
|
-
Resources connect together:
|
|
246
|
-
|
|
247
|
-
```ruby
|
|
248
|
-
{
|
|
249
|
-
...
|
|
250
|
-
relationships: {
|
|
251
|
-
positions: {
|
|
252
|
-
links: {
|
|
253
|
-
related: "http://localhost:3000/api/v1/positions?filter[employee_id]=1"
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
...
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
The salient bit: `/positions?filter[employee_id]=1`. In other words,
|
|
262
|
-
fetch all Positions for the given Employee id.That means, whether we're lazy-loading data in separate requests or
|
|
263
|
-
eager-loading in a single request, **the same logic fires
|
|
264
|
-
under-the-hood**:
|
|
265
|
-
|
|
266
|
-
```ruby
|
|
267
|
-
PositionResource.all({
|
|
268
|
-
filter: { employee_id: 1 }
|
|
269
|
-
})
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
This means we need `filter :employee_id, :integer` to satisfy the query.
|
|
273
|
-
|
|
274
|
-
We can customize the logic connecting Resources in a few different
|
|
275
|
-
ways. First some simple options:
|
|
276
|
-
|
|
277
|
-
```ruby
|
|
278
|
-
has_many :positions, foreign_key: :emp_id, primary_key: :eid
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
So far so good. The logic, and corresponding Link, both update as you'd
|
|
282
|
-
expect (though we'd of course need a corresponding `filter :emp_id, :integer` on `PositionResource`).
|
|
283
|
-
|
|
284
|
-
Those options are just simple versions of parameter customization.
|
|
285
|
-
You can customize parameters connecting Resources with the `params` block:
|
|
286
|
-
|
|
287
|
-
```ruby
|
|
288
|
-
has_many :positions do
|
|
289
|
-
params do |hash, employees|
|
|
290
|
-
hash[:filter] # => { employee_id: employees.map(&:id) }
|
|
291
|
-
hash[:filter][:active] = true
|
|
292
|
-
hash[:sort] = '-created_at'
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Customizing these params affects the Link as well as the eager-load
|
|
298
|
-
logic. Remember the parameters here should reflect the JSON:API
|
|
299
|
-
specification, or anything `PositionResource.all` accepts.
|
|
300
|
-
|
|
301
|
-
These are the most common options, but there's a bunch more. Check
|
|
302
|
-
out the [Resource Relationships Guide](/concepts/relationships) to dig even deeper.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<h2 id="next">
|
|
307
|
-
<a href="/tutorial/step_3">
|
|
308
|
-
NEXT -
|
|
309
|
-
<small>Step 3: Belongs To</small>
|
|
310
|
-
»
|
|
311
|
-
</a>
|
|
312
|
-
</h2>
|
data/docs/tutorial/step_3.md
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Step 3'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
### Step 3: Belongs To
|
|
6
|
-
|
|
7
|
-
> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_2_positions...step_3_departments)
|
|
8
|
-
|
|
9
|
-
We'll be adding the database table `departments`:
|
|
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">name</th>
|
|
16
|
-
</tr>
|
|
17
|
-
</thead>
|
|
18
|
-
<tbody>
|
|
19
|
-
<tr>
|
|
20
|
-
<td>1</td>
|
|
21
|
-
<td>Engineering</td>
|
|
22
|
-
</tr>
|
|
23
|
-
<tr>
|
|
24
|
-
<td>2</td>
|
|
25
|
-
<td>Safety</td>
|
|
26
|
-
</tr>
|
|
27
|
-
<tr>
|
|
28
|
-
<td>3</td>
|
|
29
|
-
<td>QA</td>
|
|
30
|
-
</tr>
|
|
31
|
-
</tbody>
|
|
32
|
-
</table>
|
|
33
|
-
|
|
34
|
-
We'll also be adding a `department_id:integer` foreign key column to the `positions` table.
|
|
35
|
-
|
|
36
|
-
### The Rails Stuff 🚂
|
|
37
|
-
|
|
38
|
-
Generate the `Department` model:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
$ bin/rails g model Department name:string
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
To add the foreign key to `positions`:
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
$ bin/rails g migration add_department_id_to_positions
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
```ruby
|
|
51
|
-
class AddDepartmentIdToPositions < ActiveRecord::Migration[7.1]
|
|
52
|
-
def change
|
|
53
|
-
add_foreign_key :positions, :departments
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Update the database:
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
$ bin/rails db:migrate
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Update our seed file:
|
|
65
|
-
|
|
66
|
-
```ruby
|
|
67
|
-
[Employee, Position, Department].each(&:delete_all)
|
|
68
|
-
|
|
69
|
-
engineering = Department.create! name: 'Engineering'
|
|
70
|
-
safety = Department.create! name: 'Safety'
|
|
71
|
-
qa = Department.create! name: 'QA'
|
|
72
|
-
departments = [engineering, safety, qa]
|
|
73
|
-
|
|
74
|
-
100.times do
|
|
75
|
-
employee = Employee.create! first_name: Faker::Name.first_name,
|
|
76
|
-
last_name: Faker::Name.last_name,
|
|
77
|
-
age: rand(20..80)
|
|
78
|
-
|
|
79
|
-
(1..2).each do |i|
|
|
80
|
-
employee.positions.create! title: Faker::Job.title,
|
|
81
|
-
historical_index: i,
|
|
82
|
-
active: i == 1,
|
|
83
|
-
department: departments.sample
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Make sure to update `spec/factories/departments.rb` with randomized
|
|
89
|
-
data. Then, since this is also a required relationship, update
|
|
90
|
-
`spec/factories/positions.rb` to always seed a department when we ask to
|
|
91
|
-
create a position:
|
|
92
|
-
|
|
93
|
-
```ruby
|
|
94
|
-
factory :position do
|
|
95
|
-
employee
|
|
96
|
-
department
|
|
97
|
-
|
|
98
|
-
# ... code ...
|
|
99
|
-
end
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### The Graphiti Stuff 🎨
|
|
103
|
-
|
|
104
|
-
You should be used to this by now:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
bin/rails g graphiti:resource Department name:string
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Add the association:
|
|
111
|
-
|
|
112
|
-
```ruby
|
|
113
|
-
# app/resources/position_resource.rb
|
|
114
|
-
belongs_to :department
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
And review the end of [Step 2](/tutorial/step_2) to get all your specs
|
|
118
|
-
passing (add the department to the request payload). Practice makes perfect!
|
|
119
|
-
|
|
120
|
-
#### Digging Deeper 🧐
|
|
121
|
-
|
|
122
|
-
We didn't need a filter like we did in step two. That's
|
|
123
|
-
because the primary key connecting the Resources is `id` by
|
|
124
|
-
default. In other words, the Link would be something like:
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
/departments?filter[id]=1
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Which we get out-of-the-📦
|
|
131
|
-
|
|
132
|
-
But remember, you can customize these relationships just like the
|
|
133
|
-
previous `has_many` section.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<h2 id="next">
|
|
137
|
-
<a href="/tutorial/step_4">
|
|
138
|
-
NEXT -
|
|
139
|
-
<small>Step 4: Customizing Queries</small>
|
|
140
|
-
»
|
|
141
|
-
</a>
|
|
142
|
-
</h2>
|
data/docs/tutorial/step_4.md
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Step 4'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
### Step 4: Customizing Queries
|
|
6
|
-
|
|
7
|
-
> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_3_departments...step_4_customizations)
|
|
8
|
-
|
|
9
|
-
So far, we've done fairly straightforward queries. If a user filters on
|
|
10
|
-
`first_name`:
|
|
11
|
-
|
|
12
|
-
`/api/v1/employees?filter[first_name]=Foo`
|
|
13
|
-
|
|
14
|
-
We'll query the equivalent database column:
|
|
15
|
-
|
|
16
|
-
```ruby
|
|
17
|
-
Employee.where(first_name: 'Foo')
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
But what if there's more complex logic? Let's say we want to sort
|
|
21
|
-
Employees on their `title` - which comes from the `positions` table.
|
|
22
|
-
How would that work?
|
|
23
|
-
|
|
24
|
-
### The Rails Stuff 🚂
|
|
25
|
-
|
|
26
|
-
First, we need to get data for an Employee's **current** position.
|
|
27
|
-
Let's start by defining what `current` means
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
# app/models/position.rb
|
|
31
|
-
scope :current, -> { where(historical_index: 1) }
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
> See the [ActiveRecord Scopes](https://guides.rubyonrails.org/active_record_querying.html#scopes) documentation if you're unfamiliar with this concept.
|
|
35
|
-
|
|
36
|
-
Reference this scope in a new association:
|
|
37
|
-
|
|
38
|
-
```ruby
|
|
39
|
-
has_one :current_position,
|
|
40
|
-
-> { current },
|
|
41
|
-
class_name: 'Position'
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Before moving on, let's review what we need to do. The `ActiveRecord`
|
|
45
|
-
code for sorting Employees on their current position's title would be:
|
|
46
|
-
|
|
47
|
-
```ruby
|
|
48
|
-
Employee.joins(:current_position).merge(Position.order(title: :asc))
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Let's wire this up to Graphiti:
|
|
52
|
-
|
|
53
|
-
### The Graphiti Stuff 🎨
|
|
54
|
-
|
|
55
|
-
We're only going to **sort** and **filter** on the `title` attribute -
|
|
56
|
-
never display or persist. So start by defining the attribute as such:
|
|
57
|
-
|
|
58
|
-
```ruby
|
|
59
|
-
attribute :title, :string, only: [:filterable, :sortable]
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Then the `sort` DSL to place our custom query:
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
# app/resources/employee_resource.rb
|
|
66
|
-
sort :title do |scope, direction|
|
|
67
|
-
scope.joins(:current_position).merge(Position.order(title: direction))
|
|
68
|
-
end
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
That's it! When a request to sort on the title comes in, we'll alter our
|
|
72
|
-
scope to join on the `positions` table, and order based on the current position `title`.
|
|
73
|
-
|
|
74
|
-
The solution for filtering is similar:
|
|
75
|
-
|
|
76
|
-
```ruby
|
|
77
|
-
# app/resources/employee_resource.rb
|
|
78
|
-
filter :title do
|
|
79
|
-
eq do |scope, value|
|
|
80
|
-
scope.joins(:current_position).merge(Position.where(title: value))
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
We can now filter on title:
|
|
86
|
-
|
|
87
|
-
`/api/v1/employees?filter[title]=Foo`
|
|
88
|
-
|
|
89
|
-
Let's do one more example - how would we order Employees by department
|
|
90
|
-
name? We *could* start the same way:
|
|
91
|
-
|
|
92
|
-
```ruby
|
|
93
|
-
attribute :department_name, :string, only: [:sortable]
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
But if we're ***only*** sorting, this is actually redundant. Whenever we
|
|
97
|
-
use the `sort` or `filter` DSL, we're creating a sort-only or
|
|
98
|
-
filter-only attribute under the hood. So let's define everything in one
|
|
99
|
-
shot:
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
sort :department_name, :string do |scope, value|
|
|
103
|
-
scope.joins(current_position: :department)
|
|
104
|
-
.merge(Department.order(name: value))
|
|
105
|
-
end
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Remember: you only need to pass the type as the second argument when an
|
|
109
|
-
attribute doesn't already exist. And if you ever get an error saying
|
|
110
|
-
something is unfilterable or unsortable, check to see if you've already
|
|
111
|
-
defined a filter-only or sort-only attribute using these methods.
|
|
112
|
-
|
|
113
|
-
#### Digging Deeper 🧐
|
|
114
|
-
|
|
115
|
-
There's a critical part of Graphiti that makes everything easier: start
|
|
116
|
-
by imagining it doesn't exist.
|
|
117
|
-
|
|
118
|
-
In other words, the meat of the logic above had nothing to do with
|
|
119
|
-
Graphiti code - we're "wiring up" independent ActiveRecord
|
|
120
|
-
queries. If you're ever confused about query logic, get things working
|
|
121
|
-
without Graphiti first.
|
|
122
|
-
|
|
123
|
-
We could have changed the above to ActiveRecord scopes like
|
|
124
|
-
`.order_by_title(title)`, making the wiring code even simpler. Consider
|
|
125
|
-
doing this when the logic is reusable or particlar complex, but be aware
|
|
126
|
-
of the tradeoffs of [double-testing units](https://www.graphiti.dev/guides/concepts/testing#double-testing-units).
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<h2 id="next">
|
|
130
|
-
<a href="/tutorial/step_5">
|
|
131
|
-
NEXT -
|
|
132
|
-
<small>Step 5: Has One</small>
|
|
133
|
-
»
|
|
134
|
-
</a>
|
|
135
|
-
</h2>
|
data/docs/tutorial/step_5.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Step 5'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
### Step 5: Has One
|
|
6
|
-
|
|
7
|
-
> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_4_customizations...step_5_has_one)
|
|
8
|
-
|
|
9
|
-
In the last step, we introduced the concept of a "current position" to
|
|
10
|
-
the model layer. Let's now expose that relationship to the API.
|
|
11
|
-
|
|
12
|
-
### The Rails Stuff 🚂
|
|
13
|
-
|
|
14
|
-
We already defined a `Position.current` scope that we'll re-use -
|
|
15
|
-
let's just make a small tweak to support the opposite use case as
|
|
16
|
-
well:
|
|
17
|
-
|
|
18
|
-
```ruby
|
|
19
|
-
scope :current, ->(bool) {
|
|
20
|
-
clause = { historical_index: 1 }
|
|
21
|
-
bool ? where(clause) : where.not(clause)
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### The Graphiti Stuff 🎨
|
|
26
|
-
|
|
27
|
-
You might already have an idea how this might work from the prior step
|
|
28
|
-
- we'll use the `params` block to customize the relationship. The `has_one` macro ensures the result is treated as a single object and
|
|
29
|
-
not an array.
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
# app/resources/employee_resource.rb
|
|
33
|
-
has_one :current_position, resource: PositionResource do
|
|
34
|
-
params do |hash|
|
|
35
|
-
hash[:filter][:current] = true
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Which means we'll have to implement that filter - re-using the
|
|
41
|
-
ActiveRecord scope we already defined!
|
|
42
|
-
|
|
43
|
-
```ruby
|
|
44
|
-
filter :current, :boolean do
|
|
45
|
-
eq { |scope, value| scope.current(value) }
|
|
46
|
-
end
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
#### Digging Deeper 🧐
|
|
50
|
-
|
|
51
|
-
In this example, we're able to return only a single record because we
|
|
52
|
-
have a `historical_index` column. If this column didn't exist - maybe we're just ordering on `created_at` and taking the first record - we'd
|
|
53
|
-
have a problem. What if we were loading 20 employees and wanted the
|
|
54
|
-
`current_position` of each - what SQL would limit the resultset
|
|
55
|
-
correctly?
|
|
56
|
-
|
|
57
|
-
We call this a [faux has_one](/concepts/relationships#faux-has-one) and there's nothing easily done here. Graphiti will ensure only one record
|
|
58
|
-
is returned by the API, but the query will take longer and loading extra
|
|
59
|
-
records will eat memory. If there are lots of records in the
|
|
60
|
-
association, look into adding a column like `historical_index`.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<h2 id="next">
|
|
64
|
-
<a href="/tutorial/step_6">
|
|
65
|
-
NEXT -
|
|
66
|
-
<small>Step 6: Customizing Writes</small>
|
|
67
|
-
»
|
|
68
|
-
</a>
|
|
69
|
-
</h2>
|