graphiti 2.0.0.beta.7 → 2.0.0.beta.8

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