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,324 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Usage Without ActiveRecord'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Usage Without ActiveRecord
|
|
6
|
+
Graphiti was built to be used with any ORM or datastore, from PostgreSQL
|
|
7
|
+
to elasticsearch to `Net::HTTP`. In fact, Graphiti itself is tested with
|
|
8
|
+
Plain Old Ruby Objects (POROs).
|
|
9
|
+
|
|
10
|
+
This cookbook will show how to customize a resource around a particular datastore, and how to package those
|
|
11
|
+
customizations into a reusable adapter. We'll use an in-memory datastore
|
|
12
|
+
and Plain Old Ruby Objects (POROs) here, but the lessons apply to any
|
|
13
|
+
datastore.
|
|
14
|
+
|
|
15
|
+
For working code, see [this branch of the sample application](https://github.com/graphiti-api/employee_directory/blob/poro/app/resources/post_resource.rb).
|
|
16
|
+
|
|
17
|
+
We'll start with this PORO model:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
class Post
|
|
21
|
+
# Define getters/setters
|
|
22
|
+
# e.g. post.title = 'foo'
|
|
23
|
+
ATTRS = [:id, :title]
|
|
24
|
+
ATTRS.each { |a| attr_accessor(a) }
|
|
25
|
+
|
|
26
|
+
# Instantiate with hash of attributes
|
|
27
|
+
# e.g. Post.new(title: 'foo')
|
|
28
|
+
def initialize(attrs = {})
|
|
29
|
+
attrs.each_pair { |k,v| send(:"#{k}=", v) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# This part only needed for our particular
|
|
33
|
+
# persistence implementation; you may not need it
|
|
34
|
+
# e.g. post.attributes # => { title: 'foo' }
|
|
35
|
+
def attributes
|
|
36
|
+
{}.tap do |attrs|
|
|
37
|
+
ATTRS.each do |name|
|
|
38
|
+
attrs[name] = send(name)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
And this in-memory datastore:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# If we were working with more than just Posts, we'd need a 'type'
|
|
49
|
+
# field here as well, to simulate a table name.
|
|
50
|
+
DATA = [
|
|
51
|
+
{ id: 1, title: 'Graphiti' },
|
|
52
|
+
{ id: 2, title: 'is' },
|
|
53
|
+
{ id: 3, title: 'super' },
|
|
54
|
+
{ id: 4, title: 'dope' }
|
|
55
|
+
]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Resource Overrides {#resource-overrides}
|
|
59
|
+
|
|
60
|
+
If it's your first time with a new ORM or datastore, we recommend
|
|
61
|
+
putting the logic in the Resource first. Once things are working *and*
|
|
62
|
+
there are multiple uses of the same overrides, package them into an
|
|
63
|
+
Adapter.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class PostResource < ApplicationResource
|
|
67
|
+
self.adapter = Graphiti::Adapters::Null
|
|
68
|
+
|
|
69
|
+
attribute :title, :string
|
|
70
|
+
|
|
71
|
+
def base_scope
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def resolve(scope)
|
|
76
|
+
DATA.map { |d| Post.new(d) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Here we're using the `Null` adapter, which acts as a dumb pass-through.
|
|
82
|
+
This can be helpful when you just want to get running for a simple use
|
|
83
|
+
case and don't want errors around features you haven't implemented yet.
|
|
84
|
+
But it can also be confusing when you expect certain codepaths to
|
|
85
|
+
be hit. Mostly just be aware of `Null`'s behavior, or use `Graphiti::Adapters::Abstract` to get helpful errors around what's not
|
|
86
|
+
implemented.
|
|
87
|
+
|
|
88
|
+
We're also supplying an explicit `base_scope`. This is the beginning
|
|
89
|
+
query object we'll modify as params come in. In the case of
|
|
90
|
+
ActiveRecord, we might want an `ActiveRecord::Relation` like `Post.all`. For our example, we'll modify a simple ruby hash (keep in
|
|
91
|
+
mind the premise of building a hash of options and passing it off to a
|
|
92
|
+
client can apply to any datastore).
|
|
93
|
+
|
|
94
|
+
Finally, we're [resolving that scope](/concepts/resources#resolve),
|
|
95
|
+
returning the full dataset for now. The contract of `#resolve` is to return an array of model instances, hence `DATA.map { |d| Post.new(d)
|
|
96
|
+
}`.
|
|
97
|
+
|
|
98
|
+
#### Sorting {#sorting}
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
sort_all do |scope, attribute, direction|
|
|
102
|
+
scope[:sort].merge!(attribute: att, direction: dir)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def base_scope
|
|
106
|
+
{ sort: {} }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def resolve(scope)
|
|
110
|
+
if sort = scope[:sort].presence
|
|
111
|
+
data = DATA.sort_by { |d| d[sort[:attribute].to_sym] }
|
|
112
|
+
data = data.reverse if sort[:direction] == :desc
|
|
113
|
+
end
|
|
114
|
+
DATA.map { |d| Post.new(d) }
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
We modified the base scope with a default hash key, `:sort`. When the
|
|
119
|
+
user requests sorting, we record this by merging into the hash. We can
|
|
120
|
+
then reference that information on the scope when resolving.
|
|
121
|
+
|
|
122
|
+
Note the `sort_all` scope block, in fact all scope blocks, must return the scope.
|
|
123
|
+
|
|
124
|
+
#### Paginating {#paginating}
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
paginate do |scope, current_page, per_page|
|
|
128
|
+
scope.merge!(current_page: current, per_page: per)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def resolve(scope)
|
|
132
|
+
# ... sorting ...
|
|
133
|
+
start = (scope[:current_page] - 1) * scope[:per_page]
|
|
134
|
+
stop = start + scope[:per_page]
|
|
135
|
+
data = data[start...stop]
|
|
136
|
+
# ... return models ...
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Again: merge into the scope, then reference the scope data when
|
|
141
|
+
resolving.
|
|
142
|
+
|
|
143
|
+
#### Filtering {#filtering}
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
filter :title, only: [:eq] do
|
|
147
|
+
eq do |scope, value|
|
|
148
|
+
scope[:filters][attribute] = value
|
|
149
|
+
scope
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def base_scope(*)
|
|
154
|
+
{ sort: {}, filters: {} }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def resolve(scope)
|
|
158
|
+
# ... sorting ...
|
|
159
|
+
scope[:filters].each_pair do |k, v|
|
|
160
|
+
data = data.select { |d| d[k.to_sym].in?(v) }
|
|
161
|
+
end
|
|
162
|
+
# ... pagination ...
|
|
163
|
+
# ... return models ...
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Same as above examples. Again, we must return the scope object
|
|
168
|
+
from the filter function.
|
|
169
|
+
|
|
170
|
+
#### Persisting {#persisting}
|
|
171
|
+
|
|
172
|
+
All at once:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# Instantiate a model for #create
|
|
176
|
+
def build(model_class)
|
|
177
|
+
model_class.new
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Used for create/update
|
|
181
|
+
def assign_attributes(model, attributes)
|
|
182
|
+
attributes.each_pair do |k, v|
|
|
183
|
+
model.send(:"#{k}=", v)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Used for create/update
|
|
188
|
+
def save(model)
|
|
189
|
+
attrs = model.attributes.dup
|
|
190
|
+
attrs[:id] ||= DATA.length + 1
|
|
191
|
+
if existing = DATA.find { |d| d[:id].to_s == attrs[:id].to_s }
|
|
192
|
+
existing.merge!(attrs)
|
|
193
|
+
else
|
|
194
|
+
DATA << attrs
|
|
195
|
+
end
|
|
196
|
+
model
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Used for destroy
|
|
200
|
+
def delete(model)
|
|
201
|
+
DATA.reject! { |d| d[:id].to_s == model.id.to_s }
|
|
202
|
+
model
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
These are the overrides for persistence operations. You are encouraged
|
|
207
|
+
**not** to override `create/update/destroy` directly and instead use
|
|
208
|
+
[Persistence Lifecycle Hooks](/concepts/persisting#persistence-lifecycle-hooks).
|
|
209
|
+
|
|
210
|
+
## Adapters {#adapters}
|
|
211
|
+
|
|
212
|
+
OK so we have all our read and write operations working correctly. But
|
|
213
|
+
if we had multiple Resources all using an in-memory datastore, you'd see
|
|
214
|
+
this logic repeated all over the place. Let's create an adapter to [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)
|
|
215
|
+
up this logic.
|
|
216
|
+
|
|
217
|
+
There isn't much more to do than copy/paste what we've already done.
|
|
218
|
+
Let's start with our `base_scope`, sorting, and pagination:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
class POROAdapter < Graphiti::Adapters::Abstract
|
|
222
|
+
def base_scope(*)
|
|
223
|
+
{ sort: {}, filters: {} }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def paginate(scope, current, per)
|
|
227
|
+
scope.merge!(current_page: current, per_page: per)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def order(scope, att, dir)
|
|
231
|
+
scope[:sort].merge!(attribute: att, direction: dir)
|
|
232
|
+
scope
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def resolve(scope)
|
|
236
|
+
data = DATA
|
|
237
|
+
if sort = scope[:sort].presence
|
|
238
|
+
data = data.sort_by { |d| d[sort[:attribute].to_sym] }
|
|
239
|
+
data = data.reverse if sort[:direction] == :desc
|
|
240
|
+
end
|
|
241
|
+
start = (scope[:current_page] - 1) * scope[:per_page]
|
|
242
|
+
stop = start + scope[:per_page]
|
|
243
|
+
data = data[start...stop]
|
|
244
|
+
|
|
245
|
+
data.map { |d| resource.model.new(d) }
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
There's really nothing here we haven't seen before. We're taking the
|
|
251
|
+
code we originally wrote, and sticking it into the interface defined by
|
|
252
|
+
`Graphiti::Adapters::Abstract`.
|
|
253
|
+
|
|
254
|
+
There's a *little* more to do with filtering:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
def filter(scope, attribute, value)
|
|
258
|
+
scope[:filters][attribute] = value
|
|
259
|
+
scope
|
|
260
|
+
end
|
|
261
|
+
alias :filter_string_eq :filter
|
|
262
|
+
alias :filter_integer_eq :filter
|
|
263
|
+
alias :filter_date_eq :filter
|
|
264
|
+
# ... etc ...
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The logic is the same, but we have a separate method for each filter
|
|
268
|
+
operator. This allows us to query differently based on the type - for
|
|
269
|
+
instance, ActiveRecord will default to case-insensitive for strings, but
|
|
270
|
+
straight equality for integers. If you don't need operator-specific
|
|
271
|
+
logic, just `alias` as you see here.
|
|
272
|
+
|
|
273
|
+
You may want to limit the default operators we expect to work with a
|
|
274
|
+
given type. Let's say your backend allows straight equality for strings,
|
|
275
|
+
but doesn't support `prefix`, `suffix`, etc. You can specify this in
|
|
276
|
+
your adapter:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
def self.default_operators
|
|
280
|
+
super.tap do |built_in|
|
|
281
|
+
built_in[:string] = [:eq]
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# or avoid super altogether
|
|
286
|
+
|
|
287
|
+
def self.default_operators
|
|
288
|
+
{
|
|
289
|
+
string: [:eq],
|
|
290
|
+
integer: [:eq]
|
|
291
|
+
# ... etc ...
|
|
292
|
+
}
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**That's it for reads**. For writes, I'll post the entire adapter code
|
|
297
|
+
below - again, it's just copy/pasting what we already wrote into a
|
|
298
|
+
slightly different format.
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
def destroy(model)
|
|
302
|
+
Post::DATA.reject! { |d| d[:id].to_s == model.id.to_s }
|
|
303
|
+
model
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def save(model)
|
|
307
|
+
attrs = model.attributes.dup
|
|
308
|
+
attrs[:id] ||= Post::DATA.length + 1
|
|
309
|
+
if existing = Post::DATA.find { |d| d[:id].to_s == attrs[:id].to_s }
|
|
310
|
+
existing.merge!(attrs)
|
|
311
|
+
else
|
|
312
|
+
Post::DATA << attrs
|
|
313
|
+
end
|
|
314
|
+
model
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# For wrapping persistence operations in a DB transactions
|
|
318
|
+
# Our in-memory DB doesn't have transactions, so just yield
|
|
319
|
+
def transaction(*)
|
|
320
|
+
yield
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
That's really it. [See the working code in Employee Directory here](https://github.com/graphiti-api/employee_directory/blob/poro/app/resources/post_resource.rb).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Tutorial'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="/assets/img/legacy/legacy-0c75a16b3a.gif" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
# Tutorial
|
|
10
|
+
This tutorial serves as a deeper-dive into Graphiti development,
|
|
11
|
+
building an Employee Directory application. We purposefully built this
|
|
12
|
+
to illustrate common - but non-trivial - scenarios present in many
|
|
13
|
+
applications.
|
|
14
|
+
|
|
15
|
+
You'll need Ruby 3.2+ and Rails 7.1+ installed. Step 0 starts from an empty directory, so nothing else is assumed.
|
|
16
|
+
|
|
17
|
+
A core concept of Graphiti is **Test-First** - the most pleasant way to
|
|
18
|
+
develop Graphiti is by starting with an [integration test](/topics/testing). But that can add a lot of noise to a tutorial like this. Though we'll occasionally touch on testing - and the git diffs at the top of each section contain the necessary tests - we won't test first for the purposes of this tutorial.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Server Side: Rails
|
|
22
|
+
|
|
23
|
+
[Rails Sample Application](https://github.com/graphiti-api/employee_directory)
|
|
24
|
+
|
|
25
|
+
* [Step 0: Bootstrapping](/tutorial/step_0)
|
|
26
|
+
* [Step 1: Initial Resource](/tutorial/step_1)
|
|
27
|
+
* [Step 2: Has Many](/tutorial/step_2)
|
|
28
|
+
* [Step 3: Belongs To](/tutorial/step_3)
|
|
29
|
+
* [Step 4: Customizing Queries](/tutorial/step_4)
|
|
30
|
+
* [Step 5: Has One](/tutorial/step_5)
|
|
31
|
+
* [Step 6: Customizing Writes](/tutorial/step_6)
|
|
32
|
+
* [Step 7: Many-to-Many](/tutorial/step_7)
|
|
33
|
+
* [Step 8: Polymorphic
|
|
34
|
+
Relationships](/tutorial/step_8)
|
|
35
|
+
* [Step 9: Polymorphic Resources](/tutorial/step_9)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Client Side: VueJS (diff-only)
|
|
40
|
+
|
|
41
|
+
[VueJS Sample Application](https://github.com/graphiti-api/employee-directory-vue)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
* [Step 0: Setup](https://github.com/graphiti-api/employee-directory-vue/commit/be690c3038380e17e326935d595a0b83fc8004f9)
|
|
45
|
+
* Run after `vue create employee-directory-vue` using [Vue CLI](https://cli.vuejs.org).
|
|
46
|
+
* [Step 1: Define Models](https://github.com/graphiti-api/employee-directory-vue/compare/step_0_setup...step_1_models)
|
|
47
|
+
* [Step 2: Data Grid](https://github.com/graphiti-api/employee-directory-vue/compare/step_1_models...step_2_data_grid)
|
|
48
|
+
* [Step 3: Relationships](https://github.com/graphiti-api/employee-directory-vue/compare/step_2_data_grid...step_3_includes)
|
|
49
|
+
* [Step 4: Filtering](https://github.com/graphiti-api/employee-directory-vue/compare/step_3_includes...step_4_filtering)
|
|
50
|
+
* [Step 5: Sorting](https://github.com/graphiti-api/employee-directory-vue/compare/step_4_filtering...step_5_sorting)
|
|
51
|
+
* [Step 6: Total Count](https://github.com/graphiti-api/employee-directory-vue/compare/step_5_sorting...step_6_stats)
|
|
52
|
+
* [Step 7: Pagination](https://github.com/graphiti-api/employee-directory-vue/compare/step_6_stats...step_7_pagination)
|
|
53
|
+
* [Step 8: Basic Form Setup](https://github.com/graphiti-api/employee-directory-vue/compare/step_7_pagination...step_8_basic_form_setup)
|
|
54
|
+
* [Step 9: Dropdown](https://github.com/graphiti-api/employee-directory-vue/compare/step_8_basic_form_setup...step_9_dropdown)
|
|
55
|
+
* [Step 10: Nested Form Submission](https://github.com/graphiti-api/employee-directory-vue/compare/step_9_dropdown...step_10_nested_create)
|
|
56
|
+
* [Step 11: Validation Errors](https://github.com/graphiti-api/employee-directory-vue/compare/step_10_nested_create...step_11_validations)
|
|
57
|
+
* [Step 12: Nested Destroy](https://github.com/graphiti-api/employee-directory-vue/compare/step_11_validations...step_12_nested_destroy)
|
|
58
|
+
* [Step 13: Vue-Specific Glue Code](https://github.com/graphiti-api/employee-directory-vue/compare/step_12_nested_destroy...step_13_vue)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Step 0'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Step 0: Bootstrapping
|
|
6
|
+
|
|
7
|
+
> [View the Code](https://github.com/graphiti-api/employee_directory/commit/e2552ce212c68b41a3eb8161deb822fff3e159d6)
|
|
8
|
+
|
|
9
|
+
Let's start by creating a new Rails project. For help with an existing
|
|
10
|
+
project, check out [Installation: From
|
|
11
|
+
Scratch](/getting-started/installation).
|
|
12
|
+
|
|
13
|
+
We'll use the `-m` option to install from a template, which will add a few gems and apply some setup boilerplate. Accept all the default options.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
$ rails new employee_directory --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
|
|
17
|
+
$ cd employee_directory
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> Note: if a network issue prevents you from pointing to this URL directly, you can download the file and and run this command as `-m /path/to/template`
|
|
21
|
+
|
|
22
|
+
Feel free to run `git diff` to see what the generator did, otherwise commit the result. You can now head to [Step 1: Basic Resource](/tutorial/step_1), or continue reading to better understand the code.
|
|
23
|
+
|
|
24
|
+
#### Digging Deeper 🧐
|
|
25
|
+
|
|
26
|
+
You'll see some boilerplate in `config/routes.rb`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
|
|
30
|
+
# your routes go here
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This tells Rails that our API routes will be be prefixed - `/api/v1` by default. It also says that if no extension is in the URL (`.json`, `.xml`, etc), default
|
|
35
|
+
to the [JSONAPI Specification](http://jsonapi.org).
|
|
36
|
+
|
|
37
|
+
Let's look at the above `ApplicationResource`:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
class ApplicationResource < Graphiti::Resource
|
|
41
|
+
self.abstract_class = true
|
|
42
|
+
|
|
43
|
+
# We'll be using ActiveRecord
|
|
44
|
+
self.adapter = Graphiti::Adapters::ActiveRecord
|
|
45
|
+
|
|
46
|
+
# Links are generated from base_url + endpoint_namespace
|
|
47
|
+
self.base_url = Rails.application.routes
|
|
48
|
+
.default_url_options[:host]
|
|
49
|
+
self.endpoint_namespace = '/api/v1'
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This should be pretty self-explanatory except for
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
self.base_url = Rails.application.routes
|
|
57
|
+
.default_url_options[:host]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This is configured in `config/application.rb`:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
module EmployeeDirectory
|
|
64
|
+
class Application < Rails::Application
|
|
65
|
+
routes.default_url_options[:host] = ENV.fetch('HOST', 'http://localhost:3000')
|
|
66
|
+
# ... code ...
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
When deriving and validating [Links](/concepts/links), we'll use the `HOST` variable if
|
|
72
|
+
present, falling back to the Rails development default of
|
|
73
|
+
`http://localhost:3000`. This means our Links will look like:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
"#{ENV['HOST']}/#{ApplicationRecord.endpoint_namespace}/#{Resource.type}"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For example:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
http://my-website.com/api/v1/employees
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Read more in the [Links Guide](/concepts/links).
|
|
86
|
+
|
|
87
|
+
Finally, there's some boilerplate in `ApplicationController`:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class ApplicationController < ActionController::API
|
|
91
|
+
# Support JSON, XML, JSON:API
|
|
92
|
+
include Graphiti::Rails::Responders
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This gets `respond_with` working, via the [Responders gem](https://github.com/plataformatec/responders). To render simple nested JSON like default Rails, we'll only need to add `.json` to the URL.
|
|
97
|
+
|
|
98
|
+
That's it for basic setup!
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
<h2 id="next">
|
|
102
|
+
<a href="/tutorial/step_1">
|
|
103
|
+
NEXT -
|
|
104
|
+
<small>Step 1: Basic Resource</small>
|
|
105
|
+
»
|
|
106
|
+
</a>
|
|
107
|
+
</h2>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Step 1'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Step 1: Basic Resource
|
|
6
|
+
|
|
7
|
+
> [View the Diff](https://github.com/graphiti-api/employee_directory/commit/45c1c92e14fb1c3a47b8ed246ceb2cba50e97c72)
|
|
8
|
+
|
|
9
|
+
We'll be working with a single database table, `employees`:
|
|
10
|
+
|
|
11
|
+
<table class="table table-small">
|
|
12
|
+
<thead>
|
|
13
|
+
<tr>
|
|
14
|
+
<th>id</th>
|
|
15
|
+
<th>first_name</th>
|
|
16
|
+
<th>last_name</th>
|
|
17
|
+
<th>age</th>
|
|
18
|
+
<th>created_at</th>
|
|
19
|
+
<th>updated_at</th>
|
|
20
|
+
</tr>
|
|
21
|
+
</thead>
|
|
22
|
+
<tbody>
|
|
23
|
+
<tr>
|
|
24
|
+
<td>1</td>
|
|
25
|
+
<td>Homer</td>
|
|
26
|
+
<td>Simpson</td>
|
|
27
|
+
<td>39</td>
|
|
28
|
+
<td>2018-09-04</td>
|
|
29
|
+
<td>2018-09-04</td>
|
|
30
|
+
</tr>
|
|
31
|
+
<tr>
|
|
32
|
+
<td>2</td>
|
|
33
|
+
<td>Waylon</td>
|
|
34
|
+
<td>Smithers</td>
|
|
35
|
+
<td>65</td>
|
|
36
|
+
<td>2018-09-04</td>
|
|
37
|
+
<td>2018-09-04</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<tr>
|
|
40
|
+
<td>3</td>
|
|
41
|
+
<td>Monty</td>
|
|
42
|
+
<td>Burns</td>
|
|
43
|
+
<td>123</td>
|
|
44
|
+
<td>2018-09-04</td>
|
|
45
|
+
<td>2018-09-04</td>
|
|
46
|
+
</tr>
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
|
|
50
|
+
### The Rails Stuff 🚂
|
|
51
|
+
|
|
52
|
+
Use the built-in generator to create the database table
|
|
53
|
+
and corresponding `ActiveRecord` model:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
$ bin/rails g model Employee first_name:string last_name:string age:integer
|
|
57
|
+
$ bin/rails db:migrate
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Now let's seed some random development data, using [Faker](https://github.com/stympy/faker) (which was installed in [Step 0](/tutorial/step_0)):
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# db/seeds.rb
|
|
64
|
+
Employee.delete_all # Ensure the DB is cleaned each run
|
|
65
|
+
|
|
66
|
+
100.times do
|
|
67
|
+
Employee.create! first_name: Faker::Name.first_name,
|
|
68
|
+
last_name: Faker::Name.last_name,
|
|
69
|
+
age: rand(20..80)
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Run this seed file with
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
$ bin/rails db:seed
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### The Graphiti Stuff 🎨
|
|
80
|
+
|
|
81
|
+
Just like Rails, Graphiti has built-in generators. Let's generate
|
|
82
|
+
the corresponding Resource for our `Employee` model:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
$ bin/rails g graphiti:resource Employee first_name:string last_name:string age:integer created_at:datetime updated_at:datetime
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This generated a few things, but for now let's focus on
|
|
89
|
+
`EmployeeResource`:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
class EmployeeResource < ApplicationResource
|
|
93
|
+
attribute :first_name, :string
|
|
94
|
+
attribute :last_name, :string
|
|
95
|
+
attribute :age, :integer
|
|
96
|
+
attribute :created_at, :datetime, writable: false
|
|
97
|
+
attribute :updated_at, :datetime, writable: false
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This code defined the [RESTful Resource](https://restful-api-design.readthedocs.io/en/latest/resources.html) we want our API to expose. Let's run our server and see what it does:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
$ bin/rails s
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Visit `localhost:3000/api/v1/employees`. You should see a [JSONAPI Response](http://jsonapi.org):
|
|
108
|
+
|
|
109
|
+
<br />
|
|
110
|
+
|
|
111
|
+

|
|
112
|
+
|
|
113
|
+
<br />
|
|
114
|
+
|
|
115
|
+
If you find the payload a little intimidating, add `.json` to the URL for a more traditional response, or `.xml` for XML. Both are different **renderings** of the same `EmployeeResource`.
|
|
116
|
+
|
|
117
|
+
`Resources` are comprised of `Attribute`s:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# app/resources/employee_resource.rb
|
|
121
|
+
attribute :first_name, :string
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Each attribute defines behavior for:
|
|
125
|
+
|
|
126
|
+
* Reading (display)
|
|
127
|
+
* Writing
|
|
128
|
+
* Sorting
|
|
129
|
+
* Filtering
|
|
130
|
+
* Fieldsets
|
|
131
|
+
|
|
132
|
+
Let's start with simple display, turning `first_name` into all capital
|
|
133
|
+
letters:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# app/resources/employee_resource.rb
|
|
137
|
+
attribute :first_name, :string do
|
|
138
|
+
# @object is your model instance
|
|
139
|
+
@object.first_name.upcase
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This is the most important thing to understand about Resources: they are just a collection of defaults, all of which can be overridden. `attribute :first_name` is shorthand for `attribute :first_name do @object.first_name end`.
|
|
144
|
+
|
|
145
|
+
We'll go into further Resource customizations over the course of this tutorial. For now, undo the capitalization change above, and verify our out-of-the-box defaults: the same filter, sort, and pagination capabilities you exercised in the [Quickstart](/getting-started/first-api#querying) work here too, just against `employees` instead of `posts`. See the [Overview guide](/concepts/overview) for the full capability reference.
|
|
146
|
+
|
|
147
|
+
Write operations are easiest to verify with integration tests, which were created when we generated our Resource: an **API Spec** covering the request/response cycle, and a **Resource Spec** covering the Resource's logic directly. See the [Testing Guide](/topics/testing) for what these look like and how they differ. The example there uses the same `create` payload shape the generator produced for `EmployeeResource`.
|
|
148
|
+
|
|
149
|
+
Before we run these specs, we need to edit our [factories](https://github.com/thoughtbot/factory_bot) to ensure
|
|
150
|
+
dynamic, randomized data. Let's change this:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# spec/factories/employee.rb
|
|
154
|
+
|
|
155
|
+
FactoryBot.define do
|
|
156
|
+
factory :employee do
|
|
157
|
+
first_name { "MyString" }
|
|
158
|
+
last_name { "MyString" }
|
|
159
|
+
age { 1 }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
To
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# spec/factories/employee.rb
|
|
168
|
+
|
|
169
|
+
FactoryBot.define do
|
|
170
|
+
factory :employee do
|
|
171
|
+
first_name { Faker::Name.first_name }
|
|
172
|
+
last_name { Faker::Name.last_name }
|
|
173
|
+
age { rand(20..80) }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Now run the generated specs:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
$ bundle exec rspec
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
You'll see 11 tests pass, with 3 pending. One of the pending specs was
|
|
185
|
+
autogenerated by rails - you can delete `spec/models/employee_spec.rb`
|
|
186
|
+
for now.
|
|
187
|
+
|
|
188
|
+
That leaves us with two "update" specs. These are marked pending so you
|
|
189
|
+
can manage the data yourself. Follow the comments in these specs to add
|
|
190
|
+
attributes and get them passing.
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
<h2 id="next">
|
|
194
|
+
<a href="/tutorial/step_2">
|
|
195
|
+
NEXT -
|
|
196
|
+
<small>Step 2: Has Many</small>
|
|
197
|
+
»
|
|
198
|
+
</a>
|
|
199
|
+
</h2>
|