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,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Endpoints'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Overview {#overview}
|
|
6
|
+
|
|
7
|
+
**Endpoints** expose and customize
|
|
8
|
+
[Resources](/concepts/resources).
|
|
9
|
+
|
|
10
|
+
Resources themselves can operate completely independently of a request or response:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
employees = EmployeeResource.all({
|
|
14
|
+
filter: { title: 'engineer' },
|
|
15
|
+
sort: '-created_at',
|
|
16
|
+
page: { size: 10 },
|
|
17
|
+
include: 'positions.department'
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
employees.map(&:first_name) # => ['Jane', 'John', ...]
|
|
21
|
+
employees.to_json # => { employees: [{ ... }] }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
And Resources connect to other Resources. Our graph of data is defined
|
|
25
|
+
**outside** of the actual API.
|
|
26
|
+
|
|
27
|
+
Endpoints expose this graph to the world. We might choose to have a `/employees` endpoint that can eager load comments (`?include=comments`), but never expose `/comments` directly. Or, we could do the opposite: expose lazy-loading `/comments`, but disallow eager loading from `/employees`. We can add caching rules, or add an `/exemplary_employees` endpoint with special query overrides.
|
|
28
|
+
|
|
29
|
+
Finally, Endpoints are in charge of the [HTTP specification](https://tools.ietf.org/html/rfc2616):
|
|
30
|
+
request processing, response codes, caching, MIME types, and so on. If you're thinking
|
|
31
|
+
Rails, an Endpoint is the combination of a Route and Controller.
|
|
32
|
+
|
|
33
|
+
### Endpoint Logic {#endpoint-logic}
|
|
34
|
+
|
|
35
|
+
Often, you won't need to customize Endpoints - especially if you're
|
|
36
|
+
using our [Rails Resource
|
|
37
|
+
generator](/concepts/resources#generators). Endpoint logic mostly
|
|
38
|
+
concerns:
|
|
39
|
+
|
|
40
|
+
* Caching
|
|
41
|
+
* Side-effect behavior specific to the endpoint (e.g.: sending a
|
|
42
|
+
welcome email from `/users#create` but not `/admin/users#create`)
|
|
43
|
+
* Authorization (e.g `before_action`)
|
|
44
|
+
* Custom query parameter handling
|
|
45
|
+
* Validation handling
|
|
46
|
+
* Error handling
|
|
47
|
+
* Limiting Resource behavior
|
|
48
|
+
* Customizing Resource behavior
|
|
49
|
+
|
|
50
|
+
If your logic falls elsewhere, consider a Resource or Model.
|
|
51
|
+
|
|
52
|
+
### Rails Integration {#rails-integration}
|
|
53
|
+
|
|
54
|
+
When using Rails, an endpoint is the combination of a Route and
|
|
55
|
+
Controller:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# config/routes.rb
|
|
59
|
+
resources :posts, only: [:index]
|
|
60
|
+
|
|
61
|
+
# app/controllers/posts_controller.rb
|
|
62
|
+
class PostsController < ApplicationController
|
|
63
|
+
def index
|
|
64
|
+
posts = PostResource.all(params)
|
|
65
|
+
respond_with(posts)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
You'll note that Graphiti hooks into Rails with a mixin (set when using
|
|
71
|
+
our application generator):
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class ApplicationController < ActionController::API
|
|
75
|
+
include Graphiti::Rails::Controller
|
|
76
|
+
|
|
77
|
+
# ... code ...
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This gives us [#sideload_allowlist](#sideload-allowlist) and sets the
|
|
82
|
+
[context](/concepts/resources#context).
|
|
83
|
+
|
|
84
|
+
## Customizing Resources {#customizing-resources}
|
|
85
|
+
|
|
86
|
+
### Scope Overrides {#scope-overrides}
|
|
87
|
+
|
|
88
|
+
One common use case for endpoints is customizing the Resource
|
|
89
|
+
[base scope](/concepts/resources#base-scope). This causes a new
|
|
90
|
+
"starting point" for query building.
|
|
91
|
+
|
|
92
|
+
Consider the endpoints `/posts` (basic CRUD) and `/top_posts`. Though both are associated to PostResource, `/top_posts` ensures that only
|
|
93
|
+
Posts with a certain number of upvotes get returned:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
def index
|
|
97
|
+
base_scope = Post.where("upvotes > ?", 100)
|
|
98
|
+
posts = PostResource.all(params, base_scope)
|
|
99
|
+
respond_with(posts)
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
We're able to reuse all the other logic in PostResource - relationships,
|
|
104
|
+
filters, sorts, etc - while only returning "Top Posts".
|
|
105
|
+
|
|
106
|
+
### Sideload Allowlist {#sideload-allowlist}
|
|
107
|
+
|
|
108
|
+
Resources define relationships to other resources. But we may not want
|
|
109
|
+
all of those relationships exposed at a given endpoint.
|
|
110
|
+
|
|
111
|
+
Let's say we've defined relationships:
|
|
112
|
+
|
|
113
|
+
`Employee > Position > Department > Hardware > CostHistory`
|
|
114
|
+
|
|
115
|
+
It's reasonable to get an Employee, their Positions, and Departments for
|
|
116
|
+
those positions in a single request. But is it really valid to *also* pull down
|
|
117
|
+
all the hardware, as well as all the historical data on the cost of that hardware,
|
|
118
|
+
in a single request? Allowing the entire graph to be pulled down in a single request can cause excessive load on our
|
|
119
|
+
servers (and this is probably a better fit for lazy-loading via
|
|
120
|
+
[Links](/concepts/links)).
|
|
121
|
+
|
|
122
|
+
Let's instead say that if we're entering the graph at `/employees`, the
|
|
123
|
+
furthest we can go is Department:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
class EmployeesController < ApplicationController
|
|
127
|
+
self.sideload_allowlist = {
|
|
128
|
+
index: { positions: 'department' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# ... code ...
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Caching {#caching}
|
|
136
|
+
|
|
137
|
+
### Etags {#etags}
|
|
138
|
+
|
|
139
|
+
[ETags](https://robots.thoughtbot.com/introduction-to-conditional-http-caching-with-rails) are an important concept that is often overlooked. Etags tell browsers
|
|
140
|
+
that the response to a GET request hasn't changed since the last request and
|
|
141
|
+
can be safely pulled from the browser cache. If you care about sparse fieldsets,
|
|
142
|
+
you should care about ETags - if you're limiting fields to reduce payload size,
|
|
143
|
+
how about a payload size of **zero**?
|
|
144
|
+
|
|
145
|
+
ETags are set by default in Rails, by checking the response body. This won't prevent queries from executing, but it will save clients from downloading the response again if nothing has changed.
|
|
146
|
+
|
|
147
|
+
Let's manually set an ETag:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
def index
|
|
151
|
+
posts = PostResource.all(params)
|
|
152
|
+
|
|
153
|
+
if stale?(posts.data)
|
|
154
|
+
respond_with(posts)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
From the [documentation on #stale?](https://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F):
|
|
160
|
+
|
|
161
|
+
> *In this case last_modified will be set by calling `maximum(:updated_at)` on the collection (the timestamp of the most recently updated record) and the etag by passing the object itself.*
|
|
162
|
+
|
|
163
|
+
Also consider the use case where data is ingested hourly. We can avoid a
|
|
164
|
+
query altogether by checking when the last ingestion ran:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
def index
|
|
168
|
+
if stale?(EmployeeIngestion.last)
|
|
169
|
+
employees = EmployeeResource.all(params)
|
|
170
|
+
respond_with(employees)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
> **CAVEAT**: When setting ETags, consider sideloads. In the above examples
|
|
176
|
+
> we are checking to see the last update of an Employee, but we may be
|
|
177
|
+
> sideloading (and filtering) Positions as well. Use custom endpoints or
|
|
178
|
+
> [Sideload Allowlist](#sideload-allowlist) to mitigate this issue.
|
|
179
|
+
|
|
180
|
+
## Testing {#testing}
|
|
181
|
+
|
|
182
|
+
If you have custom Endpoint logic, we suggest testing using an [API
|
|
183
|
+
Test](/topics/testing#api-tests).
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Links'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Links
|
|
6
|
+
|
|
7
|
+
## Overview {#overview}
|
|
8
|
+
|
|
9
|
+
A [Link](http://jsonapi.org/format/#document-links) is a URL Graphiti puts in a relationship, pointing at the data so a client can fetch it separately. Every relationship gets one automatically:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class PostResource < ApplicationResource
|
|
13
|
+
has_many :comments
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`GET /posts/123` renders the `comments` relationship with a `links.related` of `/comments?filter[post_id]=123`. The client follows that URL when it wants the comments, rather than asking for them up front with `?include=comments`.
|
|
18
|
+
|
|
19
|
+
### Why links {#why-links}
|
|
20
|
+
|
|
21
|
+
The URL matters most when the relationship means something more specific than "all the comments". Say `top_comments` is defined as 100 upvotes or more. A [`params` block](#linking-relationships) puts that into the generated Link, and the client still follows a URL.
|
|
22
|
+
|
|
23
|
+
The alternative is for clients to build that query themselves, which means every client (desktop, mobile, third-party) has to know what a "Top Comment" is and ship an update whenever the definition changes. Hiding it behind a dedicated `/top_comments` endpoint moves the problem rather than solving it: clients still have to know to hit a special endpoint, and nothing keeps its definition in sync with the eager-loaded one.
|
|
24
|
+
|
|
25
|
+
With a Link, the definition lives in one place. Change it to 500 upvotes, factor in recency, subtract downvotes: clients keep following the same URL.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Linking Relationships {#linking-relationships}
|
|
29
|
+
|
|
30
|
+
When defining a relationship, we get a Link for free:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
class PostResource < ApplicationResource
|
|
34
|
+
has_many :comments
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> `/comments?filter[post_id]=123`
|
|
39
|
+
|
|
40
|
+
And when customizing a relationship with `params`, our Link will be
|
|
41
|
+
updated:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
has_many :comments do
|
|
45
|
+
params do |hash|
|
|
46
|
+
hash[:filter][:upvotes] = { gte: 100 }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> `/comments?filter[post_id]=123&filter[upvotes][gte]=100`
|
|
52
|
+
|
|
53
|
+
Note: if you use the `scope` block directly, it may cause incorrect links. Avoid using `scope` directly and instead use `params` and `pre_load` if possible.
|
|
54
|
+
|
|
55
|
+
To manually generate a Link:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
has_many :comments do
|
|
59
|
+
link do |post|
|
|
60
|
+
helpers = Rails.application.routes.url_helpers
|
|
61
|
+
helpers.comments_url(params: { filter: { post_id: post.id } })
|
|
62
|
+
# or
|
|
63
|
+
# http://example.com/api/v1/comments?filter[post_id]=123
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
To avoid a Relationship Link altogether:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
has_many :comments, link: false
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Resource Endpoints {#resource-endpoints}
|
|
75
|
+
|
|
76
|
+
To generate links, we need to associate a Resource to a URL. By default,
|
|
77
|
+
this happens automatically:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
class ApplicationResource < Graphiti::Resource
|
|
81
|
+
# ... code ...
|
|
82
|
+
self.endpoint_namespace = '/api/v1'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class PostResource < ApplicationResource
|
|
86
|
+
# under the hood:
|
|
87
|
+
primary_endpoint 'posts',
|
|
88
|
+
[:index, :show, :create, :update, :destroy]
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Which would generate links to `/api/v1/posts`.
|
|
93
|
+
|
|
94
|
+
### Validation {#validation}
|
|
95
|
+
|
|
96
|
+
Associating a Resource to an Endpoint serves two purposes. We've gone
|
|
97
|
+
over link generation. But we also want to make sure we're not linking to
|
|
98
|
+
something that doesn't actually exist. That's why we perform **Endpoint
|
|
99
|
+
Validation**.
|
|
100
|
+
|
|
101
|
+
If we tried to access the above resource at a `/comments` endpoint:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
class CommentsController < ApplicationController
|
|
105
|
+
def index
|
|
106
|
+
PostResource.all(params)
|
|
107
|
+
# ...
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
We'd get a `Graphiti::Errors::InvalidEndpoint` error. Endpoint
|
|
113
|
+
validation ensures that our auto-generated Links are actually valid.
|
|
114
|
+
|
|
115
|
+
To change the endpoint associated to a Resource:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
primary_endpoint 'special_posts', [:index, :show]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Or to alter only the **path**:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
self.endpoint[:path] = 'special_posts'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or to alter only the **actions** supported:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
self.endpoint[:actions] = [:index, :show]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A resource may be accessible by multiple endpoints. Maybe `PostResource` is also used at `/top_posts`. We want to keep all auto-generated links pointing to `/posts` (the primary endpoint), but *allow* accessing `PostResource` from the `/top_posts` endpoint:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
secondary_endpoint '/top_posts', [:index]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Configuration {#configuration}
|
|
140
|
+
|
|
141
|
+
### Autolinking {#autolinking}
|
|
142
|
+
|
|
143
|
+
To turn off automatically generated links:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
class ApplicationResource < Graphiti::Resource
|
|
147
|
+
self.autolink = false
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Endpoint Validation {#endpoint-validation}
|
|
152
|
+
|
|
153
|
+
To turn off Endpoint Validation:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
class ApplicationResource < Graphiti::Resource
|
|
157
|
+
self.validate_endpoints = false
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Links-on-Demand {#links-on-demand}
|
|
162
|
+
|
|
163
|
+
To only render links when requested in the URL with `?links=true`:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
Graphiti.configure do |c|
|
|
167
|
+
c.links_on_demand = true
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Pagination Links {#pagination-links}
|
|
172
|
+
|
|
173
|
+
Requesting big collections can result into slow responses sometimes. In order to avoid this, you could use [pagination](https://jsonapi.org/format/#fetching-pagination). It'll break your response into smaller pieces that will make your server responds faster. Paginations links can be present in your response in the following ways:
|
|
174
|
+
|
|
175
|
+
#### Showing by default {#pagination-links-showing-by-default}
|
|
176
|
+
|
|
177
|
+
With this configuration, all the responses will return the pagination links
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
Graphiti.configure do |c|
|
|
181
|
+
c.pagination_links = true
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### When requested {#pagination-links-when-requested}
|
|
186
|
+
|
|
187
|
+
You can showing the pagination links when it was requested in the URL with `?pagination_links=true`
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
Graphiti.configure do |c|
|
|
191
|
+
c.pagination_links_on_demand = true
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Pagination links won't show up for *#show* actions.
|
|
196
|
+
|
|
197
|
+
### Custom Endpoint URLs {#custom-endpoint-urls}
|
|
198
|
+
|
|
199
|
+
To change the URL associated with a Resource:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
class PostResource < ApplicationResource
|
|
203
|
+
# Most commonly seen in ApplicationResource
|
|
204
|
+
self.endpoint_namespace = '/api/v1'
|
|
205
|
+
|
|
206
|
+
primary_endpoint '/posts', [:index, :show]
|
|
207
|
+
# OR
|
|
208
|
+
self.endpoint[:path] = '/posts'
|
|
209
|
+
# OR
|
|
210
|
+
self.endpoint[:actions] = [:index, :show]
|
|
211
|
+
end
|
|
212
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Lifecycle of a Request'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Lifecycle of a Request
|
|
6
|
+
|
|
7
|
+
A request goes down through a Resource to your data, and comes back up as a serialized response.
|
|
8
|
+
|
|
9
|
+
<figure>
|
|
10
|
+
<svg viewBox="0 0 790 300" role="img" aria-label="A request reaches an Endpoint (your Rails controller), then a Resource. The Resource builds a scope from the request, queries your Backend through an Adapter, resolves the results into Models, and serializes them into the JSON:API response that goes back to the client. The Resource and the JSON:API response are Graphiti. The Endpoint and Backend are yours." style={{width: '100%', height: 'auto'}}>
|
|
11
|
+
<defs>
|
|
12
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
13
|
+
<path d="M0 0 L10 5 L0 10 z" fill="var(--ifm-color-emphasis-500)"/>
|
|
14
|
+
</marker>
|
|
15
|
+
<marker id="arrowStart" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
16
|
+
<path d="M10 0 L0 5 L10 10 z" fill="var(--ifm-color-emphasis-500)"/>
|
|
17
|
+
</marker>
|
|
18
|
+
</defs>
|
|
19
|
+
|
|
20
|
+
<g fill="none" stroke="var(--ifm-color-emphasis-500)" strokeWidth="1.5">
|
|
21
|
+
<path d="M150 64 H176" markerEnd="url(#arrow)"/>
|
|
22
|
+
<path d="M310 64 H336" markerEnd="url(#arrow)"/>
|
|
23
|
+
<path d="M574 96 H626" markerEnd="url(#arrow)" markerStart="url(#arrowStart)"/>
|
|
24
|
+
<path d="M336 230 H156" markerEnd="url(#arrow)"/>
|
|
25
|
+
<path d="M85 206 V92" markerEnd="url(#arrow)"/>
|
|
26
|
+
</g>
|
|
27
|
+
|
|
28
|
+
<g stroke="var(--ifm-color-emphasis-300)" fill="var(--ifm-background-surface-color)">
|
|
29
|
+
<rect x="20" y="40" width="130" height="48" rx="6"/>
|
|
30
|
+
<rect x="180" y="40" width="130" height="48" rx="6"/>
|
|
31
|
+
<rect x="630" y="70" width="140" height="52" rx="6"/>
|
|
32
|
+
</g>
|
|
33
|
+
<g stroke="var(--ifm-color-primary)" strokeWidth="1.5" fill="none">
|
|
34
|
+
<rect x="340" y="30" width="230" height="230" rx="8"/>
|
|
35
|
+
<rect x="20" y="206" width="130" height="48" rx="6"/>
|
|
36
|
+
</g>
|
|
37
|
+
|
|
38
|
+
<g fontFamily="var(--ifm-font-family-base)" fontSize="15" fill="var(--ifm-font-color-base)" textAnchor="middle">
|
|
39
|
+
<text x="85" y="70">Request</text>
|
|
40
|
+
<text x="245" y="70">Endpoint</text>
|
|
41
|
+
<text x="700" y="102">Backend</text>
|
|
42
|
+
<text x="85" y="236">JSON:API</text>
|
|
43
|
+
</g>
|
|
44
|
+
<g fontFamily="var(--ifm-font-family-base)" fontSize="15" fill="var(--ifm-color-primary)" textAnchor="middle">
|
|
45
|
+
<text x="455" y="62">Resource</text>
|
|
46
|
+
</g>
|
|
47
|
+
|
|
48
|
+
<g fontFamily="var(--ifm-font-family-base)" fontSize="12.5" fill="var(--ifm-color-emphasis-700)" textAnchor="middle">
|
|
49
|
+
<text x="455" y="104">base_scope + filters,</text>
|
|
50
|
+
<text x="455" y="122">sorts, pagination</text>
|
|
51
|
+
<text x="455" y="163">resolve(scope)</text>
|
|
52
|
+
<text x="455" y="181">to your Models</text>
|
|
53
|
+
<text x="455" y="222">serialize</text>
|
|
54
|
+
<text x="600" y="86">Adapter</text>
|
|
55
|
+
</g>
|
|
56
|
+
<g fontFamily="var(--ifm-font-family-base)" fontSize="12.5" fill="var(--ifm-color-emphasis-700)">
|
|
57
|
+
<text x="95" y="152">response</text>
|
|
58
|
+
</g>
|
|
59
|
+
</svg>
|
|
60
|
+
</figure>
|
|
61
|
+
|
|
62
|
+
Graphiti is the highlighted part: the Resource, and the JSON:API it renders. The Endpoint is your Rails controller, which handles routing, response codes and MIME types. The Backend is yours too.
|
|
63
|
+
|
|
64
|
+
| Piece | What it does |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| [Endpoint](/concepts/endpoints) | Your controller. Graphiti registers its path and actions, which drives link generation and endpoint validation, and lets you vary a Resource's behavior per route. |
|
|
67
|
+
| [Resource](/concepts/resources) | Turns request params into a **scope**, resolves that scope into Models, and serializes them on the way back out. |
|
|
68
|
+
| Adapter | Reusable glue between a Resource and a Backend. Defaults to `Graphiti::Adapters::ActiveRecord`. |
|
|
69
|
+
| [Backend](/concepts/backends-and-models) | Whatever you query: a database, a search index, an HTTP service. |
|
|
70
|
+
| [Model](/concepts/backends-and-models) | What you return and serialize. With ActiveRecord, the same object as the Backend. |
|
|
71
|
+
|
|
72
|
+
## The graph
|
|
73
|
+
|
|
74
|
+
Resources connect to other Resources:
|
|
75
|
+
|
|
76
|
+
* **Sideloading**: fetch an employee, her positions, and those positions' departments in one request
|
|
77
|
+
* **Sideposting**: *save* an employee and her positions in one request
|
|
78
|
+
* **[Links](/concepts/links)**: a URL to lazy-load positions in a separate request
|
|
79
|
+
|
|
80
|
+
Query logic written for one Resource applies at every level of that graph, so you can ask for an employee and her last three positions ordered by `created_at`. That's [deep querying](/concepts/relationships#deep-queries).
|