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,289 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Build Your First API'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Build Your First API
|
|
6
|
+
|
|
7
|
+
By the end of this page you'll have a working Rails API, backed by Graphiti, that supports filtering, sorting, pagination, and nested relationships out of the box.
|
|
8
|
+
|
|
9
|
+
We'll use Rails and ActiveRecord here, on familiar ground. For how the pieces fit together, see [Lifecycle of a Request](/concepts/overview).
|
|
10
|
+
|
|
11
|
+
You'll need Ruby 3.2+ and Rails 7.1+ installed for this walkthrough. Graphiti itself only requires Ruby 3.2+ and ActiveSupport, so you can [use it without Rails](/getting-started/installation#without-rails).
|
|
12
|
+
|
|
13
|
+
## Installation {#installation}
|
|
14
|
+
|
|
15
|
+
Let's start with a classic Rails blog. We'll use a [template](http://guides.rubyonrails.org/rails_application_templates.html) to handle some of the boilerplate. Run this command and accept all the defaults for now:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ rails new blog --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Feel free to run `git diff` if you're interested in the
|
|
22
|
+
particulars. This is mostly installing gems and including modules.
|
|
23
|
+
|
|
24
|
+
> Note: if a network issue prevents you from pointing to this URL
|
|
25
|
+
> directly, you can download the file and and run this command as `-m
|
|
26
|
+
> /path/to/template`
|
|
27
|
+
|
|
28
|
+
Alternatively, you can [**add to an existing project**](/getting-started/installation#adding-to-an-existing-app).
|
|
29
|
+
|
|
30
|
+
## Defining a Resource {#defining-a-resource}
|
|
31
|
+
|
|
32
|
+
A [**Resource**](/concepts/resources) defines how to query and persist your [**Model**](/concepts/backends-and-models). In other
|
|
33
|
+
words: a Model is to the database as Resource is to the API. So
|
|
34
|
+
first, let's define our Model:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
$ bundle exec rails generate model Post title:string upvotes:integer active:boolean
|
|
38
|
+
$ bundle exec rails db:migrate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Now we can use the built-in [generator](/concepts/resources#generators) to define our Resource,
|
|
42
|
+
corresponding [**Endpoint**](/concepts/endpoints), and
|
|
43
|
+
[**Integration Tests**](/topics/testing).
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
$ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You'll see a number of files created. Now run your app!:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
$ bundle exec rails s
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Verify `http://localhost:3000/api/v1/posts` renders JSON correctly.
|
|
56
|
+
Now we need data.
|
|
57
|
+
|
|
58
|
+
##### Seeding Data {#seeding-data}
|
|
59
|
+
|
|
60
|
+
Edit `db/seeds.rb` to create a few `Post`s:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Post.create!(title: 'My title', upvotes: 10, active: true)
|
|
64
|
+
Post.create!(title: 'Another title', upvotes: 20, active: false)
|
|
65
|
+
Post.create!(title: 'OMG! A title', upvotes: 30, active: true)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
And run the script:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
$ bundle exec rails db:seed
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Now load `http://localhost:3000/api/v1/posts`. You should have 3 `Post`s in
|
|
75
|
+
your database.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
<hr />
|
|
80
|
+
|
|
81
|
+
## Querying {#querying}
|
|
82
|
+
|
|
83
|
+
Now that we've defined our Resource and seeded some data, let's see
|
|
84
|
+
what query functionality we have. We've listed all `Post`s at `http://localhost:3000/api/v1/posts`. Let's see what we can do:
|
|
85
|
+
|
|
86
|
+
| What you want | URL |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| Sort by title, ascending | `/api/v1/posts?sort=title` |
|
|
89
|
+
| Sort by title, descending | `/api/v1/posts?sort=-title` |
|
|
90
|
+
| Paginate, 2 per page | `/api/v1/posts?page[size]=2` |
|
|
91
|
+
| Paginate, 2 per page, second page | `/api/v1/posts?page[size]=2&page[number]=2` |
|
|
92
|
+
| Sparse fieldset: only `title` | `/api/v1/posts?fields[posts]=title` |
|
|
93
|
+
| Filter, simple equality | `/api/v1/posts?filter[title]=my title` |
|
|
94
|
+
| Filter, case-insensitive equality | `/api/v1/posts?filter[title][eql]=My title` |
|
|
95
|
+
| Filter, prefix | `/api/v1/posts?filter[title][prefix]=my` |
|
|
96
|
+
| Filter, suffix | `/api/v1/posts?filter[title][suffix]=title` |
|
|
97
|
+
| Filter, contains | `/api/v1/posts?filter[title][match]=itl` |
|
|
98
|
+
| Filter, greater than | `/api/v1/posts?filter[upvotes][gt]=20` |
|
|
99
|
+
| Filter, greater than or equal to | `/api/v1/posts?filter[upvotes][gte]=20` |
|
|
100
|
+
| Filter, less than | `/api/v1/posts?filter[upvotes][lt]=20` |
|
|
101
|
+
| Filter, less than or equal to | `/api/v1/posts?filter[upvotes][lte]=20` |
|
|
102
|
+
|
|
103
|
+
Filtering on an attribute you haven't made filterable raises `Graphiti::Errors::InvalidAttributeAccess`. Filtering on one that doesn't exist raises `Graphiti::Errors::UnknownAttribute`. All filter logic can be customized, and customizations can be packaged into an **Adapter** for reuse. See [Filter](/concepts/resources#filter).
|
|
104
|
+
|
|
105
|
+
### Extra Fields
|
|
106
|
+
|
|
107
|
+
Some fields are expensive enough that you only want to compute them when a client asks. Declare those with `extra_attribute`:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# app/resources/post_resource.rb
|
|
111
|
+
extra_attribute :description, :string do
|
|
112
|
+
@object.active? ? 'Active Post' : 'Inactive Post'
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Request it with `/api/v1/posts?extra_fields[posts]=description`. You can also eager load data conditionally when the field is requested.
|
|
117
|
+
|
|
118
|
+
### Statistics
|
|
119
|
+
|
|
120
|
+
Useful for search grids ("the first 10 active posts, plus the total count of all posts") in a single request. Hit `/api/v1/posts?stats[total]=count` and the result arrives in the `meta` section of the response:
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
Statistics respect your filters, so the count adjusts accordingly. There are several built-in stats and you can [add your own](/concepts/resources#statistics).
|
|
125
|
+
|
|
126
|
+
### Error Handling
|
|
127
|
+
|
|
128
|
+
Your app always renders a JSONAPI-compliant error response. Raise something in the controller:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# app/controllers/posts_controller.rb
|
|
132
|
+
def index
|
|
133
|
+
raise 'foo'
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
and this is what you'd see in production:
|
|
138
|
+
|
|
139
|
+

|
|
140
|
+
|
|
141
|
+
Different errors can be given different response codes, JSON, and side effects. See [Error Handling](/topics/error-handling).
|
|
142
|
+
|
|
143
|
+
## Persisting {#persisting}
|
|
144
|
+
|
|
145
|
+
Resources can Create, Update, and Delete (and you can persist multiple
|
|
146
|
+
Resources in a single request). The best way to observe this behavior is
|
|
147
|
+
to take a look at the tests the generator created. One example:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# spec/api/v1/employees/create_spec.rb
|
|
151
|
+
subject(:make_request) do
|
|
152
|
+
jsonapi_post "/api/v1/employees", payload
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe 'basic create' do
|
|
156
|
+
let(:payload) do
|
|
157
|
+
{
|
|
158
|
+
data: {
|
|
159
|
+
type: 'employees',
|
|
160
|
+
attributes: {
|
|
161
|
+
first_name: 'Jane'
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'works' do
|
|
168
|
+
expect(EmployeeResource).to receive(:build).and_call_original
|
|
169
|
+
expect {
|
|
170
|
+
make_request
|
|
171
|
+
}.to change { Employee.count }.by(1)
|
|
172
|
+
expect(response.status).to eq(201)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Read more about [Persistence](/concepts/persisting) and
|
|
178
|
+
[Testing Persistence](/topics/testing#writes).
|
|
179
|
+
|
|
180
|
+
## Adding Relationships {#adding-relationships}
|
|
181
|
+
|
|
182
|
+
Let’s start by defining our Model:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
$ bundle exec rails g model Comment post_id:integer body:text active:boolean
|
|
186
|
+
$ bundle exec rails db:migrate
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# app/models/post.rb
|
|
191
|
+
has_many :comments
|
|
192
|
+
|
|
193
|
+
# app/models/comment.rb
|
|
194
|
+
belongs_to :post
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
...and corresponding Resource object:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
$ bundle exec rails g graphiti:resource Comment body:string active:boolean created_at:datetime
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Configure the relationship in `PostResource`:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
# app/resources/post_resource.rb
|
|
207
|
+
has_many :comments
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
And allow filtering Comments based on the Post `id`:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# app/resources/comment_resource.rb
|
|
214
|
+
attribute :post_id, :integer, only: [:filterable]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
This code:
|
|
218
|
+
|
|
219
|
+
* Allows eager-loading the relationship.
|
|
220
|
+
* URL: `/api/v1/posts?include=comments`
|
|
221
|
+
* SQL: `SELECT * FROM comments WHERE post_id = 123`
|
|
222
|
+
* Generates a [**Link**](/concepts/links) for
|
|
223
|
+
lazy-loading.
|
|
224
|
+
* Will use `CommentResource` for querying logic (so we can [Deep
|
|
225
|
+
Query](/concepts/relationships#deep-queries), e.g.
|
|
226
|
+
"only return the latest 3 active comments").
|
|
227
|
+
* By default, this will generate the query `CommentResource.all(filter: { post_id: 123 })`, but [relationships can be customized](/concepts/relationships)
|
|
228
|
+
|
|
229
|
+
You should now be able to hit `/api/v1/comments` with all the same
|
|
230
|
+
functionality as before. We need to seed data.
|
|
231
|
+
|
|
232
|
+
#### Seeding Relationships {#seeding-relationships}
|
|
233
|
+
|
|
234
|
+
Start by clearing out your database:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
$ bundle exec rails db:migrate:reset
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Replace your `db/seeds.rb` with this code to persist one `Post` and three `Comment`s:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
comment1 = Comment.new(body: 'comment one', active: true)
|
|
244
|
+
comment2 = Comment.new(body: 'comment two', active: false)
|
|
245
|
+
comment3 = Comment.new(body: 'comment three', active: true)
|
|
246
|
+
|
|
247
|
+
Post.create! \
|
|
248
|
+
title: 'My title!',
|
|
249
|
+
active: true,
|
|
250
|
+
comments: [comment1, comment2, comment3]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
And run it:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
$ bundle exec rails db:seed
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Relationship Usage {#relationship-usage}
|
|
260
|
+
|
|
261
|
+
Now let's fetch a `Post` and filtered `Comment`s in a single request:
|
|
262
|
+
|
|
263
|
+
`/api/v1/posts?include=comments`
|
|
264
|
+
|
|
265
|
+
Any logic in `CommentResource` is available to us. Let's sort the comments by `created_at` descending:
|
|
266
|
+
|
|
267
|
+
`/api/v1/posts?include=comments&sort=-comments.created_at`.
|
|
268
|
+
|
|
269
|
+
Logic from `CommentResource` is accessible at the `/api/v1/comments` endpoint, and reusable when eager-loading Comments at `/api/v1/posts:`
|
|
270
|
+
|
|
271
|
+
* `/api/v1/comments?filter[active]=true`
|
|
272
|
+
* `/api/v1/posts?include=comments&filter[comments.active]=true`
|
|
273
|
+
|
|
274
|
+
This is why Resource objects exist: they provide an interface to
|
|
275
|
+
reuse code across multiple Endpoints.
|
|
276
|
+
|
|
277
|
+
Just as we can query a graph of Resources in a single
|
|
278
|
+
request, we can *persist* a graph of Resources in a single request. See
|
|
279
|
+
[Sideposting](/concepts/persisting#sideposting).
|
|
280
|
+
|
|
281
|
+
## Exploring with Vandal {#exploring-with-vandal}
|
|
282
|
+
|
|
283
|
+
Graphiti ships with Vandal, a UI that introspects your schema for point-and-click data exploration. See the [Vandal Guide](/reference/vandal) to try it against this blog.
|
|
284
|
+
|
|
285
|
+
## Next Steps {#whats-next}
|
|
286
|
+
|
|
287
|
+
* Continue with the [Tutorial](/tutorial) for a deeper walkthrough of customization and relationships.
|
|
288
|
+
* Browse the [Resources guide](/) for the full capability reference.
|
|
289
|
+
* Read the [Testing Guide](/topics/testing) to start testing your API.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Installation'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## From Scratch {#from-scratch}
|
|
6
|
+
|
|
7
|
+
The easiest way to start from scratch is to use the application
|
|
8
|
+
template:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
$ rails new blog --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Alternatively, download and point to the template locally:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
$ curl -O https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
|
|
18
|
+
$ rails new blog --api -m all.rb
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run `git diff` to see the changes to a blank Rails app.
|
|
22
|
+
|
|
23
|
+
## Adding to an Existing App {#adding-to-an-existing-app}
|
|
24
|
+
|
|
25
|
+
This process is straightforward. You can add Graphiti to an existing
|
|
26
|
+
Rails app alongside [JBuilder](https://github.com/rails/jbuilder) or [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers).
|
|
27
|
+
|
|
28
|
+
Start with gems:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# The only strictly-required gem
|
|
32
|
+
gem 'graphiti'
|
|
33
|
+
|
|
34
|
+
# For automatic ActiveRecord pagination
|
|
35
|
+
gem 'kaminari'
|
|
36
|
+
|
|
37
|
+
# Test-specific gems
|
|
38
|
+
group :development, :test do
|
|
39
|
+
gem 'rspec-rails'
|
|
40
|
+
gem 'factory_bot_rails'
|
|
41
|
+
gem 'faker'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
group :test do
|
|
45
|
+
gem 'database_cleaner'
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You'll be up-and-running at this point. Verify with a simple standalone
|
|
50
|
+
Resource:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Assuming you already have a Post ActiveRecord Model
|
|
54
|
+
class PostResource < Graphiti::Resource
|
|
55
|
+
self.adapter = Graphiti::Adapters::ActiveRecord
|
|
56
|
+
attribute :title, :string
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
PostResource.all.data # => [#<Post>, #<Post>, ...]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Now we need to integrate with Rails endpoints (to give us things
|
|
63
|
+
like [#context](/concepts/resources#context)):
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# app/controllers/application_controller.rb
|
|
67
|
+
class ApplicationController < ActionController::Base
|
|
68
|
+
include Graphiti::Rails::Controller
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
And wire-up our error-handling:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# app/controllers/application_controller.rb
|
|
76
|
+
# When #show action does not find record, return 404
|
|
77
|
+
register_exception Graphiti::Errors::RecordNotFound,
|
|
78
|
+
status: 404
|
|
79
|
+
|
|
80
|
+
rescue_from Exception do |e|
|
|
81
|
+
handle_exception(e)
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That's it for the basics. You may have issues with generators
|
|
86
|
+
conflicting with your existing application structure - but you can
|
|
87
|
+
always write files manually or [submit an issue](https://github.com/graphiti-api/graphiti/issues).
|
|
88
|
+
|
|
89
|
+
### Responders {#responders}
|
|
90
|
+
|
|
91
|
+
Graphiti supports JSONAPI, simple JSON, and XML. You can do this
|
|
92
|
+
manually when inheriting from `ActionController::Base`
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
def index
|
|
96
|
+
posts = PostResource.all(params)
|
|
97
|
+
|
|
98
|
+
respond_to do |format|
|
|
99
|
+
format.json { render(json: posts) }
|
|
100
|
+
format.jsonapi { render(jsonapi: posts) }
|
|
101
|
+
format.xml { render(xml: posts) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
But we can inherit from `ActionController::API` while avoiding this
|
|
107
|
+
boilerplate with with the [Responders](https://github.com/plataformatec/responders) gem:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
def index
|
|
111
|
+
posts = PostResource.all(params)
|
|
112
|
+
respond_with(posts)
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
To get this functionality:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Gemfile
|
|
120
|
+
gem 'responders'
|
|
121
|
+
|
|
122
|
+
# app/controllers/application_controller.rb
|
|
123
|
+
include Graphiti::Rails::Responders
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> Note: Persistence operations only support JSONAPI format, so you'll
|
|
127
|
+
> still use `render jsonapi:` and `render jsonapi_errors:` for those.
|
|
128
|
+
|
|
129
|
+
### .graphiticfg.yml {#graphiticfg}
|
|
130
|
+
|
|
131
|
+
The `.graphiticfg.yml` file lives in the root directory of your
|
|
132
|
+
application. It holds configuration we need to reuse across a variety of
|
|
133
|
+
contexts (primarily generates and rake tasks). If you use our template to create your application, it's created for you.
|
|
134
|
+
|
|
135
|
+
Primarily this is used to hold your "API namespace":
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
namespace: /my_api/v1
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If this file doesn't exist you may get unexpected errors - make sure to
|
|
142
|
+
create it!
|
|
143
|
+
|
|
144
|
+
### Testing {#testing}
|
|
145
|
+
|
|
146
|
+
To add our [Integration Tests](/topics/testing):
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# Gemfile
|
|
150
|
+
group :development, :test do
|
|
151
|
+
gem 'factory_bot_rails'
|
|
152
|
+
gem 'rspec_rails'
|
|
153
|
+
gem 'faker'
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
group :test do
|
|
157
|
+
gem 'database_cleaner'
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Bootstrap RSpec if you haven't already:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
$ bin/rails g rspec:install
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Then add the Graphiti spec helpers and database cleaning to your `RSpec.configure` block. See [RSpec Setup](/topics/testing#rspec) in the Testing guide for the config to paste in.
|
|
168
|
+
|
|
169
|
+
### will_paginate {#will-paginate}
|
|
170
|
+
|
|
171
|
+
By default, we use [Kaminari](https://github.com/kaminari/kaminari) for
|
|
172
|
+
ActiveRecord pagination. If you prefer [will_paginate] (or anything
|
|
173
|
+
else):
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# app/resources/application_resource.rb
|
|
177
|
+
paginate do |scope, current_page, per_page|
|
|
178
|
+
scope.paginate(page: current_page, per_page: per_page)
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Without Rails {#without-rails}
|
|
183
|
+
|
|
184
|
+
You can use Graphiti in any plain `.rb` file. To see this in action,
|
|
185
|
+
check out the [Plain Ruby Sample App](https://github.com/graphiti-api/plain_ruby_example).
|