graphiti 2.0.0.beta.7 → 2.0.0.beta.8
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/.git-blame-ignore-revs +2 -0
- data/.github/workflows/ci.yml +55 -3
- data/.github/workflows/docs.yml +1 -1
- data/.github/workflows/notify-example-app.yml +18 -0
- data/.gitignore +2 -0
- data/.standard.yml +6 -0
- data/CHANGELOG.md +45 -0
- data/CONTRIBUTING.md +3 -1
- data/README.md +13 -206
- data/Rakefile +6 -3
- data/UPGRADING.md +1 -1
- data/graphiti.gemspec +8 -4
- data/lib/generators/graphiti/generator_mixin.rb +1 -1
- data/lib/generators/graphiti/install_generator.rb +1 -1
- data/lib/generators/graphiti/resource_generator.rb +6 -3
- data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
- data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
- data/lib/graphiti/audit/report.rb +6 -6
- data/lib/graphiti/audit.rb +3 -4
- data/lib/graphiti/delegates/pagination.rb +2 -2
- data/lib/graphiti/deserializer.rb +3 -3
- data/lib/graphiti/errors.rb +6 -6
- data/lib/graphiti/extensions/temp_id.rb +1 -1
- data/lib/graphiti/query.rb +3 -3
- data/lib/graphiti/rails/context.rb +2 -1
- data/lib/graphiti/rails/controller.rb +3 -0
- data/lib/graphiti/rails/debugging.rb +1 -1
- data/lib/graphiti/rails/responders.rb +2 -1
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/dsl.rb +2 -2
- data/lib/graphiti/resource/links.rb +1 -1
- data/lib/graphiti/resource/persistence.rb +1 -1
- data/lib/graphiti/resource/polymorphism.rb +1 -1
- data/lib/graphiti/resource.rb +2 -2
- data/lib/graphiti/resource_proxy.rb +0 -1
- data/lib/graphiti/scoping/filter.rb +5 -3
- data/lib/graphiti/scoping/paginate.rb +1 -1
- data/lib/graphiti/scoping/sort.rb +1 -1
- data/lib/graphiti/sideload/has_many.rb +1 -1
- data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
- data/lib/graphiti/spec_helpers/matchers.rb +1 -1
- data/lib/graphiti/types.rb +5 -1
- data/lib/graphiti/util/cache_debug.rb +3 -3
- data/lib/graphiti/util/hash.rb +1 -1
- data/lib/graphiti/util/persistence.rb +6 -6
- data/lib/graphiti/util/serializer_relationships.rb +1 -1
- data/lib/graphiti/version.rb +1 -1
- data/lib/tasks/graphiti.rake +1 -1
- metadata +31 -197
- data/docs/concepts/backends-and-models.md +0 -122
- data/docs/concepts/endpoints.md +0 -183
- data/docs/concepts/links.md +0 -212
- data/docs/concepts/overview.md +0 -80
- data/docs/concepts/persisting.md +0 -376
- data/docs/concepts/relationships.md +0 -627
- data/docs/concepts/resources.md +0 -677
- data/docs/getting-started/first-api.md +0 -289
- data/docs/getting-started/installation.md +0 -185
- data/docs/intro.md +0 -307
- data/docs/js/authentication.md +0 -63
- data/docs/js/ddau.md +0 -20
- data/docs/js/extra-params.md +0 -41
- data/docs/js/index.md +0 -112
- data/docs/js/installation.md +0 -120
- data/docs/js/middleware.md +0 -72
- data/docs/js/models.md +0 -202
- data/docs/js/reads.md +0 -494
- data/docs/js/state-syncing.md +0 -100
- data/docs/js/writes.md +0 -373
- data/docs/reference/vandal.md +0 -63
- data/docs/reference/why.md +0 -13
- data/docs/topics/authorization.md +0 -155
- data/docs/topics/caching.md +0 -55
- data/docs/topics/customizing-sideloads.md +0 -156
- data/docs/topics/debugging.md +0 -242
- data/docs/topics/error-handling.md +0 -210
- data/docs/topics/etags.md +0 -46
- data/docs/topics/hopping-relationships.md +0 -149
- data/docs/topics/json-attributes.md +0 -77
- data/docs/topics/openstruct-models.md +0 -50
- data/docs/topics/remote-resources.md +0 -291
- data/docs/topics/testing.md +0 -916
- data/docs/topics/without-activerecord.md +0 -324
- data/docs/tutorial/index.md +0 -58
- data/docs/tutorial/step_0.md +0 -107
- data/docs/tutorial/step_1.md +0 -199
- data/docs/tutorial/step_2.md +0 -312
- data/docs/tutorial/step_3.md +0 -142
- data/docs/tutorial/step_4.md +0 -135
- data/docs/tutorial/step_5.md +0 -69
- data/docs/tutorial/step_6.md +0 -82
- data/docs/tutorial/step_7.md +0 -205
- data/docs/tutorial/step_8.md +0 -128
- data/docs/tutorial/step_9.md +0 -171
- data/docs/upgrading.md +0 -267
- data/website/.gitignore +0 -20
- data/website/README.md +0 -43
- data/website/docusaurus.config.js +0 -197
- data/website/package-lock.json +0 -19474
- data/website/package.json +0 -46
- data/website/sidebars.js +0 -82
- data/website/src/css/custom.css +0 -85
- data/website/src/pages/markdown-page.mdx +0 -7
- data/website/static/.nojekyll +0 -0
- data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
- data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
- data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
- data/website/static/1.13/2019/10/14/tutorial.html +0 -198
- data/website/static/1.13/CNAME +0 -1
- data/website/static/1.13/README.md +0 -16
- data/website/static/1.13/assets/css/syntax.css +0 -60
- 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 +0 -9
- 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 +0 -1
- data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
- 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/fancy-cushion.png +0 -0
- data/website/static/1.13/assets/img/gh.png +0 -0
- data/website/static/1.13/assets/img/home-bg.jpg +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/sunrise.png +0 -0
- data/website/static/1.13/assets/img/why.png +0 -0
- data/website/static/1.13/assets/js/highlight.pack.js +0 -2
- data/website/static/1.13/assets/main.css +0 -15518
- data/website/static/1.13/assets/main.css.map +0 -1
- data/website/static/1.13/bin/bundle +0 -109
- data/website/static/1.13/bin/jekyll +0 -27
- data/website/static/1.13/bin/kramdown +0 -27
- data/website/static/1.13/bin/listen +0 -27
- data/website/static/1.13/bin/rake +0 -27
- data/website/static/1.13/bin/rougify +0 -27
- data/website/static/1.13/bin/safe_yaml +0 -27
- data/website/static/1.13/bin/sass +0 -27
- data/website/static/1.13/bin/sass-convert +0 -27
- data/website/static/1.13/bin/scss +0 -27
- data/website/static/1.13/blog.html +0 -259
- data/website/static/1.13/cheatsheet.html +0 -316
- 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 +0 -325
- data/website/static/1.13/cookbooks/etags.md +0 -0
- data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
- 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 +0 -510
- data/website/static/1.13/features.html +0 -249
- data/website/static/1.13/feed.xml +0 -106
- data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
- data/website/static/1.13/guides/concepts/debugging.html +0 -440
- data/website/static/1.13/guides/concepts/endpoints.html +0 -432
- data/website/static/1.13/guides/concepts/error-handling.html +0 -396
- data/website/static/1.13/guides/concepts/links.html +0 -501
- data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
- data/website/static/1.13/guides/concepts/resources.html +0 -2176
- data/website/static/1.13/guides/concepts/testing.html +0 -1469
- data/website/static/1.13/guides/getting-started/installation.html +0 -420
- data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
- data/website/static/1.13/guides/index.html +0 -269
- data/website/static/1.13/guides/overview.html +0 -325
- data/website/static/1.13/guides/upgrading-2-0.html +0 -193
- data/website/static/1.13/guides/upgrading.html +0 -314
- data/website/static/1.13/guides/vandal.html +0 -282
- data/website/static/1.13/guides/why.html +0 -1121
- data/website/static/1.13/index.html +0 -72
- data/website/static/1.13/js/authentication.html +0 -295
- data/website/static/1.13/js/ddau.html +0 -238
- data/website/static/1.13/js/extra-params.html +0 -270
- data/website/static/1.13/js/index.html +0 -321
- data/website/static/1.13/js/installation.html +0 -637
- data/website/static/1.13/js/introduction.html +0 -257
- data/website/static/1.13/js/middleware.html +0 -318
- data/website/static/1.13/js/reads/fieldsets.html +0 -271
- data/website/static/1.13/js/reads/filtering.html +0 -289
- data/website/static/1.13/js/reads/includes.html +0 -260
- data/website/static/1.13/js/reads/index.html +0 -497
- data/website/static/1.13/js/reads/nested-queries.html +0 -353
- data/website/static/1.13/js/reads/pagination.html +0 -260
- data/website/static/1.13/js/reads/sorting.html +0 -265
- data/website/static/1.13/js/reads/statistics.html +0 -289
- data/website/static/1.13/js/state-syncing.html +0 -340
- data/website/static/1.13/js/writes/deferred.html +0 -296
- data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
- data/website/static/1.13/js/writes/index.html +0 -391
- data/website/static/1.13/js/writes/nested.html +0 -330
- data/website/static/1.13/js/writes/validations.html +0 -272
- data/website/static/1.13/quickstart.html +0 -660
- data/website/static/1.13/template +0 -161
- data/website/static/1.13/tutorial/index.html +0 -250
- data/website/static/1.13/tutorial/step_0.html +0 -292
- data/website/static/1.13/tutorial/step_1.html +0 -517
- data/website/static/1.13/tutorial/step_2.html +0 -481
- data/website/static/1.13/tutorial/step_3.html +0 -323
- data/website/static/1.13/tutorial/step_4.html +0 -318
- data/website/static/1.13/tutorial/step_5.html +0 -265
- data/website/static/1.13/tutorial/step_6.html +0 -276
- data/website/static/1.13/tutorial/step_7.html +0 -390
- data/website/static/1.13/tutorial/step_8.html +0 -316
- data/website/static/1.13/tutorial/step_9.html +0 -365
- data/website/static/CNAME +0 -1
- 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 +0 -1
- data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
- data/website/static/img/undraw_docusaurus_react.svg +0 -170
- data/website/static/img/undraw_docusaurus_tree.svg +0 -40
data/docs/concepts/links.md
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Links'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Links
|
|
6
|
-
|
|
7
|
-
## Overview {#overview}
|
|
8
|
-
|
|
9
|
-
A [Link](http://jsonapi.org/format/#document-links) is a URL Graphiti puts in a relationship, pointing at the data so a client can fetch it separately. Every relationship gets one automatically:
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
class PostResource < ApplicationResource
|
|
13
|
-
has_many :comments
|
|
14
|
-
end
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
`GET /posts/123` renders the `comments` relationship with a `links.related` of `/comments?filter[post_id]=123`. The client follows that URL when it wants the comments, rather than asking for them up front with `?include=comments`.
|
|
18
|
-
|
|
19
|
-
### Why links {#why-links}
|
|
20
|
-
|
|
21
|
-
The URL matters most when the relationship means something more specific than "all the comments". Say `top_comments` is defined as 100 upvotes or more. A [`params` block](#linking-relationships) puts that into the generated Link, and the client still follows a URL.
|
|
22
|
-
|
|
23
|
-
The alternative is for clients to build that query themselves, which means every client (desktop, mobile, third-party) has to know what a "Top Comment" is and ship an update whenever the definition changes. Hiding it behind a dedicated `/top_comments` endpoint moves the problem rather than solving it: clients still have to know to hit a special endpoint, and nothing keeps its definition in sync with the eager-loaded one.
|
|
24
|
-
|
|
25
|
-
With a Link, the definition lives in one place. Change it to 500 upvotes, factor in recency, subtract downvotes: clients keep following the same URL.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## Linking Relationships {#linking-relationships}
|
|
29
|
-
|
|
30
|
-
When defining a relationship, we get a Link for free:
|
|
31
|
-
|
|
32
|
-
```ruby
|
|
33
|
-
class PostResource < ApplicationResource
|
|
34
|
-
has_many :comments
|
|
35
|
-
end
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
> `/comments?filter[post_id]=123`
|
|
39
|
-
|
|
40
|
-
And when customizing a relationship with `params`, our Link will be
|
|
41
|
-
updated:
|
|
42
|
-
|
|
43
|
-
```ruby
|
|
44
|
-
has_many :comments do
|
|
45
|
-
params do |hash|
|
|
46
|
-
hash[:filter][:upvotes] = { gte: 100 }
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
> `/comments?filter[post_id]=123&filter[upvotes][gte]=100`
|
|
52
|
-
|
|
53
|
-
Note: if you use the `scope` block directly, it may cause incorrect links. Avoid using `scope` directly and instead use `params` and `pre_load` if possible.
|
|
54
|
-
|
|
55
|
-
To manually generate a Link:
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
has_many :comments do
|
|
59
|
-
link do |post|
|
|
60
|
-
helpers = Rails.application.routes.url_helpers
|
|
61
|
-
helpers.comments_url(params: { filter: { post_id: post.id } })
|
|
62
|
-
# or
|
|
63
|
-
# http://example.com/api/v1/comments?filter[post_id]=123
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
To avoid a Relationship Link altogether:
|
|
69
|
-
|
|
70
|
-
```ruby
|
|
71
|
-
has_many :comments, link: false
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Resource Endpoints {#resource-endpoints}
|
|
75
|
-
|
|
76
|
-
To generate links, we need to associate a Resource to a URL. By default,
|
|
77
|
-
this happens automatically:
|
|
78
|
-
|
|
79
|
-
```ruby
|
|
80
|
-
class ApplicationResource < Graphiti::Resource
|
|
81
|
-
# ... code ...
|
|
82
|
-
self.endpoint_namespace = '/api/v1'
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
class PostResource < ApplicationResource
|
|
86
|
-
# under the hood:
|
|
87
|
-
primary_endpoint 'posts',
|
|
88
|
-
[:index, :show, :create, :update, :destroy]
|
|
89
|
-
end
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Which would generate links to `/api/v1/posts`.
|
|
93
|
-
|
|
94
|
-
### Validation {#validation}
|
|
95
|
-
|
|
96
|
-
Associating a Resource to an Endpoint serves two purposes. We've gone
|
|
97
|
-
over link generation. But we also want to make sure we're not linking to
|
|
98
|
-
something that doesn't actually exist. That's why we perform **Endpoint
|
|
99
|
-
Validation**.
|
|
100
|
-
|
|
101
|
-
If we tried to access the above resource at a `/comments` endpoint:
|
|
102
|
-
|
|
103
|
-
```ruby
|
|
104
|
-
class CommentsController < ApplicationController
|
|
105
|
-
def index
|
|
106
|
-
PostResource.all(params)
|
|
107
|
-
# ...
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
We'd get a `Graphiti::Errors::InvalidEndpoint` error. Endpoint
|
|
113
|
-
validation ensures that our auto-generated Links are actually valid.
|
|
114
|
-
|
|
115
|
-
To change the endpoint associated to a Resource:
|
|
116
|
-
|
|
117
|
-
```ruby
|
|
118
|
-
primary_endpoint 'special_posts', [:index, :show]
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Or to alter only the **path**:
|
|
122
|
-
|
|
123
|
-
```ruby
|
|
124
|
-
self.endpoint[:path] = 'special_posts'
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Or to alter only the **actions** supported:
|
|
128
|
-
|
|
129
|
-
```ruby
|
|
130
|
-
self.endpoint[:actions] = [:index, :show]
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
A resource may be accessible by multiple endpoints. Maybe `PostResource` is also used at `/top_posts`. We want to keep all auto-generated links pointing to `/posts` (the primary endpoint), but *allow* accessing `PostResource` from the `/top_posts` endpoint:
|
|
134
|
-
|
|
135
|
-
```ruby
|
|
136
|
-
secondary_endpoint '/top_posts', [:index]
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Configuration {#configuration}
|
|
140
|
-
|
|
141
|
-
### Autolinking {#autolinking}
|
|
142
|
-
|
|
143
|
-
To turn off automatically generated links:
|
|
144
|
-
|
|
145
|
-
```ruby
|
|
146
|
-
class ApplicationResource < Graphiti::Resource
|
|
147
|
-
self.autolink = false
|
|
148
|
-
end
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Endpoint Validation {#endpoint-validation}
|
|
152
|
-
|
|
153
|
-
To turn off Endpoint Validation:
|
|
154
|
-
|
|
155
|
-
```ruby
|
|
156
|
-
class ApplicationResource < Graphiti::Resource
|
|
157
|
-
self.validate_endpoints = false
|
|
158
|
-
end
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### Links-on-Demand {#links-on-demand}
|
|
162
|
-
|
|
163
|
-
To only render links when requested in the URL with `?links=true`:
|
|
164
|
-
|
|
165
|
-
```ruby
|
|
166
|
-
Graphiti.configure do |c|
|
|
167
|
-
c.links_on_demand = true
|
|
168
|
-
end
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Pagination Links {#pagination-links}
|
|
172
|
-
|
|
173
|
-
Requesting large collections can make for slow responses. [Pagination](https://jsonapi.org/format/#fetching-pagination) breaks the response into smaller pieces, and pagination links tell the client how to walk them. They can appear in a response two ways.
|
|
174
|
-
|
|
175
|
-
#### Showing by default {#pagination-links-showing-by-default}
|
|
176
|
-
|
|
177
|
-
Every collection response returns pagination links:
|
|
178
|
-
|
|
179
|
-
```ruby
|
|
180
|
-
Graphiti.configure do |c|
|
|
181
|
-
c.pagination_links = true
|
|
182
|
-
end
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
#### When requested {#pagination-links-when-requested}
|
|
186
|
-
|
|
187
|
-
Links are rendered only when the request asks for them with `?pagination_links=true`. Worth doing when the collection is large: the `last` link needs a total count, so rendering links costs a `stat(:total, :count)` on every request that gets them.
|
|
188
|
-
|
|
189
|
-
```ruby
|
|
190
|
-
Graphiti.configure do |c|
|
|
191
|
-
c.pagination_links_on_demand = true
|
|
192
|
-
end
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Pagination links won't show up for *#show* actions.
|
|
196
|
-
|
|
197
|
-
### Custom Endpoint URLs {#custom-endpoint-urls}
|
|
198
|
-
|
|
199
|
-
To change the URL associated with a Resource:
|
|
200
|
-
|
|
201
|
-
```ruby
|
|
202
|
-
class PostResource < ApplicationResource
|
|
203
|
-
# Most commonly seen in ApplicationResource
|
|
204
|
-
self.endpoint_namespace = '/api/v1'
|
|
205
|
-
|
|
206
|
-
primary_endpoint '/posts', [:index, :show]
|
|
207
|
-
# OR
|
|
208
|
-
self.endpoint[:path] = '/posts'
|
|
209
|
-
# OR
|
|
210
|
-
self.endpoint[:actions] = [:index, :show]
|
|
211
|
-
end
|
|
212
|
-
```
|
data/docs/concepts/overview.md
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Lifecycle of a Request'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Lifecycle of a Request
|
|
6
|
-
|
|
7
|
-
A request goes down through a Resource to your data, and comes back up as a serialized response.
|
|
8
|
-
|
|
9
|
-
<figure>
|
|
10
|
-
<svg viewBox="0 0 790 300" role="img" aria-label="A request reaches an Endpoint (your Rails controller), then a Resource. The Resource builds a scope from the request, queries your Backend through an Adapter, resolves the results into Models, and serializes them into the JSON:API response that goes back to the client. The Resource and the JSON:API response are Graphiti. The Endpoint and Backend are yours." style={{width: '100%', height: 'auto'}}>
|
|
11
|
-
<defs>
|
|
12
|
-
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
13
|
-
<path d="M0 0 L10 5 L0 10 z" fill="var(--ifm-color-emphasis-500)"/>
|
|
14
|
-
</marker>
|
|
15
|
-
<marker id="arrowStart" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
16
|
-
<path d="M10 0 L0 5 L10 10 z" fill="var(--ifm-color-emphasis-500)"/>
|
|
17
|
-
</marker>
|
|
18
|
-
</defs>
|
|
19
|
-
|
|
20
|
-
<g fill="none" stroke="var(--ifm-color-emphasis-500)" strokeWidth="1.5">
|
|
21
|
-
<path d="M150 64 H176" markerEnd="url(#arrow)"/>
|
|
22
|
-
<path d="M310 64 H336" markerEnd="url(#arrow)"/>
|
|
23
|
-
<path d="M574 96 H626" markerEnd="url(#arrow)" markerStart="url(#arrowStart)"/>
|
|
24
|
-
<path d="M336 230 H156" markerEnd="url(#arrow)"/>
|
|
25
|
-
<path d="M85 206 V92" markerEnd="url(#arrow)"/>
|
|
26
|
-
</g>
|
|
27
|
-
|
|
28
|
-
<g stroke="var(--ifm-color-emphasis-300)" fill="var(--ifm-background-surface-color)">
|
|
29
|
-
<rect x="20" y="40" width="130" height="48" rx="6"/>
|
|
30
|
-
<rect x="180" y="40" width="130" height="48" rx="6"/>
|
|
31
|
-
<rect x="630" y="70" width="140" height="52" rx="6"/>
|
|
32
|
-
</g>
|
|
33
|
-
<g stroke="var(--ifm-color-primary)" strokeWidth="1.5" fill="none">
|
|
34
|
-
<rect x="340" y="30" width="230" height="230" rx="8"/>
|
|
35
|
-
<rect x="20" y="206" width="130" height="48" rx="6"/>
|
|
36
|
-
</g>
|
|
37
|
-
|
|
38
|
-
<g fontFamily="var(--ifm-font-family-base)" fontSize="15" fill="var(--ifm-font-color-base)" textAnchor="middle">
|
|
39
|
-
<text x="85" y="70">Request</text>
|
|
40
|
-
<text x="245" y="70">Endpoint</text>
|
|
41
|
-
<text x="700" y="102">Backend</text>
|
|
42
|
-
<text x="85" y="236">JSON:API</text>
|
|
43
|
-
</g>
|
|
44
|
-
<g fontFamily="var(--ifm-font-family-base)" fontSize="15" fill="var(--ifm-color-primary)" textAnchor="middle">
|
|
45
|
-
<text x="455" y="62">Resource</text>
|
|
46
|
-
</g>
|
|
47
|
-
|
|
48
|
-
<g fontFamily="var(--ifm-font-family-base)" fontSize="12.5" fill="var(--ifm-color-emphasis-700)" textAnchor="middle">
|
|
49
|
-
<text x="455" y="104">base_scope + filters,</text>
|
|
50
|
-
<text x="455" y="122">sorts, pagination</text>
|
|
51
|
-
<text x="455" y="163">resolve(scope)</text>
|
|
52
|
-
<text x="455" y="181">to your Models</text>
|
|
53
|
-
<text x="455" y="222">serialize</text>
|
|
54
|
-
<text x="600" y="86">Adapter</text>
|
|
55
|
-
</g>
|
|
56
|
-
<g fontFamily="var(--ifm-font-family-base)" fontSize="12.5" fill="var(--ifm-color-emphasis-700)">
|
|
57
|
-
<text x="95" y="152">response</text>
|
|
58
|
-
</g>
|
|
59
|
-
</svg>
|
|
60
|
-
</figure>
|
|
61
|
-
|
|
62
|
-
Graphiti is the highlighted part: the Resource, and the JSON:API it renders. The Endpoint is your Rails controller, which handles routing, response codes and MIME types. The Backend is yours too.
|
|
63
|
-
|
|
64
|
-
| Piece | What it does |
|
|
65
|
-
| --- | --- |
|
|
66
|
-
| [Endpoint](/concepts/endpoints) | Your controller. Graphiti registers its path and actions, which drives link generation and endpoint validation, and lets you vary a Resource's behavior per route. |
|
|
67
|
-
| [Resource](/concepts/resources) | Turns request params into a **scope**, resolves that scope into Models, and serializes them on the way back out. |
|
|
68
|
-
| Adapter | Reusable glue between a Resource and a Backend. Defaults to `Graphiti::Adapters::ActiveRecord`. |
|
|
69
|
-
| [Backend](/concepts/backends-and-models) | Whatever you query: a database, a search index, an HTTP service. |
|
|
70
|
-
| [Model](/concepts/backends-and-models) | What you return and serialize. With ActiveRecord, the same object as the Backend. |
|
|
71
|
-
|
|
72
|
-
## The graph
|
|
73
|
-
|
|
74
|
-
Resources connect to other Resources:
|
|
75
|
-
|
|
76
|
-
* **Sideloading**: fetch an employee, her positions, and those positions' departments in one request
|
|
77
|
-
* **Sideposting**: *save* an employee and her positions in one request
|
|
78
|
-
* **[Links](/concepts/links)**: a URL to lazy-load positions in a separate request
|
|
79
|
-
|
|
80
|
-
Query logic written for one Resource applies at every level of that graph, so you can ask for an employee and her last three positions ordered by `created_at`. That's [deep querying](/concepts/relationships#deep-queries).
|
data/docs/concepts/persisting.md
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Persisting'
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Persisting {#persisting}
|
|
6
|
-
|
|
7
|
-
This page covers how Graphiti writes data: the persistence lifecycle, sideposting a graph of resources in one request, validation errors, and reading data back after a write.
|
|
8
|
-
|
|
9
|
-
Graphiti allows writing a graph of data in a single request. We'll do
|
|
10
|
-
the work of parsing the graph and ordering operations, so you can focus
|
|
11
|
-
on the part you care about: the logic for actually persisting an object.
|
|
12
|
-
|
|
13
|
-
By default, persistence operations are handled by your adapter, and the flow breaks into three steps: build or find the model, assign attributes to it, then save it.
|
|
14
|
-
|
|
15
|
-
Attributes are assigned up front, before the persistence hooks run. That means the model exists (populated but unwritten) before anything touches the database, and **the model you inspect is the model that saves**:
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
employee = EmployeeResource.build(payload)
|
|
19
|
-
|
|
20
|
-
employee.data # the model, attributes already assigned, nothing written yet
|
|
21
|
-
employee.data.valid? # inspect it, or modify it
|
|
22
|
-
employee.save # persists that same instance
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Reading `data` repeatedly returns the same instance, and the attribute callbacks run only once no matter how often you read it. For an update, the proxy reads the persisted record until you apply the payload:
|
|
26
|
-
|
|
27
|
-
```ruby
|
|
28
|
-
proxy = EmployeeResource.find(payload)
|
|
29
|
-
proxy.data.first_name # => "asdf", straight from the database
|
|
30
|
-
proxy.assign_attributes(payload)
|
|
31
|
-
proxy.data.first_name # => "Jane", assigned but not yet persisted
|
|
32
|
-
proxy.save(action: :update)
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
`assign_attributes` validates the payload and runs your writable guards, but writes nothing. `#save` will not re-validate a payload it already validated, so inspecting the model costs no extra guard evaluations. `ResourceProxy#update` is the Rails-style shorthand that assigns and saves in one call.
|
|
36
|
-
|
|
37
|
-
You can override `#create`, `#update` and `#destroy` on a Resource, but you are encouraged **not** to. Use the hooks below instead. If you do override them, `#create` and `#update` receive an attributes hash while `#destroy` receives an id, and all three **must return the Model instance**. Graphiti processes any `writable: false` or guarded attributes before these methods run, and checks the returned Model for validation errors afterward, rolling back the transaction if any Model in the graph is invalid.
|
|
38
|
-
|
|
39
|
-
## Persistence Lifecycle Hooks {#persistence-lifecycle-hooks}
|
|
40
|
-
|
|
41
|
-
Let's dive into a persistence request. If you look at the code snippets in
|
|
42
|
-
the prior section, the flow breaks down into 3 steps:
|
|
43
|
-
|
|
44
|
-
* Build or find the model
|
|
45
|
-
* Assign attributes to the model
|
|
46
|
-
* Save
|
|
47
|
-
|
|
48
|
-
You can hook into each step:
|
|
49
|
-
|
|
50
|
-
```ruby
|
|
51
|
-
class PostResource < ApplicationResource
|
|
52
|
-
before_attributes do |attributes|
|
|
53
|
-
# Before attributes have been assigned to the model
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
after_attributes do |model|
|
|
57
|
-
# After attributes have been assigned to the model
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
around_attributes :do_around_attributes
|
|
61
|
-
|
|
62
|
-
def do_around_attributes(attributes)
|
|
63
|
-
# before
|
|
64
|
-
model_instance = yield attributes
|
|
65
|
-
# after
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
before_save do |model|
|
|
69
|
-
# After attributes assigned, but before persisting
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
after_save do |model|
|
|
73
|
-
# After model has been saved
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
around_save :do_around_save
|
|
77
|
-
|
|
78
|
-
def do_around_save(model)
|
|
79
|
-
# before
|
|
80
|
-
yield model
|
|
81
|
-
# after
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# This is an *override*
|
|
85
|
-
# During #create, build a blank model instance
|
|
86
|
-
# By default, we'd call adapter.build(model_class)
|
|
87
|
-
def build(model_class)
|
|
88
|
-
model_class.new
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# This is an *override*
|
|
92
|
-
# During #create/#update, assign new attributes to the model instance
|
|
93
|
-
# By default, we'd call adapter.assign_attributes(model_instance, attributes)
|
|
94
|
-
def assign_attributes(model_instance, attributes)
|
|
95
|
-
attributes.each_pair do |key, value|
|
|
96
|
-
model_instance.send(:"#{key}=", value)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# This is an *override*
|
|
101
|
-
# During #create/#update, actually save the model instance
|
|
102
|
-
# By default, we'd call adapter.save(model_instance)
|
|
103
|
-
def save(model_instance)
|
|
104
|
-
model_instance.save
|
|
105
|
-
model_instance
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
# This is an *override*
|
|
110
|
-
# During #destroy, actually save the model instance
|
|
111
|
-
# By default, we'd call adapter.destroy(model_instance)
|
|
112
|
-
def delete(model_instance)
|
|
113
|
-
model_instance.destroy
|
|
114
|
-
model_instance
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Finally, you may want to hook around *all* the above steps:
|
|
118
|
-
# Only applies to #create/#update
|
|
119
|
-
around_persistence :do_around_persistence
|
|
120
|
-
|
|
121
|
-
def do_around_persistence(attributes)
|
|
122
|
-
attributes[:foo] = 'bar'
|
|
123
|
-
model = yield # build/find, assign attrs, save
|
|
124
|
-
model.update_counter_cache
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
* All hooks have `only/except` options, e.g. `before_attributes only: [:update]`
|
|
130
|
-
* Most hooks can be called with an in-line block, or by passing a method
|
|
131
|
-
name (e.g. `before_attributes :do_something`). The exception is `around_*` hooks, which *must* be called with a method name.
|
|
132
|
-
|
|
133
|
-
When persisting multiple objects at once, we'll open a database
|
|
134
|
-
transaction, process each model individually, ensure all models pass
|
|
135
|
-
validation, then close the transaction. This means that if you raise an
|
|
136
|
-
error at any point, or any model does not pass validations, the
|
|
137
|
-
transaction will be rolled back.
|
|
138
|
-
|
|
139
|
-
You may want to perform an operation after all models have been
|
|
140
|
-
processed and validated, but before the transaction is closed. One
|
|
141
|
-
example is sending an email - you don't want to send if the models were
|
|
142
|
-
invalid, so `after_save` wouldn't work. And you still want to do it
|
|
143
|
-
*within* the transaction, so if your email server is down and an error
|
|
144
|
-
is raised the transaction gets rolled back.
|
|
145
|
-
|
|
146
|
-
For this scenario, use `before_commit`:
|
|
147
|
-
|
|
148
|
-
```ruby
|
|
149
|
-
before_commit do |model|
|
|
150
|
-
PostMailer.with(post: model).some_email.deliver
|
|
151
|
-
end
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Sideposting {#sideposting}
|
|
155
|
-
|
|
156
|
-
The act of persisting multiple Resources in a single request is called
|
|
157
|
-
**Sideposting**. The payload mirrors the **sideloading** payload for
|
|
158
|
-
read operations, with minor additions.
|
|
159
|
-
|
|
160
|
-
Let's create a Post and associate it to an existing Blog in a single
|
|
161
|
-
request:
|
|
162
|
-
|
|
163
|
-
```ruby
|
|
164
|
-
# POST /api/v1/posts
|
|
165
|
-
{
|
|
166
|
-
type: 'posts',
|
|
167
|
-
attributes: { title: 'My post' },
|
|
168
|
-
relationships: {
|
|
169
|
-
blog: {
|
|
170
|
-
data: {
|
|
171
|
-
id: '1',
|
|
172
|
-
type: 'blogs',
|
|
173
|
-
method: 'update'
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
The critical addition here is the `method` key. When we persist RESTful
|
|
181
|
-
Resources, we send a corresponding HTTP verb. This follows the same
|
|
182
|
-
pattern, adding a verb for each Resource in the graph. `method` can be
|
|
183
|
-
one of:
|
|
184
|
-
|
|
185
|
-
* `create`
|
|
186
|
-
* `update`
|
|
187
|
-
* `destroy`
|
|
188
|
-
* `disassociate` (e.g. `null` foreign key)
|
|
189
|
-
|
|
190
|
-
When we sidepost, all objects will be persisted within the same database
|
|
191
|
-
transaction, which rolls back if an error is raised or any objects are invalid.
|
|
192
|
-
|
|
193
|
-
### Create {#create}
|
|
194
|
-
|
|
195
|
-
Let's say we want to create a Post and its Blog in a single request.
|
|
196
|
-
You'll note that we don't have the `id` key to generate a [Resource Identifier](http://jsonapi.org/format/#document-resource-identifier-objects) (combination of `id` and `type`
|
|
197
|
-
that uniquely identifies a Resource).
|
|
198
|
-
|
|
199
|
-
To accomodate this, send an ephemeral `temp-id` (any UUID):
|
|
200
|
-
|
|
201
|
-
```ruby
|
|
202
|
-
{
|
|
203
|
-
# POST /api/v1/posts
|
|
204
|
-
{
|
|
205
|
-
type: 'posts',
|
|
206
|
-
attributes: { title: 'My post' },
|
|
207
|
-
relationships: {
|
|
208
|
-
blog: {
|
|
209
|
-
data: {
|
|
210
|
-
:'temp-id' => 'abc123',
|
|
211
|
-
type: 'blogs',
|
|
212
|
-
method: 'create'
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
included: [
|
|
217
|
-
{
|
|
218
|
-
:'temp-id' => 'abc123'
|
|
219
|
-
type: 'blogs',
|
|
220
|
-
attributes: { name: 'New Blog' }
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
This random UUID:
|
|
228
|
-
|
|
229
|
-
* Connects relevant sections of the payload.
|
|
230
|
-
* Tells clients how to associate their in-memory objects with the ids returned from the server.
|
|
231
|
-
|
|
232
|
-
### Expanded Example {#expanded-example}
|
|
233
|
-
|
|
234
|
-
Here we're updating a Post, changing the name of its associated Blog, creating a Tag, deleting one Comment, and disassociating (`null` foreign key) a different Comment, all in a single request:
|
|
235
|
-
|
|
236
|
-
```ruby
|
|
237
|
-
{
|
|
238
|
-
data: {
|
|
239
|
-
type: 'posts',
|
|
240
|
-
id: 123,
|
|
241
|
-
attributes: { title: 'Updated!' },
|
|
242
|
-
relationships: {
|
|
243
|
-
blog: {
|
|
244
|
-
data: {
|
|
245
|
-
type: 'blogs',
|
|
246
|
-
id: 123,
|
|
247
|
-
method: 'update'
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
tags: {
|
|
251
|
-
data: [{
|
|
252
|
-
type: 'tags',
|
|
253
|
-
temp-id: 's0m3uu1d',
|
|
254
|
-
method: 'create'
|
|
255
|
-
}]
|
|
256
|
-
},
|
|
257
|
-
comments: {
|
|
258
|
-
data: [
|
|
259
|
-
{
|
|
260
|
-
type: 'comments',
|
|
261
|
-
id: '123',
|
|
262
|
-
method: 'destroy'
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
type: 'comments',
|
|
266
|
-
id: '456',
|
|
267
|
-
method: 'disassociate'
|
|
268
|
-
}
|
|
269
|
-
]
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
included: [
|
|
274
|
-
{
|
|
275
|
-
type: 'tags',
|
|
276
|
-
:'temp-id' => 's0m3uu1d',
|
|
277
|
-
attributes: { name: 'Important' }
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
type: 'blogs',
|
|
281
|
-
id: => '123',
|
|
282
|
-
attributes: { name: 'Updated!' }
|
|
283
|
-
}
|
|
284
|
-
]
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
## Validation Errors {#validation-errors}
|
|
289
|
-
|
|
290
|
-
When a persistence operation is attempted but the corresponding Resource
|
|
291
|
-
is invalid, the transaction will be rolled back and an [errors payload](http://jsonapi.org/format/#errors) will be returned
|
|
292
|
-
with a `422` response code:
|
|
293
|
-
|
|
294
|
-
```ruby
|
|
295
|
-
{
|
|
296
|
-
errors: [{
|
|
297
|
-
code: 'unprocessable_entity',
|
|
298
|
-
status: '422',
|
|
299
|
-
title: "Validation Error",
|
|
300
|
-
detail: "Title can't be blank",
|
|
301
|
-
source: { pointer: '/data/attributes/title' },
|
|
302
|
-
meta: {
|
|
303
|
-
attribute: :title,
|
|
304
|
-
message: "can't be blank",
|
|
305
|
-
code: :blank
|
|
306
|
-
}
|
|
307
|
-
}]
|
|
308
|
-
}
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
To get this functionality, your Model must adhere to the
|
|
312
|
-
[ActiveModel::Validations API](https://api.rubyonrails.org/classes/ActiveModel/Validations.html).
|
|
313
|
-
|
|
314
|
-
You get this for free with ActiveRecord, or it can be mixed in to any
|
|
315
|
-
PORO:
|
|
316
|
-
|
|
317
|
-
```ruby
|
|
318
|
-
class Post
|
|
319
|
-
include ActiveModel::Validations
|
|
320
|
-
validates :title, presence: true
|
|
321
|
-
end
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
Errors on associations will have a slightly expanded payload:
|
|
325
|
-
|
|
326
|
-
```ruby
|
|
327
|
-
{
|
|
328
|
-
errors: [{
|
|
329
|
-
code: 'unprocessable_entity',
|
|
330
|
-
status: '422',
|
|
331
|
-
title: 'Validation Error',
|
|
332
|
-
detail: "Name can't be blank",
|
|
333
|
-
source: { pointer: '/data/attributes/name' },
|
|
334
|
-
meta: {
|
|
335
|
-
relationship: {
|
|
336
|
-
attribute: :name,
|
|
337
|
-
message: "can't be blank",
|
|
338
|
-
code: :blank,
|
|
339
|
-
name: :pets,
|
|
340
|
-
id: '444',
|
|
341
|
-
type: 'pets'
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}]
|
|
345
|
-
}
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
When [Sideposting](#sideposting), the errors payload will contain all
|
|
349
|
-
invalid Resources in the graph.
|
|
350
|
-
|
|
351
|
-
## Read on Write {#read-on-write}
|
|
352
|
-
|
|
353
|
-
By default, the response of a persistence operation will mirror your
|
|
354
|
-
request. But sometimes you need control over the response. The most
|
|
355
|
-
common scenario is sideloading an additional entity - imagine creating
|
|
356
|
-
an order, and wanting the order's shipping information to come back in
|
|
357
|
-
the response.
|
|
358
|
-
|
|
359
|
-
You can do this by POSTing the payload as normal, but adding query
|
|
360
|
-
parameters to the URL:
|
|
361
|
-
|
|
362
|
-
```ruby
|
|
363
|
-
# POST /api/v1/orders?include=shipping_information
|
|
364
|
-
|
|
365
|
-
{
|
|
366
|
-
type: 'orders',
|
|
367
|
-
attributes: { ... }
|
|
368
|
-
}
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
This will sideload the shipping information in the response. When using
|
|
372
|
-
[Spraypaint](/js/), do this with:
|
|
373
|
-
|
|
374
|
-
```typescript
|
|
375
|
-
order.save({ returnScope: Order.includes('shipping_information') })
|
|
376
|
-
```
|