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,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Customizing Sideloads'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Customizing Sideloads
|
|
6
|
+
> [See the code in our sample app](https://github.com/graphiti-api/employee_directory/commit/e5dbb24b7e5853a9f39aed455a5d318d303df37e)
|
|
7
|
+
|
|
8
|
+
This cookbook will help you understand sideloading. It would be great to
|
|
9
|
+
live in a world where everything follows default ActiveRecord table
|
|
10
|
+
conventions, but in my experience this is rarely the case. From legacy
|
|
11
|
+
code to alternate datastores, we need to think in Real World terms.
|
|
12
|
+
|
|
13
|
+
Our [Employee Directory](https://github.com/graphiti-api/employee_directory) sample application
|
|
14
|
+
has a clean schema - let's screw with it. Let's say `Department` has a column called `watcher_emails`, which is an array of strings. We want to sideload `Department > Watchers`. Though the *relationship* is called `watchers`, these will be `Employee` records.
|
|
15
|
+
|
|
16
|
+
Let's start by adding a spec:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# spec/resources/department/reads_spec.rb
|
|
20
|
+
|
|
21
|
+
describe 'sideloading' do
|
|
22
|
+
describe 'watchers' do
|
|
23
|
+
let!(:employee1) { create(:employee) }
|
|
24
|
+
let!(:employee2) { create(:employee) }
|
|
25
|
+
let!(:employee3) { create(:employee) }
|
|
26
|
+
let!(:department) do
|
|
27
|
+
create :department,
|
|
28
|
+
watcher_emails: [employee1.email, employee3.email]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
before do
|
|
32
|
+
params[:include] = 'watchers'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'sideloads employees via watcher_emails' do
|
|
36
|
+
render
|
|
37
|
+
sl = d[0].sideload(:watchers)
|
|
38
|
+
expect(sl.map(&:id)).to eq([employee1.id, employee3.id])
|
|
39
|
+
expect(sl.map(&:jsonapi_type).uniq).to eq(['employees'])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Add the relationship:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# app/resources/department_resource.rb
|
|
49
|
+
has_many :watchers, resource: EmployeeResource
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Run the test and you'll get this error:
|
|
53
|
+
|
|
54
|
+
```error
|
|
55
|
+
Graphiti::Errors::AttributeError:
|
|
56
|
+
EmployeeResource: Tried to filter on attribute :department_id, but could not find an attribute with that name.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
How would we track down this error? Well, we know Resources connect
|
|
60
|
+
together with [Links](/concepts/links). Let's
|
|
61
|
+
take a look at the query parameters that would be used to connect these
|
|
62
|
+
two Resources:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
has_many :watchers, resource: EmployeeResource do
|
|
66
|
+
params do |hash, departments|
|
|
67
|
+
binding.pry
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> Note - we're using [pry](https://github.com/pry/pry) to debug here.
|
|
73
|
+
|
|
74
|
+
The value of `hash` here is:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
{ filter: { department_id: "1" } }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Which makes sense. If we say `has_many :things`, by default we expect `Thing` to have a `department_id` we can query.
|
|
81
|
+
|
|
82
|
+
That's not our case, though. Instead, let's customize those parameters
|
|
83
|
+
to fit our use case:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
params do |hash, departments|
|
|
87
|
+
emails = departments.map(&:watcher_emails).flatten
|
|
88
|
+
hash[:filter] = { email: emails }
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Instead of querying by `department_id`, we need to query by `email`. And
|
|
93
|
+
the value we pass in will be an array of email addresses
|
|
94
|
+
|
|
95
|
+
We'd need to add an `email` filter to `EmployeeResource` to make this
|
|
96
|
+
work. This gets us ***querying*** correctly, but there's another error:
|
|
97
|
+
|
|
98
|
+
```error
|
|
99
|
+
undefined method `department_id' for #<Employee:0x00007f9652ae6d80>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Here's the thing to keep in mind: let's say our request was
|
|
103
|
+
`/departments?include=watchers`. We queried all the data, and we now have an array of `Department`s and an array of `Employee`s. Now we need
|
|
104
|
+
to specify which employees should be assigned as watchers of which
|
|
105
|
+
department.
|
|
106
|
+
|
|
107
|
+
Let's write that code manually:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
has_many :watchers, resource: EmployeeResource do
|
|
111
|
+
# ... code ...
|
|
112
|
+
assign do |departments, employees|
|
|
113
|
+
departments.each do |d|
|
|
114
|
+
d.watchers = employees.select do |e|
|
|
115
|
+
e.email.in?(d.watcher_emails)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
We're selecting all relevant `Employee`s for a given `Department` by checking the array of `watcher_emails`.
|
|
123
|
+
|
|
124
|
+
This code can be tightened up a little with `assign_each` (recommended).
|
|
125
|
+
This way we don't have to iterate departments or worry about the
|
|
126
|
+
assignment ourselves:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
has_many :watchers, resource: EmployeeResource do
|
|
130
|
+
# ... code ...
|
|
131
|
+
|
|
132
|
+
assign_each do |department, employees|
|
|
133
|
+
employees.select { |e| e.email.in?(d.watcher_emails) }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
We're using `#select` to return an array of relevant `Employee`s. If this was a `belongs_to` or `has_one` relationship, we'd probably want to use `#find` to return a single `Employee`.
|
|
139
|
+
|
|
140
|
+
OK there's *one last error*:
|
|
141
|
+
|
|
142
|
+
```error
|
|
143
|
+
undefined method `watchers=' for #<Department:0x00007feb625a7468>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This one is simple - the `assign` function will call your Adapter's assignment logic, which by default will be a simple `department.watchers
|
|
147
|
+
= relevant_employees`. That means we need to add a getter/setter for
|
|
148
|
+
this property:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# app/models/department.rb
|
|
152
|
+
attr_accessor :watchers
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
And we're done! The test should now pass. [Check out the working code
|
|
156
|
+
here](https://github.com/graphiti-api/employee_directory/tree/customize_sideloads_cookbook).
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Debugging'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Debugger {#debugger}
|
|
6
|
+
|
|
7
|
+
Graphiti comes with a debugger that shows the queries executed for a
|
|
8
|
+
given request. Remember that Resources [have a query interface](/concepts/resources#query-interface) independent of a request or response. And Resources connect similar to ActiveRecord's `includes`:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
employees = EmployeeResource.all
|
|
12
|
+
PositionResource.all(filter: { employee_id: employees.map(&:id) })
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> *Remember, this is all [customizable](/concepts/relationships#customizing-relationships)*.
|
|
16
|
+
|
|
17
|
+
That means we can log the requests made by individual Resources:
|
|
18
|
+
|
|
19
|
+
`/api/v1/employees?include=notes,positions.department.teams`
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img width="100%" src="/assets/img/legacy/legacy-a2cc4363c3.png" />
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
And even copy/paste these queries into a console session to debug:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
$ bin/rails c
|
|
28
|
+
>> TeamResource.all({:filter=>{:department_id=>"1,2,3"}})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If you're having trouble with a request, see if you can isolate to a
|
|
32
|
+
specific Resource, then test that Resource directly.
|
|
33
|
+
|
|
34
|
+
Finally: if an error occurs, we'll note the query that caused it:
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img width="100%" src="/assets/img/legacy/legacy-05bbd3e5fd.png" />
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
### JSON Output {#json-output}
|
|
41
|
+
|
|
42
|
+
It can be helpful to have this debug output come back as part of the
|
|
43
|
+
JSON response. To enable this:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# app/controllers/application_controller.rb
|
|
47
|
+
def allow_graphiti_debug_json?
|
|
48
|
+
true
|
|
49
|
+
# or, current_user.admin?
|
|
50
|
+
# or, Rails.env.development?
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
And request the debug output:
|
|
55
|
+
|
|
56
|
+
`/your/url?debug=true`
|
|
57
|
+
|
|
58
|
+
You should now see the debug output in `meta`:
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<img width="100%" src="/assets/img/legacy/legacy-7f6889bc89.png" />
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<br />
|
|
65
|
+
|
|
66
|
+
If there's an error, and you've [enabled raw errors](/topics/error-handling#displaying-raw-errors), you'll also see the query that caused the error in the JSON response:
|
|
67
|
+
|
|
68
|
+
<br />
|
|
69
|
+
|
|
70
|
+
<p align="center">
|
|
71
|
+
<img width="100%" src="/assets/img/legacy/legacy-f67cfa89ab.png" />
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
<br />
|
|
75
|
+
|
|
76
|
+
### Configuration {#configuration}
|
|
77
|
+
|
|
78
|
+
By default, we'll log to `Rails.logger`, and only enable debugging (logs or JSON) when `Rails.logger.level` is set to `debug`. Here are the
|
|
79
|
+
various ways to configure.
|
|
80
|
+
|
|
81
|
+
Use `config.debug` to explicitly toggle debugging:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# config/initializers/graphiti.rb
|
|
85
|
+
Graphiti.configure do |c|
|
|
86
|
+
c.debug = false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Or use environment variable
|
|
90
|
+
# GRAPHITI_DEBUG=false
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Use `config.debug_models` to get additional (but verbose) output:
|
|
94
|
+
|
|
95
|
+
<p align="center">
|
|
96
|
+
<img width="100%" src="/assets/img/legacy/legacy-3076df6209.png" />
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# config/initializers/graphiti.rb
|
|
101
|
+
Graphiti.configure do |c|
|
|
102
|
+
c.debug_models = true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Or use environment variable
|
|
106
|
+
# GRAPHITI_DEBUG_MODELS=true
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
As noted above, `allow_graphiti_debug_json?` must return `true` if you
|
|
110
|
+
want JSON output:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# app/controllers/application_controller.rb
|
|
114
|
+
def allow_graphiti_debug_json?
|
|
115
|
+
true
|
|
116
|
+
# or, current_user.admin?
|
|
117
|
+
# or, Rails.env.development?
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Note you need to explicitly pass `?debug=true` in the request.
|
|
122
|
+
|
|
123
|
+
Assign a different logger:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Graphiti.logger = Logger.new(...)
|
|
127
|
+
|
|
128
|
+
# Or the built-in STDOUT logger:
|
|
129
|
+
Graphiti.logger = Graphiti.stdout_logger
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Manually apply the debugging (when using Rails, this normally happens in
|
|
133
|
+
a `around_action`):
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
Graphiti::Debugger.debug do
|
|
137
|
+
EmployeeResource.all
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Rake Tasks {#rake-tasks}
|
|
142
|
+
|
|
143
|
+
There are some common debugging scenarios that are possible to do
|
|
144
|
+
manually, but their frequency warrants common patterns. For these, we
|
|
145
|
+
have rake tasks.
|
|
146
|
+
|
|
147
|
+
#### graphiti:request {#graphiti-request}
|
|
148
|
+
|
|
149
|
+
> `bin/rake graphiti:request[PATH,DEBUG]`
|
|
150
|
+
|
|
151
|
+
Execute a request using `ActionDispatch::Integration::Session` (which
|
|
152
|
+
underlies request specs).
|
|
153
|
+
|
|
154
|
+
This can be helpful when you don't have, or don't want to spin up, a web
|
|
155
|
+
server. Imagine you want to debug something on production, so you shell
|
|
156
|
+
into a docker container and edit some files locally. Now you want to
|
|
157
|
+
execute a request and see if your changes worked:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
$ bin/rake graphiti:request[/employees]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Will execute the request and spit out the JSON response. You may want to
|
|
164
|
+
run with the Debugger enabled:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
$ bin/rake graphiti:request[/employees,true]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Which add Debugger output as well.
|
|
171
|
+
|
|
172
|
+
The `PATH` should not contain the domain unless you want to hit a live
|
|
173
|
+
API instead of a test server.
|
|
174
|
+
|
|
175
|
+
#### graphiti:benchmark {#graphiti-benchmark}
|
|
176
|
+
|
|
177
|
+
> `bin/rake graphiti:benchmark[PATH,NUM_REQUESTS]`
|
|
178
|
+
|
|
179
|
+
It can be helpful to run a quick benchmark without hitting a live web
|
|
180
|
+
server, to eliminate the vagaries of latency. To do this:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
$ bin/rake graphiti:benchmark[/employees,100]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Which will return the average response time.
|
|
187
|
+
|
|
188
|
+
#### Authorization headers {#Authorization-headers}
|
|
189
|
+
|
|
190
|
+
If you have an Authorization scheme implemented (for example [authenticate_or_request_with_http_token](https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html) in rails) you can supply the `Authorization` http header value with the `AUTHORIZATION_HEADER` environment variable:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
$ export AUTHORIZATION_HEADER="Token --PRIVATE_API_KEY--"
|
|
194
|
+
$ bin/rake graphiti:request[/employees,true]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
This also will work for `Basic` ([request_http_basic_authentication](https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html$$)) and `Bearer` values
|
|
198
|
+
|
|
199
|
+
## Tips {#tips}
|
|
200
|
+
|
|
201
|
+
When debugging an application, try to isolate the individual Resource
|
|
202
|
+
call and debug the Resource directly (instead of running the entire
|
|
203
|
+
request). This helps eliminate variables, and plain ruby code is easier
|
|
204
|
+
to work with. If possible, try to remove Graphiti entirely and focus on
|
|
205
|
+
your Models and Backends.
|
|
206
|
+
|
|
207
|
+
The most common scenario is debugging a query. We suggest overriding
|
|
208
|
+
`resolve` and using [pry](https://github.com/pry/pry) (or equivalent):
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
# Introspect the scope without firing a query
|
|
212
|
+
# Call 'super' to fire the query
|
|
213
|
+
def resolve(scope)
|
|
214
|
+
binding.pry
|
|
215
|
+
end
|
|
216
|
+
```
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Error Handling'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Overview {#overview}
|
|
6
|
+
|
|
7
|
+
Whenever we have an application error, we want to respond with a
|
|
8
|
+
[JSONAPI-compliant errors payload](http://jsonapi.org/format/#errors).
|
|
9
|
+
This way clients have a predictable response detailing information about
|
|
10
|
+
the error.
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"errors": [
|
|
15
|
+
{
|
|
16
|
+
"code": "internal_server_error",
|
|
17
|
+
"status": "500",
|
|
18
|
+
"title": "Internal Server Error",
|
|
19
|
+
"detail": "We've notified our engineers and hope to address this issue shortly."
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
We'll also need a way to customize this payload. For instance, if a
|
|
26
|
+
`NotAuthorized` error is raised, the response should have a `403` status
|
|
27
|
+
code. For other errors, we may want to render a helpful error message:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
class ApplicationController < ActionController::API
|
|
31
|
+
register_exception NotAuthorized, status: 403
|
|
32
|
+
register_exception ShipmentDelayed,
|
|
33
|
+
detail: ->(e) { "Contact us at 123-456-7899" }
|
|
34
|
+
# ... code ...
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Exception handling lives in Graphiti's Rails integration. Customizing the behavior based on error class happens in the [RescueRegistry](https://github.com/wagenet/rescue_registry) dependency.
|
|
39
|
+
|
|
40
|
+
### Setup {#setup}
|
|
41
|
+
|
|
42
|
+
Include the Rails integration in the controllers serving your resources:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class ApplicationController < ActionController::Base
|
|
46
|
+
include Graphiti::Rails::Controller
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That registers handlers for Graphiti's own exceptions and renders anything else as JSON:API. `register_exception` itself is available on every controller without it. See below.
|
|
51
|
+
|
|
52
|
+
#### Displaying Raw Errors {#displaying-raw-errors}
|
|
53
|
+
|
|
54
|
+
When raw errors are on, the same payload carries the underlying exception under `meta.__raw_error__`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"errors": [
|
|
59
|
+
{
|
|
60
|
+
"code": "internal_server_error",
|
|
61
|
+
"status": "500",
|
|
62
|
+
"title": "Internal Server Error",
|
|
63
|
+
"detail": "We've notified our engineers and hope to address this issue shortly.",
|
|
64
|
+
"meta": {
|
|
65
|
+
"__raw_error__": {
|
|
66
|
+
"message": "EmployeesController::SomeError",
|
|
67
|
+
"backtrace": [
|
|
68
|
+
"app/controllers/employees_controller.rb:5:in `index'",
|
|
69
|
+
"..."
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
It can be useful to display the raw error as part of the JSON response -
|
|
80
|
+
but you probably don't want to expose your stack trace to customers.
|
|
81
|
+
Let's only show raw errors for the `staging` environment:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class ApplicationController < ActionController::API
|
|
85
|
+
# ... code ...
|
|
86
|
+
|
|
87
|
+
def show_detailed_exceptions?
|
|
88
|
+
Rails.env.staging?
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Another common pattern is to only show raw errors when the user is
|
|
94
|
+
privileged to see them:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class ApplicationController < ActionController::API
|
|
98
|
+
# ... code ...
|
|
99
|
+
|
|
100
|
+
def show_detailed_exceptions?
|
|
101
|
+
current_user.admin?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
When `#show_detailed_exceptions?` returns `true`, you'll get the raw error class,
|
|
108
|
+
message, and backtrace in the JSON response.
|
|
109
|
+
|
|
110
|
+
## Usage {#usage}
|
|
111
|
+
|
|
112
|
+
### Basic {#basic}
|
|
113
|
+
|
|
114
|
+
Let's register an error with a custom response code:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
register_exception Errors::NotAuthorized, status: 403
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Now if we `raise Errors::NotAuthorized`, the response code will be `403`.
|
|
121
|
+
|
|
122
|
+
Additional options:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
register_exception Errors::NotAuthorized,
|
|
126
|
+
status: 403,
|
|
127
|
+
title: "You cannot perform this action",
|
|
128
|
+
detail: :exception, # render the raw error message
|
|
129
|
+
detail: ->(error) { "Invalid Action" } # message via proc
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
[See full documentation in the RescueRegistry README](https://github.com/wagenet/rescue_registry).
|
|
133
|
+
|
|
134
|
+
All controllers will inherit any registered exceptions from their parent. They can also add their own. In this example, `FooError` will only throw a custom status code when thrown from `FooController`:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
class FooController < ApplicationController
|
|
138
|
+
register_exception FooError, status: 422
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Advanced {#advanced}
|
|
143
|
+
|
|
144
|
+
The final option `register_exception` accepts is `handler`. Here you can inject your own error handling class that customize `RescueRegistry::ExceptionHandler`. For example:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
class MyCustomHandler < Graphiti::Rails::ExceptionHandler
|
|
148
|
+
# self.exception accessible within all instance methods
|
|
149
|
+
|
|
150
|
+
def status_code
|
|
151
|
+
# ...customize...
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def error_code
|
|
155
|
+
# ...customize...
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def title
|
|
159
|
+
# ...customize...
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def detail
|
|
163
|
+
# ...customize...
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def meta
|
|
167
|
+
# ...customize...
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
register_exception FooError, handler: MyCustomHandler
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If you would like to use the same custom handler for all errors, override `default_exception_handler`:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# app/controllers/application_controller.rb
|
|
178
|
+
def self.default_exception_handler
|
|
179
|
+
MyCustomHandler
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Testing {#testing}
|
|
184
|
+
|
|
185
|
+
This pattern of globally rescuing exceptions makes sense when
|
|
186
|
+
running our live application...but during testing, we may want to
|
|
187
|
+
raise real errors and bypass this rescue logic.
|
|
188
|
+
|
|
189
|
+
This is why we turn off error-handling during tests by default:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# spec/rails_helper.rb
|
|
193
|
+
RSpec.configure do |config|
|
|
194
|
+
config.include Graphiti::Rails::TestHelpers
|
|
195
|
+
# ... code ...
|
|
196
|
+
|
|
197
|
+
config.before :each do
|
|
198
|
+
handle_request_exceptions(false)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
If you want to turn this on for an individual test (so you can test
|
|
204
|
+
error codes, etc):
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
before do
|
|
208
|
+
handle_request_exceptions(true)
|
|
209
|
+
end
|
|
210
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'ETags'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# ETags
|
|
6
|
+
|
|
7
|
+
Every resource proxy can compute a weak ETag for its current result set via `proxy.etag`. It's a plain string. Graphiti doesn't wire up `If-None-Match` handling or send `304 Not Modified` responses itself, so using it for HTTP conditional requests is up to your controller (for example, with Rails' own `fresh_when`/`stale?`).
|
|
8
|
+
|
|
9
|
+
## How it's computed
|
|
10
|
+
|
|
11
|
+
`etag` is a weak ETag built from the same cache key used for [caching](/topics/caching), but the *versioned* one:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
def etag
|
|
15
|
+
"W/#{ActiveSupport::Digest.hexdigest(cache_key_with_version.to_s)}"
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`cache_key_with_version` combines the scope's versioned cache key (which folds in every sideloaded proxy's versioned cache key and the underlying object's own `cache_key_with_version`), the query's cache key, and the resource cache tag if one is configured. Those are the same three ingredients described in the caching doc, except the scope portion here is version-aware rather than the plain identity-only key. In practice this means the ETag changes whenever the resolved records' `updated_at` values change, or whenever the rendering-relevant query params (fields, extra_fields, links, pagination_links, format) change.
|
|
20
|
+
|
|
21
|
+
Because it's derived purely from `cache_key_with_version`, calling `etag` twice on equivalent proxies (same resource, scope, and query) produces the same weak ETag, and it's always prefixed with `W/`.
|
|
22
|
+
|
|
23
|
+
## Using it
|
|
24
|
+
|
|
25
|
+
Since there's no built-in controller integration, you compute and use it explicitly:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
def index
|
|
29
|
+
employees = EmployeeResource.all(params)
|
|
30
|
+
response.headers["ETag"] = employees.etag
|
|
31
|
+
render jsonapi: employees
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or combine it with Rails' conditional-GET support if you want automatic `304` handling:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
def index
|
|
39
|
+
employees = EmployeeResource.all(params)
|
|
40
|
+
fresh_when(etag: employees.etag)
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Relationship to resource-level caching
|
|
45
|
+
|
|
46
|
+
`etag` doesn't require `cache_resource` or `Graphiti.config.cache_rendering = true`. It's available on any resource proxy regardless of whether that resource participates in rendering caching. It does, however, share its key ingredients with the cache-rendering machinery: the same `cache_key_with_version` that ETags are hashed from is also what `Graphiti::Util::CacheDebug` tracks (as `current_version[:etag]` / `last_version[:etag]`) when the [debugger](/topics/debugging) reports on cache-key changes for a cached resource. So if you're seeing an ETag change unexpectedly, the debugger's cache section (enabled the same way as for [caching](/topics/caching)) will show you which cache-key segment changed.
|