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
data/docs/intro.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: intro
|
|
3
|
+
title: 'Graphiti'
|
|
4
|
+
sidebar_label: 'Overview'
|
|
5
|
+
sidebar_position: 0
|
|
6
|
+
slug: /
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Graphiti
|
|
10
|
+
|
|
11
|
+
:::info These are the 2.0 docs
|
|
12
|
+
Graphiti 2.0 folds `graphiti-rails`, `graphiti_spec_helpers` and `graphiti_errors` into the main gem, and requires Ruby 3.2+ and ActiveSupport 7.1+. Rails is optional, and 7.1+ if you use it. Coming from 1.x? Start with the **[2.0 upgrade guide](/upgrading)**. Docs for 1.x are in the version picker.
|
|
13
|
+
:::
|
|
14
|
+
|
|
15
|
+
Graphiti sits on top of your models and exposes them over a [JSON:API](https://jsonapi.org)-compliant interface. You define Resources instead of controllers and serializers, and get filtering, sorting, pagination, sparse fieldsets, statistics, and nested reads and writes across relationships, all over one endpoint.
|
|
16
|
+
|
|
17
|
+
Here is the whole loop. A Resource declares what's exposed:
|
|
18
|
+
|
|
19
|
+
```ruby title="app/resources/employee_resource.rb"
|
|
20
|
+
class EmployeeResource < ApplicationResource
|
|
21
|
+
attribute :first_name, :string
|
|
22
|
+
attribute :last_name, :string
|
|
23
|
+
attribute :age, :integer
|
|
24
|
+
|
|
25
|
+
has_many :positions
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The controller hands it the request params and renders the result:
|
|
30
|
+
|
|
31
|
+
```ruby title="app/controllers/employees_controller.rb"
|
|
32
|
+
class EmployeesController < ApplicationController
|
|
33
|
+
def index
|
|
34
|
+
employees = EmployeeResource.all(params)
|
|
35
|
+
|
|
36
|
+
respond_to do |format|
|
|
37
|
+
format.jsonapi { render(jsonapi: employees) }
|
|
38
|
+
format.json { render(json: employees) }
|
|
39
|
+
format.xml { render(xml: employees) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def show
|
|
44
|
+
employee = EmployeeResource.find(params)
|
|
45
|
+
authorize employee.data # pundit. .data is the model itself
|
|
46
|
+
render(jsonapi: employee)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A client asks for employees and their positions in one request:
|
|
52
|
+
|
|
53
|
+
```http title="Request"
|
|
54
|
+
GET /api/v1/employees?include=positions
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```json title="Response"
|
|
58
|
+
{
|
|
59
|
+
"data": [
|
|
60
|
+
{
|
|
61
|
+
"id": "1",
|
|
62
|
+
"type": "employees",
|
|
63
|
+
"attributes": {
|
|
64
|
+
"first_name": "Jane",
|
|
65
|
+
"last_name": "Doe",
|
|
66
|
+
"age": 34
|
|
67
|
+
},
|
|
68
|
+
"relationships": {
|
|
69
|
+
"positions": {
|
|
70
|
+
"data": [
|
|
71
|
+
{ "type": "positions", "id": "1" },
|
|
72
|
+
{ "type": "positions", "id": "2" }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"included": [
|
|
79
|
+
{
|
|
80
|
+
"id": "1",
|
|
81
|
+
"type": "positions",
|
|
82
|
+
"attributes": { "title": "Engineer" }
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "2",
|
|
86
|
+
"type": "positions",
|
|
87
|
+
"attributes": { "title": "Senior Engineer" }
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
That same Resource also serves `?filter[age][gt]=30`, `?sort=-age`, `?page[size]=10`, `?fields[employees]=first_name`, and `?stats[total]=count`, without writing any of them.
|
|
94
|
+
|
|
95
|
+
The same proxy renders all three formats, so `/employees.jsonapi`, `/employees.json` and `/employees.xml` all work off one action.
|
|
96
|
+
|
|
97
|
+
`.all` and `.find` return that proxy, so nothing has been queried yet. `.data` is where you reach the model, and also where per-record authorization goes. See [Authorization](/topics/authorization#integrating-with-pundit).
|
|
98
|
+
|
|
99
|
+
If repeating that `respond_to` block gets old, the optional [`responders`](https://github.com/heartcombo/responders) integration collapses it to `respond_with(employees)`. See [Installation](/getting-started/installation#responders).
|
|
100
|
+
|
|
101
|
+
## The whole Resource API
|
|
102
|
+
|
|
103
|
+
Every Resource is a collection of defaults, and you can override any of them. Below is one Resource with those defaults written out the long way, the entire surface area on a single page. You wouldn't write this much by hand. It's here so you can see what's available.
|
|
104
|
+
|
|
105
|
+
### ApplicationResource
|
|
106
|
+
|
|
107
|
+
Every Resource inherits from an `ApplicationResource`, the same way models inherit from `ApplicationRecord`. This is where cross-cutting configuration lives, so individual Resources stay small. It's also the right place to put helpers like `current_user`, which guards throughout your API can then call.
|
|
108
|
+
|
|
109
|
+
```ruby title="app/resources/application_resource.rb"
|
|
110
|
+
class ApplicationResource < Graphiti::Resource
|
|
111
|
+
# Required when there's no corresponding model
|
|
112
|
+
self.abstract_class = true
|
|
113
|
+
|
|
114
|
+
# Subclasses override as needed
|
|
115
|
+
self.adapter = Graphiti::Adapters::ActiveRecord
|
|
116
|
+
|
|
117
|
+
# Flip any of these to lock down every Resource at once,
|
|
118
|
+
# e.g. a read-only API
|
|
119
|
+
self.attributes_readable_by_default = true
|
|
120
|
+
self.attributes_writable_by_default = true
|
|
121
|
+
self.attributes_sortable_by_default = true
|
|
122
|
+
self.attributes_filterable_by_default = true
|
|
123
|
+
|
|
124
|
+
# Used for link generation
|
|
125
|
+
self.base_url = Rails.application.routes.default_url_options[:host]
|
|
126
|
+
self.endpoint_namespace = '/api/v1'
|
|
127
|
+
|
|
128
|
+
def current_user
|
|
129
|
+
context.current_user
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### A Resource
|
|
135
|
+
|
|
136
|
+
An individual Resource declares its attributes and relationships, plus anything about it that differs from the defaults:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class EmployeeResource < ApplicationResource
|
|
140
|
+
# Both inferred from the class name. Set them only when they differ
|
|
141
|
+
self.model = Employee
|
|
142
|
+
self.type = :employees # the JSONAPI type
|
|
143
|
+
|
|
144
|
+
self.default_sort = [{ name: :desc }] # default nil
|
|
145
|
+
self.default_page_size = 10 # default 20
|
|
146
|
+
|
|
147
|
+
attribute :name, :string
|
|
148
|
+
attribute :age, :integer
|
|
149
|
+
attribute :hired_at, :datetime, writable: false
|
|
150
|
+
|
|
151
|
+
has_many :positions
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
That is a complete, working Resource. Everything below is how you override a piece of it.
|
|
156
|
+
|
|
157
|
+
### Attributes
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# Each flag defaults to the corresponding class-level setting
|
|
161
|
+
attribute :name, :string,
|
|
162
|
+
readable: self.attributes_readable_by_default,
|
|
163
|
+
writable: self.attributes_writable_by_default,
|
|
164
|
+
sortable: self.attributes_sortable_by_default,
|
|
165
|
+
filterable: self.attributes_filterable_by_default
|
|
166
|
+
|
|
167
|
+
# Alter display
|
|
168
|
+
# @object is your model instance
|
|
169
|
+
attribute :name, :string do
|
|
170
|
+
@object.name.upcase
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Sorting
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# Pass a type - sort :name, :string - if no attribute is defined
|
|
178
|
+
sort :name do |scope, dir|
|
|
179
|
+
scope.order(name: dir)
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Filtering
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# Pass a type - filter :name, :string - if no attribute is defined
|
|
187
|
+
filter :name do
|
|
188
|
+
# All of these operators have not_ equivalents, e.g. not_eq
|
|
189
|
+
# Imagine ".where.not" instead of ".where"
|
|
190
|
+
|
|
191
|
+
eq do |scope, value|
|
|
192
|
+
scope.where("lower(name) IN ?", value.map(&:downcase))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
eql do |scope, value|
|
|
196
|
+
scope.where(name: value)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
prefix do |scope, value|
|
|
200
|
+
value.each do |v|
|
|
201
|
+
scope = scope.where('lower(name) LIKE ?', "#{v.downcase}%")
|
|
202
|
+
end
|
|
203
|
+
scope
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
suffix do |scope, value|
|
|
207
|
+
value.each do |v|
|
|
208
|
+
scope = scope.where('lower(name) LIKE ?', "%#{v.downcase}")
|
|
209
|
+
end
|
|
210
|
+
scope
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
match do |scope, value|
|
|
214
|
+
value.each do |v|
|
|
215
|
+
scope = scope.where('lower(name) LIKE ?', "%#{v.downcase}%")
|
|
216
|
+
end
|
|
217
|
+
scope
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Comparison operators, for integer, float, datetime, etc
|
|
222
|
+
filter :age, :integer do
|
|
223
|
+
eq do |scope, value|
|
|
224
|
+
scope.where(age: value)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
gt do |scope, value|
|
|
228
|
+
value.each { |v| scope = scope.where('age > ?', v) }
|
|
229
|
+
scope
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
gte do |scope, value|
|
|
233
|
+
value.each { |v| scope = scope.where('age >= ?', v) }
|
|
234
|
+
scope
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
lt do |scope, value|
|
|
238
|
+
value.each { |v| scope = scope.where('age < ?', v) }
|
|
239
|
+
scope
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
lte do |scope, value|
|
|
243
|
+
value.each { |v| scope = scope.where('age <= ?', v) }
|
|
244
|
+
scope
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Filters receive an array of values by default, which is why each operator above iterates. Pass `single: true` to accept one value instead.
|
|
250
|
+
|
|
251
|
+
### Querying
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# Passed to sort, filter, paginate, etc
|
|
255
|
+
# Apply global logic here: only return active Employees,
|
|
256
|
+
# scope results to the current user, and so on
|
|
257
|
+
def base_scope
|
|
258
|
+
Employee.all
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Must execute the query and return an array of Model instances
|
|
262
|
+
def resolve(scope)
|
|
263
|
+
scope.to_a
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Persisting
|
|
268
|
+
|
|
269
|
+
Your adapter handles writes for you, so most Resources define nothing here. Reach for [lifecycle hooks](/concepts/persisting#persistence-lifecycle-hooks) when you need to intervene:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
before_attributes do |attributes|
|
|
273
|
+
# before attributes are assigned to the model
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
before_save do |model|
|
|
277
|
+
# assigned, but not yet persisted
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
before_commit do |model|
|
|
281
|
+
# saved and validated, still inside the transaction
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The model you inspect is the model that saves. Attributes are assigned up front, so you can hold the model, check it, and change it before anything is written. The instance you were handed is the one that gets persisted:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
employee = EmployeeResource.build(payload)
|
|
289
|
+
|
|
290
|
+
employee.data # the model, attributes already assigned, nothing written yet
|
|
291
|
+
employee.data.valid? # inspect it, or modify it
|
|
292
|
+
employee.save # persists that same instance
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Updates work the same way, reading the persisted record until you apply the payload:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
proxy = EmployeeResource.find(payload)
|
|
299
|
+
proxy.data.first_name # => "asdf", straight from the database
|
|
300
|
+
proxy.assign_attributes(payload)
|
|
301
|
+
proxy.data.first_name # => "Jane", assigned but still unsaved
|
|
302
|
+
proxy.save(action: :update)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Upgrading from 1.x
|
|
306
|
+
|
|
307
|
+
The [2.0 upgrade guide](/upgrading) covers the whole migration: the three gems that folded into core, the deprecated spellings that still work but warn, and the two real behavior changes. Controllers now opt in via `Graphiti::Rails::Controller`, and `around_persistence` receives the model rather than an attributes hash.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Authentication'
|
|
3
|
+
sidebar_position: 7
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
### Authentication
|
|
7
|
+
|
|
8
|
+
Spraypaint supports [JSON Web Tokens](https://jwt.io/introduction). These can
|
|
9
|
+
be set manually, or automatically fetched from `localStorage`.
|
|
10
|
+
|
|
11
|
+
To set manually:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
ApplicationRecord.jwt = 'myt0k3n'
|
|
15
|
+
```
|
|
16
|
+
> All requests will now send the header:<br />
|
|
17
|
+
> `Authorization: Token token="myt0k3n"`.
|
|
18
|
+
|
|
19
|
+
To set via `localStorage`, store the token with a key of `jwt` and it will be set automatically. To customize the `localStorage` key:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
ApplicationRecord.jwtStorage = "authtoken"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
...or to opt-out of `localStorage` altogether:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
ApplicationRecord.jwtStorage = false
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can control the format of the header that is sent to the
|
|
32
|
+
server:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
class ApplicationRecord extends SpraypaintBase {
|
|
36
|
+
// ... code ...
|
|
37
|
+
static generateAuthHeader(token) {
|
|
38
|
+
return `Bearer ${token}`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
var ApplicationRecord = SpraypaintBase.extend({
|
|
45
|
+
// ... code ...
|
|
46
|
+
static: {
|
|
47
|
+
generateAuthHeader: function(token) {
|
|
48
|
+
return "Bearer " + token;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Finally, if your server returns a refreshed JWT within the `X-JWT` header, it will be used in all subsequent requests (and `localStorage`
|
|
55
|
+
will be updated automatically if you're using it).
|
|
56
|
+
|
|
57
|
+
<h2 id="next">
|
|
58
|
+
<a href="/js/state-syncing">
|
|
59
|
+
NEXT:
|
|
60
|
+
<small>State Syncing</small>
|
|
61
|
+
»
|
|
62
|
+
</a>
|
|
63
|
+
</h2>
|
data/docs/js/ddau.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Ddau'
|
|
3
|
+
sidebar_position: 9
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
### Data Down, Actions Up
|
|
7
|
+
|
|
8
|
+
It's a [popular pattern](http://www.samselikoff.com/blog/data-down-actions-up) to pass data **down** to components, avoid modifying state within the component, and instead pass **actions up** to modify state. This can make complex applications easier to track and reason about, and you'll see it in client-side frameworks like React.
|
|
9
|
+
|
|
10
|
+
To follow this pattern, use `#dup()` when passing down to your component:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
<my-component something="model.dup()" />
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This will create a new instance of the model with all the same state.
|
|
17
|
+
Avoid modifying this instance in your component and instead pass
|
|
18
|
+
**actions up**.
|
|
19
|
+
|
|
20
|
+
When opting-in to [state-syncing](/js/state-syncing) these instances will sync-up whenever one of these is instances is persisted. You won't have to worry about updating the child component when the parent instance is saved.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Extra Params'
|
|
3
|
+
sidebar_position: 10
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
### Extra Params
|
|
7
|
+
|
|
8
|
+
Sometimes you need to submit params that are not standard jsonapi params. One great example would be
|
|
9
|
+
`https://yourdomain.com/users?debug=true` which is not a param for the `UserResource` you may have, but
|
|
10
|
+
might enable functionality in your controller as needed.
|
|
11
|
+
|
|
12
|
+
Invoking it is pretty straightforward, just invoke `extraParams` and pass in params and values you wish
|
|
13
|
+
to add to your API call when executed.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
YourRecord.extraParams({ debug: true })
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
One common way to use this globally is to put this into a base class so it can be chained as part of
|
|
21
|
+
every resource.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
@Model
|
|
25
|
+
export class ApplicationRecord extends SpraypaintBase {
|
|
26
|
+
static withDebug<T extends ApplicationRecord>(): Scope<T> {
|
|
27
|
+
return this.extraParams({ debug: true }) as Scope<T>;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// unfortunately you will need to pass in the
|
|
31
|
+
// implementing class' type as a generic
|
|
32
|
+
UserRecord.withDebug<UserRecord>().all()
|
|
33
|
+
```
|
|
34
|
+
```javascript
|
|
35
|
+
const ApplicationRecord = SpraypaintBase.extend({
|
|
36
|
+
static: {
|
|
37
|
+
withDebug: () => this.extraParams({ debug: true });
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
UserRecord.withDebug().all()
|
|
41
|
+
```
|
data/docs/js/index.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Index'
|
|
3
|
+
sidebar_position: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<h1>
|
|
7
|
+
Spraypaint
|
|
8
|
+
<small>the isomorphic, framework-agnostic Graphiti ORM</small>
|
|
9
|
+
</h1>
|
|
10
|
+
|
|
11
|
+
### Why Spraypaint?
|
|
12
|
+
|
|
13
|
+
Contracts like JSONAPI and GraphQL treat the API like a database. When querying a database, we have two options:
|
|
14
|
+
|
|
15
|
+
* Type the low-level query language directly (in the database world, this would be hand-typing SQL).
|
|
16
|
+
* Use an ORM (like Rails's `ActiveRecord`, Phoenix's `Ecto`, Django's `DjangoORM`, or Node's `Sequelize`).
|
|
17
|
+
|
|
18
|
+
While both options have pros and cons, we tend to think ORMs have two overwhelming benefits: ***ease of use*** and ***composable queries***. We'll explore both these concepts in other sections.
|
|
19
|
+
|
|
20
|
+
So, we want a javascript ORM for our JSONAPI "database". Because `ActiveRecord` is arguably the most well-known ORM, we've tried to match its interface to make this library accessible to new users. That said, you'll find we've tried to favor *explicitness* over *implicitness* in order to avoid common `ActiveRecord` pitfalls.
|
|
21
|
+
|
|
22
|
+
<span>Typescript</span>
|
|
23
|
+
<span>Javascript</span>
|
|
24
|
+
```typescript
|
|
25
|
+
// Spraypaint is like "ActiveRecord in Javascript". It can:
|
|
26
|
+
//
|
|
27
|
+
// * Deeply nest reads and writes
|
|
28
|
+
// * Automatically handle validation errors
|
|
29
|
+
// * Replace *ux patterns
|
|
30
|
+
// * ...and much more!
|
|
31
|
+
|
|
32
|
+
// define models
|
|
33
|
+
@Model()
|
|
34
|
+
class ApplicationRecord extends SpraypaintBase {
|
|
35
|
+
static baseUrl = "http://my-api.com"
|
|
36
|
+
static apiNamespace = "/api/v1"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Model()
|
|
40
|
+
class Person extends ApplicationRecord {
|
|
41
|
+
static jsonapiType = "people"
|
|
42
|
+
|
|
43
|
+
@Attr() firstName: string
|
|
44
|
+
@Attr() lastName: string
|
|
45
|
+
|
|
46
|
+
get fullName() {
|
|
47
|
+
return `${this.firstName} ${this.lastName}`
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// execute queries
|
|
52
|
+
Person
|
|
53
|
+
.where({ first_name: 'John' })
|
|
54
|
+
.order({ created_at: 'desc' })
|
|
55
|
+
.per(10).page(2)
|
|
56
|
+
.includes({ jobs: 'company' })
|
|
57
|
+
.select({ people: ['first_name', 'last_name'] })
|
|
58
|
+
|
|
59
|
+
// persist data
|
|
60
|
+
let person = new Person({ firstName: 'Jane' })
|
|
61
|
+
person.save()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// Spraypaint is like "ActiveRecord in Javascript". It can:
|
|
66
|
+
//
|
|
67
|
+
// * Deeply nest reads and writes
|
|
68
|
+
// * Automatically handle validation errors
|
|
69
|
+
// * Replace *ux patterns
|
|
70
|
+
// * ...and much more!
|
|
71
|
+
|
|
72
|
+
var spnt = require('spraypaint/dist/spraypaint')
|
|
73
|
+
|
|
74
|
+
// define models
|
|
75
|
+
const ApplicationRecord = spnt.JSORMBase.extend({
|
|
76
|
+
static: {
|
|
77
|
+
baseUrl: 'http://my-api.com',
|
|
78
|
+
apiNamespace: '/api/v1'
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const Person = ApplicationRecord.extend({
|
|
83
|
+
attrs: {
|
|
84
|
+
firstName: spnt.attr(),
|
|
85
|
+
lastName: spnt.attr()
|
|
86
|
+
},
|
|
87
|
+
methods: {
|
|
88
|
+
fullName: function() {
|
|
89
|
+
return this.firstName + ' ' + this.lastName;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// execute queries
|
|
95
|
+
Person
|
|
96
|
+
.where({ first_name: 'John' })
|
|
97
|
+
.order({ created_at: 'desc' })
|
|
98
|
+
.per(10).page(2)
|
|
99
|
+
.includes({ jobs: 'company' })
|
|
100
|
+
.select({ people: ['first_name', 'last_name'] })
|
|
101
|
+
|
|
102
|
+
// persist data
|
|
103
|
+
var person = new Person({ firstName: 'Jane' })
|
|
104
|
+
person.save()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Where to Go Next
|
|
108
|
+
|
|
109
|
+
* [Installation](/js/installation) - install spraypaint and connect it to your API
|
|
110
|
+
* [Models](/js/models) - define models, attributes, and relationships
|
|
111
|
+
* [Reads](/js/reads) - query your API with a composable, ActiveRecord-like interface
|
|
112
|
+
* [Writes](/js/writes) - create, update, and destroy records
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Installation'
|
|
3
|
+
sidebar_position: 2
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
### Installation
|
|
7
|
+
|
|
8
|
+
Installation is straightforward. Since we use `fetch` underneath the hood, we recommend installing alongside a `fetch` polyfill.
|
|
9
|
+
|
|
10
|
+
If using `yarn`:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
$ yarn add spraypaint isomorphic-fetch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If using `npm`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
$ npm install spraypaint isomorphic-fetch
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Now import it:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import {
|
|
26
|
+
Model,
|
|
27
|
+
SpraypaintBase,
|
|
28
|
+
Attr,
|
|
29
|
+
BelongsTo,
|
|
30
|
+
HasMany
|
|
31
|
+
// etc
|
|
32
|
+
} from "spraypaint"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
const {
|
|
37
|
+
SpraypaintBase,
|
|
38
|
+
attr,
|
|
39
|
+
belongsTo,
|
|
40
|
+
hasMany
|
|
41
|
+
// etc
|
|
42
|
+
} = require("spraypaint/dist/spraypaint")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
...or, if you're avoiding JS modules, `spraypaint` will be available as a global in the browser.
|
|
46
|
+
|
|
47
|
+
### Typescript
|
|
48
|
+
|
|
49
|
+
Spraypaint works with modern TypeScript. Depending on your `tsconfig.json` settings, you may need a `!` after each attribute and relationship declaration:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
@Attr first_name!: string
|
|
53
|
+
@HasMany() positions!: Position[]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This is because of [Strict Class Initialization](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#strict-class-initialization) - `strictPropertyInitialization` expects every declared class field to be assigned in the constructor, which Spraypaint's decorators handle at runtime rather than at construction time. For the purposes of Spraypaint, we don't need this check. Remove the need for `!` (as the rest of these guides do) by setting
|
|
57
|
+
|
|
58
|
+
`"strictPropertyInitialization": false`
|
|
59
|
+
|
|
60
|
+
in `tsconfig.json`.
|
|
61
|
+
|
|
62
|
+
### Connecting to the API
|
|
63
|
+
|
|
64
|
+
Just like `ActiveRecord`, our models will inherit from a base class that holds connection information (`ApplicationRecord`, or `ActiveRecord::Base` in Rails < 5):
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
@Model()
|
|
68
|
+
class ApplicationRecord extends SpraypaintBase {
|
|
69
|
+
static baseUrl = "http://my-api.com"
|
|
70
|
+
static apiNamespace = "/api/v1"
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
const ApplicationRecord = SpraypaintBase.extend({
|
|
76
|
+
static: {
|
|
77
|
+
baseUrl: "http://my-api.com",
|
|
78
|
+
apiNamespace: "/api/v1"
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
All URLs follow the following pattern:
|
|
84
|
+
|
|
85
|
+
* `baseUrl` + `apiNamespace` + `jsonapiType`
|
|
86
|
+
|
|
87
|
+
As you can see above, typically `baseUrl` and `apiNamespace` are set on a top-level `ApplicationRecord` (though any subclass can override). `jsonapiType`, however, is set per-model - see [Models](/js/models) for how to define it.
|
|
88
|
+
|
|
89
|
+
> **TIP**: Avoid CORS and use relative paths by setting `baseUrl` to `""`
|
|
90
|
+
|
|
91
|
+
> **TIP**: You can always use the `endpoint` option to override this pattern and set the endpoint manually.
|
|
92
|
+
|
|
93
|
+
#### Setting Application Name
|
|
94
|
+
|
|
95
|
+
It can be helpful to send the name of your client application in request headers. With this information, servers can keep track of which clients are hitting which APIs.
|
|
96
|
+
|
|
97
|
+
To do this:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
@Model()
|
|
101
|
+
class Person extends ApplicationRecord {
|
|
102
|
+
static clientApplication = "sales-backend"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
const Person = ApplicationRecord.extend({
|
|
108
|
+
static: {
|
|
109
|
+
clientApplication: "sales-backend"
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
<h2 id="next">
|
|
115
|
+
<a href="/js/models">
|
|
116
|
+
NEXT:
|
|
117
|
+
<small>Models</small>
|
|
118
|
+
»
|
|
119
|
+
</a>
|
|
120
|
+
</h2>
|