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,677 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Resources'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Resources
|
|
6
|
+
|
|
7
|
+
A Resource is an abstraction around an API endpoint, the way a Model is an abstraction around a database table. It holds the logic for **querying**, **persisting**, and **serializing** one kind of thing.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class EmployeeResource < ApplicationResource
|
|
11
|
+
attribute :first_name, :string
|
|
12
|
+
attribute :age, :integer
|
|
13
|
+
|
|
14
|
+
has_many :positions
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This page is the full reference. For the whole API on one screen, see the [cheatsheet on the home page](/). For how a request flows through a Resource, see [Lifecycle of a Request](/concepts/overview).
|
|
19
|
+
|
|
20
|
+
Resources connect to each other. That's covered separately in [Relationships](/concepts/relationships), and writes in [Persisting](/concepts/persisting).
|
|
21
|
+
|
|
22
|
+
## Attributes {#attributes}
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
attribute :first_name, :string
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A **name** (`first_name`) maps to a JSON key. A **Type** (`string`) maps to a JSON value and its coercion rules.
|
|
29
|
+
|
|
30
|
+
### Limiting Behavior {#limiting-behavior}
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
attribute :name, :string,
|
|
34
|
+
readable: true, # renders in responses
|
|
35
|
+
writable: true, # accepted on create/update
|
|
36
|
+
sortable: true, # ?sort=name works
|
|
37
|
+
filterable: true, # ?filter[name]=... works
|
|
38
|
+
schema: true # exported to schema.json, not affected by only/except
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Turn any flag off directly, or with `only`/`except` shorthand:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
attribute :name, :string, sortable: false
|
|
45
|
+
attribute :name, :string, only: [:sortable]
|
|
46
|
+
attribute :name, :string, except: [:writable]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Guards.** `readable` and `writable` also accept a symbol, string, or proc. The behavior applies only when the guard returns `true`, and the guard's arity decides what it receives:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
attribute :name, :string, writable: :admin?
|
|
53
|
+
attribute :salary, :integer, readable: :visible?, writable: :salary_writable?
|
|
54
|
+
|
|
55
|
+
def admin? # no arguments
|
|
56
|
+
context.current_user.admin?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def visible?(model) # the model
|
|
60
|
+
model.internal == false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def salary_writable?(model, attribute_name) # the model and the attribute name
|
|
64
|
+
PolicyChecker.new(model).attribute_writable?(attribute_name)
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The model is only looked up when a guard declares a parameter for it, so zero-argument guards cost nothing. On an update it's the persisted record. On a create, it's a new unsaved instance.
|
|
69
|
+
|
|
70
|
+
| Guard returns `false` on | Result |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `readable` | The attribute is omitted from the response. |
|
|
73
|
+
| `writable` | The request is rejected with an `unwritable_attribute` validation error, before anything is persisted. |
|
|
74
|
+
|
|
75
|
+
### Default Behavior {#default-behavior}
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# On ApplicationResource, affects every subclass
|
|
79
|
+
self.attributes_readable_by_default = false # default true
|
|
80
|
+
self.attributes_writable_by_default = false # default true
|
|
81
|
+
self.attributes_filterable_by_default = false # default true
|
|
82
|
+
self.attributes_sortable_by_default = false # default true
|
|
83
|
+
self.attributes_schema_by_default = false # default true
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each `*_by_default` setting can also be a guard symbol, delegating the check to a method. Useful for wiring every attribute through one authorization system:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
self.attributes_readable_by_default = :attribute_readable?
|
|
90
|
+
|
|
91
|
+
def attribute_readable?(model_instance, attribute_name)
|
|
92
|
+
PolicyChecker.new(model_instance).attribute_readable?(attribute_name)
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Customizing Display {#customizing-display}
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
attribute :name, :string do
|
|
100
|
+
@object.name.upcase # @object is the model instance
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Types {#types}
|
|
105
|
+
|
|
106
|
+
| Type | Notes |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `string` | |
|
|
109
|
+
| `integer` | |
|
|
110
|
+
| `integer_id` | Renders as a string, queries/persists as an integer. Default type for `id`. |
|
|
111
|
+
| `uuid` | Like `string`, but only `eq`/`not_eq`, case-sensitive by default. |
|
|
112
|
+
| `string_enum` | Like `string`, but only `eq`/`not_eq`/`eql`/`not_eql`, and requires `allow:`. |
|
|
113
|
+
| `integer_enum` | Like `integer`, but only `eq`/`not_eq`, and requires `allow:`. |
|
|
114
|
+
| `big_decimal` | |
|
|
115
|
+
| `float` | |
|
|
116
|
+
| `boolean` | |
|
|
117
|
+
| `date` | |
|
|
118
|
+
| `datetime` | |
|
|
119
|
+
| `hash` | |
|
|
120
|
+
| `array` | |
|
|
121
|
+
|
|
122
|
+
Every type except `boolean`, `hash`, and `array` also has an `array_of_*` variant: `array_of_integers`, `array_of_dates`, `array_of_uuids`, and so on.
|
|
123
|
+
|
|
124
|
+
Each Type governs reading, writing, and filtering by wrapping a [Dry Type](https://dry-rb.org/gems/dry-types). Inspect one to see its parts:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
Graphiti::Types[:integer_id]
|
|
128
|
+
|
|
129
|
+
# {
|
|
130
|
+
# params: Dry::Types['coercible.integer'],
|
|
131
|
+
# read: Dry::Types['coercible.string'],
|
|
132
|
+
# write: Dry::Types['coercible.integer'],
|
|
133
|
+
# ...
|
|
134
|
+
# }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Edit an implementation in place. Here, `:string` is made to render as an integer:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
Graphiti::Types[:string][:read] = Dry::Types['coercible.integer']
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Enum Types {#enum-types}
|
|
144
|
+
|
|
145
|
+
`string_enum` and `integer_enum` behave like `string` and `integer`, except declaring one (as an attribute or a filter) requires the `allow:` option, the list of acceptable values:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
attribute :status, :string_enum, allow: ['draft', 'published']
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If your attribute is backed by an ActiveRecord enum, reference the values directly:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# app/models/post.rb
|
|
155
|
+
class Post < ApplicationRecord
|
|
156
|
+
enum status: {
|
|
157
|
+
draft: 0,
|
|
158
|
+
published: 1
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# app/resources/post_resource.rb
|
|
163
|
+
class PostResource < ApplicationResource
|
|
164
|
+
attribute :status, :string_enum, allow: Post.statuses.keys
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
See [Filter Options](#filter-options) for more on `allow`.
|
|
169
|
+
|
|
170
|
+
Graphiti does not validate enum values on write. Your model layer is still expected to validate incoming data.
|
|
171
|
+
|
|
172
|
+
#### Custom Types {#custom-types}
|
|
173
|
+
|
|
174
|
+
[Dry Types supports custom types](https://dry-rb.org/gems/dry-types/main/custom-types/):
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# Define the Type
|
|
178
|
+
definition = Dry::Types::Nominal.new(String)
|
|
179
|
+
type = definition.constructor do |input|
|
|
180
|
+
input.upcase
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Register it with Graphiti
|
|
184
|
+
Graphiti::Types[:caps_lock] = {
|
|
185
|
+
params: type,
|
|
186
|
+
read: type,
|
|
187
|
+
write: type,
|
|
188
|
+
kind: 'scalar',
|
|
189
|
+
canonical_name: :caps_lock,
|
|
190
|
+
description: 'All capital letters'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Use in a Resource
|
|
194
|
+
attribute :name, :caps_lock
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Querying {#querying}
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
class PostResource < ApplicationResource
|
|
201
|
+
# Applies to every query: start with a base scope, alter it based on
|
|
202
|
+
# the incoming request. Called just like ActiveRecord's Post.all.
|
|
203
|
+
def base_scope
|
|
204
|
+
Post.all
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Must execute the query and return an array of Model instances.
|
|
208
|
+
def resolve(scope)
|
|
209
|
+
scope.to_a
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Query Interface {#query-interface}
|
|
215
|
+
|
|
216
|
+
Resources can query and persist without an API request or response. Pass a [JSONAPI-compliant](http://jsonapi.org) query hash directly:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
EmployeeResource.all({
|
|
220
|
+
filter: { first_name: 'Jane' },
|
|
221
|
+
sort: '-created_at',
|
|
222
|
+
page: { size: 10, number: 2 }
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The return value from `.all` is a **proxy** object, similar to `ActiveRecord::Relation`. No query fires until you call `.map`, `.data`, or a render method:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
employees = EmployeeResource.all
|
|
230
|
+
employees.class # Graphiti::ResourceProxy
|
|
231
|
+
employees.map(&:first_name) # => ["Jane", "Joe", ...]
|
|
232
|
+
employees.data # => [#<Employee>, #<Employee>, ...]
|
|
233
|
+
|
|
234
|
+
employees.to_jsonapi
|
|
235
|
+
employees.to_json
|
|
236
|
+
employees.to_xml
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`.find` returns a single record's proxy by id, raising `Graphiti::Errors::RecordNotFound` if none are returned:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
employee = EmployeeResource.find(id: 123)
|
|
243
|
+
employee.data.first_name # => "Jane"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Composing with Scopes {#composing-with-scopes}
|
|
247
|
+
|
|
248
|
+
#### #base_scope {#base-scope}
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
def base_scope
|
|
252
|
+
Position.where(active: true)
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Override `#base_scope` for logic that should apply to every query. Here, it only ever returns active Positions.
|
|
257
|
+
|
|
258
|
+
Pass a second argument to `.all` to override the base scope for a single call:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
class InactivePostsController < PostsController
|
|
262
|
+
def index
|
|
263
|
+
posts = PostResource.all(params, Post.where(active: false))
|
|
264
|
+
respond_with(posts)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Sort {#sort}
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
sort :name, :string do |scope, direction|
|
|
273
|
+
scope.order(first_name: direction, last_name: direction)
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Omit the type if a matching `attribute` is already defined. This overrides its default sort behavior:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
attribute :name, :string
|
|
281
|
+
|
|
282
|
+
sort :name do |scope, direction|
|
|
283
|
+
# ... code ...
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`sort` on its own defines a sort-only attribute. Define the `attribute` first if you also need filtering or other behavior.
|
|
288
|
+
|
|
289
|
+
#### Sort Options {#sort-options}
|
|
290
|
+
|
|
291
|
+
| Option | Description |
|
|
292
|
+
| --- | --- |
|
|
293
|
+
| `only` | Restrict to a single direction, e.g. `sort :name, only: [:desc]` |
|
|
294
|
+
|
|
295
|
+
### Filter {#filter}
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
filter :name, :string do
|
|
299
|
+
eq do |scope, value|
|
|
300
|
+
scope.where(first_name: value)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# prefix do ... end
|
|
304
|
+
# suffix do ... end
|
|
305
|
+
# etc
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Omit the type if a matching `attribute` is already defined. This overrides its default filter behavior. `filter` on its own defines a filter-only attribute. Define the `attribute` first if you also need sorting or other behavior.
|
|
310
|
+
|
|
311
|
+
Every operator below also has a `not_` counterpart (`not_eq`, `not_prefix`, ...). Values arrive as an array unless the filter is `single: true`. Comma-delimit multiple values in a query string (`/employees?filter[name]=Jane,John`).
|
|
312
|
+
|
|
313
|
+
| Type | Default operators |
|
|
314
|
+
| --- | --- |
|
|
315
|
+
| `string` | `eq`, `eql`, `prefix`, `suffix`, `match` |
|
|
316
|
+
| `uuid` | `eq` |
|
|
317
|
+
| `string_enum`, `integer_enum` | `eq`, `eql` |
|
|
318
|
+
| `integer_id`, `integer`, `big_decimal`, `float`, `date`, `datetime` | `eq`, `gt`, `gte`, `lt`, `lte` |
|
|
319
|
+
| `boolean` | `eq` (always `single: true`) |
|
|
320
|
+
| `hash` | `eq` |
|
|
321
|
+
| `array` | `eq` |
|
|
322
|
+
|
|
323
|
+
Define custom operators on the fly:
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
filter :name do
|
|
327
|
+
fuzzy_match do |scope, value|
|
|
328
|
+
# ... code ...
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
This supports `filter[name][fuzzy_match]=foo`.
|
|
334
|
+
|
|
335
|
+
#### Filter Options {#filter-options}
|
|
336
|
+
|
|
337
|
+
| Option | Description |
|
|
338
|
+
| --- | --- |
|
|
339
|
+
| `only`, `except` | Limit the operators generated from the type's defaults, e.g. `filter :name, :string, only: [:eq, :suffix]` |
|
|
340
|
+
| `allow` | Only permit these values, e.g. `filter :size, :string, allow: ['Big', 'Medium', 'Small']` |
|
|
341
|
+
| `deny` | Reject these values, e.g. `filter :size, :string, deny: ['X-Large']` |
|
|
342
|
+
| `single` | Accept one value instead of an array. `boolean` filters are `single: true` by default. |
|
|
343
|
+
| `required` | Reject the request if the filter is absent, e.g. `filter :customer_id, :string, required: true` (equivalently, `attribute :customer_id, :integer, filterable: :required`) |
|
|
344
|
+
| `dependent` | Require other filters alongside this one, e.g. `filter :customer_id, :integer, dependent: [:customer_type]` paired with `filter :customer_type, :string, dependent: [:customer_id]`, so querying by id requires type, and vice versa |
|
|
345
|
+
| `allow_nil` | Coerce an incoming `null` to Ruby `nil` instead of the string `"null"`. Default `false`. Set `self.filters_accept_nil_by_default = true` on a Resource to flip it for all of that Resource's filters. |
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
# Default behavior
|
|
349
|
+
filter :name, :string do
|
|
350
|
+
eq do |scope, value|
|
|
351
|
+
value # => ["Jane"]
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# With single: true
|
|
356
|
+
filter :name, :string, single: true do
|
|
357
|
+
eq do |scope, value|
|
|
358
|
+
value # => "Jane"
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### Boolean Filter {#boolean-filter}
|
|
364
|
+
|
|
365
|
+
Filters with type `boolean` are `single: true` by default. A boolean filter accepting multiple values doesn't make sense.
|
|
366
|
+
|
|
367
|
+
#### Hash Filter {#hash-filter}
|
|
368
|
+
|
|
369
|
+
Filters with type `hash` parse JSON automatically when passed in a URL query string:
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
# GET /employees?filter[metadata]={ "foo": 100 }
|
|
373
|
+
|
|
374
|
+
filter :metadata, :hash do
|
|
375
|
+
eq do |scope, value|
|
|
376
|
+
value # => [{ "foo" => 100 }]
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### Escaping Values {#escaping-values}
|
|
382
|
+
|
|
383
|
+
By default, Graphiti parses a comma-delimited string as an array. Wrap a value in `{{curlies}}` to keep it intact, for a "keyword search" field that could itself contain a comma:
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
# GET /employees?filter[keywords]={{some,value}}
|
|
387
|
+
|
|
388
|
+
filter :keywords, :string do
|
|
389
|
+
eq do |scope, value|
|
|
390
|
+
value # => "some,value"
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Or define an array explicitly instead of relying on comma-splitting:
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# GET /employees?filter[keywords]=[some,value]
|
|
399
|
+
|
|
400
|
+
filter :keywords, :string do
|
|
401
|
+
eq do |scope, value|
|
|
402
|
+
value # => ["some", "value"]
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
A `single: true` filter skips array parsing entirely and escapes the value for you, filtering on the string as given.
|
|
408
|
+
|
|
409
|
+
### Statistics {#statistics}
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
stat total: [:count]
|
|
413
|
+
stat rating: [:average]
|
|
414
|
+
stat likes: [:sum]
|
|
415
|
+
stat score: [:maximum]
|
|
416
|
+
|
|
417
|
+
stat rating: [:average] do
|
|
418
|
+
standard_deviation do |scope, attr|
|
|
419
|
+
# your standard deviation code here
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Every Resource has a `total: :count` statistic by default. Statistics respect filtering but not pagination, so you can show a "Total Posts" count above a paginated grid without a second request:
|
|
425
|
+
|
|
426
|
+
```ruby
|
|
427
|
+
PostResource.all({
|
|
428
|
+
stats: { total: 'count' }
|
|
429
|
+
})
|
|
430
|
+
# GET /posts?stats[total]=count
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
{
|
|
435
|
+
meta: {
|
|
436
|
+
stats: {
|
|
437
|
+
total: {
|
|
438
|
+
count: 100
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Extra Fields {#extra-fields}
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
extra_attribute :net_worth
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Works like `attribute`, except the field is read-only and only returned when explicitly requested: `?extra_fields[employees]=net_worth`.
|
|
452
|
+
|
|
453
|
+
Adjust the scope (e.g. to eager-load) only when the extra field is requested:
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
resource.on_extra_attribute :net_worth do |scope|
|
|
457
|
+
scope.includes(:assets)
|
|
458
|
+
end
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### #resolve {#resolve}
|
|
462
|
+
|
|
463
|
+
`#resolve` must execute the query and return an array of `Model` instances. Override it to add behavior around the default:
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
def resolve(scope)
|
|
467
|
+
Rails.logger.info "begin resolving scope..."
|
|
468
|
+
result = super
|
|
469
|
+
Rails.logger.info "resolved!"
|
|
470
|
+
result
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Configuration {#configuration}
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
class PostResource < ApplicationResource
|
|
478
|
+
self.model = Post
|
|
479
|
+
self.type = 'posts'
|
|
480
|
+
|
|
481
|
+
# Only used if you care about Links
|
|
482
|
+
primary_endpoint '/posts', [:index, :show, :create, :update, :destroy]
|
|
483
|
+
|
|
484
|
+
self.default_sort = [{ title: :asc }] # default nil
|
|
485
|
+
self.default_page_size = 10 # default 20
|
|
486
|
+
end
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Typically inherited from `ApplicationResource`, where cross-cutting settings live:
|
|
490
|
+
|
|
491
|
+
```ruby
|
|
492
|
+
class ApplicationResource < Graphiti::Resource
|
|
493
|
+
# Required when there's no corresponding model
|
|
494
|
+
self.abstract_class = true
|
|
495
|
+
|
|
496
|
+
# Subclasses override as needed
|
|
497
|
+
self.adapter = Graphiti::Adapters::ActiveRecord
|
|
498
|
+
|
|
499
|
+
# Default attribute flags. See #limiting-behavior
|
|
500
|
+
self.attributes_readable_by_default = true
|
|
501
|
+
self.attributes_writable_by_default = true
|
|
502
|
+
self.attributes_sortable_by_default = true
|
|
503
|
+
self.attributes_filterable_by_default = true
|
|
504
|
+
|
|
505
|
+
# Used for link generation
|
|
506
|
+
self.base_url = Rails.application.routes.default_url_options[:host]
|
|
507
|
+
# Suggest referencing this in config/routes.rb:
|
|
508
|
+
# scope path: ApplicationResource.endpoint_namespace do
|
|
509
|
+
# resources :posts
|
|
510
|
+
# end
|
|
511
|
+
self.endpoint_namespace = '/api/v1'
|
|
512
|
+
|
|
513
|
+
# Raise if a Resource is accessed from a URL it isn't allowlisted for
|
|
514
|
+
self.validate_endpoints = false
|
|
515
|
+
|
|
516
|
+
# Automatically generate JSONAPI links?
|
|
517
|
+
self.autolink = true
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Polymorphic Resources {#polymorphic-resources}
|
|
522
|
+
|
|
523
|
+
Polymorphic Resources are similar to [ActiveRecord STI](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html): a single query returns multiple Resource types. Querying `/tasks` can return `bugs`, `features`, and `epics`.
|
|
524
|
+
|
|
525
|
+
```ruby
|
|
526
|
+
class Employee < ApplicationRecord
|
|
527
|
+
has_many :tasks
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# tasks table has a 'type' column
|
|
531
|
+
class Task < ApplicationRecord
|
|
532
|
+
belongs_to :employee
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
class Bug < Task
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# ONLY Feature has #points
|
|
539
|
+
class Feature < Task
|
|
540
|
+
def points
|
|
541
|
+
5
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# ONLY Epic has the milestones relationship
|
|
546
|
+
class Epic < Task
|
|
547
|
+
has_many :milestones
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
class Milestone < ApplicationRecord
|
|
551
|
+
belongs_to :epic
|
|
552
|
+
end
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
class TaskResource < ApplicationResource
|
|
557
|
+
# Reference child classes
|
|
558
|
+
self.polymorphic = [
|
|
559
|
+
'BugResource',
|
|
560
|
+
'FeatureResource',
|
|
561
|
+
'EpicResource'
|
|
562
|
+
]
|
|
563
|
+
|
|
564
|
+
attribute :title, :string
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
class BugResource < TaskResource
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
class FeatureResource < TaskResource
|
|
571
|
+
attribute :points, :integer
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
class EpicResource < TaskResource
|
|
575
|
+
has_many :milestones
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
class MilestoneResource < TaskResource
|
|
579
|
+
belongs_to :epic
|
|
580
|
+
end
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
`/tasks` returns [JSONAPI types](http://jsonapi.org/format/#document-resource-identifier-objects) of `bugs`, `features`, and `epics`. Only `features` render `points`. Only `epics` render the `milestones` relationship. `/tasks?include=milestones` correctly only queries and renders Milestones for Epics.
|
|
584
|
+
|
|
585
|
+
Resources connect to each other through relationships. See [Relationships](/concepts/relationships).
|
|
586
|
+
|
|
587
|
+
## Generators {#generators}
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
$ rails generate graphiti:resource NAME [attribute:type] [options]
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
$ rails generate graphiti:resource Employee first_name:string age:integer
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Adds a route, controller, resource, and tests.
|
|
598
|
+
|
|
599
|
+
Limit the actions the resource supports with `-a`:
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
$ rails generate graphiti:resource Employee -a index show
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Writing data (creating, updating, and destroying resources, including a graph of them in a single request) is covered in [Persisting](/concepts/persisting).
|
|
606
|
+
|
|
607
|
+
## Context {#context}
|
|
608
|
+
|
|
609
|
+
```ruby
|
|
610
|
+
# app/resources/post_resource.rb
|
|
611
|
+
attribute :active, :boolean, writable: :admin?
|
|
612
|
+
|
|
613
|
+
def admin?
|
|
614
|
+
context.current_user.admin?
|
|
615
|
+
end
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Every Resource has access to `#context`. Under Rails, `context` is the controller instance processing the request.
|
|
619
|
+
|
|
620
|
+
Put common helpers like `current_user` on `ApplicationResource`, so every Resource can call them:
|
|
621
|
+
|
|
622
|
+
```ruby
|
|
623
|
+
# app/resources/application_resource.rb
|
|
624
|
+
class ApplicationResource < Graphiti::Resource
|
|
625
|
+
# ... code ...
|
|
626
|
+
def current_user
|
|
627
|
+
context.current_user
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# app/resources/post_resource.rb
|
|
632
|
+
class PostResource < ApplicationResource
|
|
633
|
+
# ... code ...
|
|
634
|
+
def admin?
|
|
635
|
+
current_user.admin?
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Set context manually with `with_context`:
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
ctx = OpenStruct.new(current_user: User.first)
|
|
644
|
+
Graphiti.with_context(ctx) do
|
|
645
|
+
# current_user == ctx.current_user
|
|
646
|
+
PostResource.all
|
|
647
|
+
end
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## Concurrency {#concurrency}
|
|
651
|
+
|
|
652
|
+
```ruby
|
|
653
|
+
# config/initializers/graphiti.rb
|
|
654
|
+
Graphiti.configure do |c|
|
|
655
|
+
c.concurrency = false
|
|
656
|
+
end
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Under Rails, concurrency turns on by default when `::Rails.application.config.cache_classes` is `true` (the default for staging and production). Sibling sideloads then load concurrently, so a `Post` sideloading `Comments` and `Author` loads both at the same time.
|
|
660
|
+
|
|
661
|
+
Concurrency runs sideloads in new Threads, so thread locals are dropped. Use `Graphiti.context` instead of `Thread.current` for anything that needs to survive a sideload:
|
|
662
|
+
|
|
663
|
+
```ruby
|
|
664
|
+
# BAD:
|
|
665
|
+
Thread.current[:foo] = "bar"
|
|
666
|
+
Thread.current[:foo] # => will be nil when sideloading!
|
|
667
|
+
|
|
668
|
+
# GOOD:
|
|
669
|
+
Graphiti.context[:foo] = "bar"
|
|
670
|
+
Graphiti.context[:foo] # => "bar", even when sideloading
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
## Adapters {#adapters}
|
|
674
|
+
|
|
675
|
+
Common resource overrides can be packaged into an Adapter for code re-use, most commonly to use a different client/datastore than ActiveRecord/RelationalDB.
|
|
676
|
+
|
|
677
|
+
[Adapters are best explained in the 'Without ActiveRecord' recipe](/topics/without-activerecord).
|