graphiti 2.0.0.beta.4 → 2.0.0.beta.6
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 +5 -0
- data/.github/workflows/docs.yml +4 -7
- data/CHANGELOG.md +29 -0
- data/docs/concepts/links.md +3 -3
- data/docs/concepts/relationships.md +109 -9
- data/docs/topics/debugging.md +26 -0
- data/docs/upgrading.md +18 -17
- data/graphiti.gemspec +1 -19
- data/lib/generators/graphiti/resource_generator.rb +25 -1
- data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
- data/lib/graphiti/audit/report.rb +226 -0
- data/lib/graphiti/audit.rb +231 -0
- data/lib/graphiti/errors.rb +47 -0
- data/lib/graphiti/rails/rake_helpers.rb +42 -0
- data/lib/graphiti/request_validators/validator.rb +10 -1
- data/lib/graphiti/resource/configuration.rb +16 -1
- data/lib/graphiti/resource/polymorphism.rb +10 -0
- data/lib/graphiti/schema.rb +1 -1
- data/lib/graphiti/sideload/belongs_to.rb +22 -29
- data/lib/graphiti/sideload.rb +48 -13
- data/lib/graphiti/spec_helpers/matchers.rb +196 -0
- data/lib/graphiti/spec_helpers/rspec.rb +1 -0
- data/lib/graphiti/spec_helpers.rb +1 -0
- data/lib/graphiti/util/serializer_relationships.rb +27 -4
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +2 -0
- data/lib/tasks/graphiti.rake +16 -32
- data/website/docusaurus.config.js +67 -11
- data/website/src/css/custom.css +29 -2
- data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
- data/website/static/1.13/assets/img/home-bg.jpg +0 -0
- data/website/static/1.13/assets/img/sunrise.png +0 -0
- data/website/static/1.13/assets/main.css +4 -4
- data/website/static/CNAME +1 -0
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e108e5f71103c3fb7f38f5328a890ff131b264201c583509081cfd3106a7b255
|
|
4
|
+
data.tar.gz: 18edd07b0ab429ec89309ec94164a2324a21bbbfbd6dd37f6e49bb6f9707f1c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2affdd800c2966ef15c5d1765b9d1acc48a032c3ee143b9283d69b9545290e227454d77559e25c94b4cd7b0131d0cdc2addbb031a4177155d8085086d818b3b
|
|
7
|
+
data.tar.gz: 79c294b775b8be269194cae7e61cf179a16ec8dd01e8b0b24e7a141d3333683bfbd54032e4c07623f6d2dc3fbe3ce6c721884658da89cbe24bfd9dac935f8e07
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -41,25 +41,30 @@ jobs:
|
|
|
41
41
|
- { ruby: "3.2", gemfile: Gemfile, appraisal: false }
|
|
42
42
|
- { ruby: "3.3", gemfile: Gemfile, appraisal: false }
|
|
43
43
|
- { ruby: "3.4", gemfile: Gemfile, appraisal: false }
|
|
44
|
+
- { ruby: "4.0", gemfile: Gemfile, appraisal: false }
|
|
44
45
|
- { ruby: ruby-head, gemfile: Gemfile, appraisal: false }
|
|
45
46
|
|
|
46
47
|
# Rails 7.1 - the floor
|
|
47
48
|
- { ruby: "3.2", gemfile: gemfiles/rails_7_1.gemfile, appraisal: true }
|
|
48
49
|
- { ruby: "3.4", gemfile: gemfiles/rails_7_1.gemfile, appraisal: true }
|
|
50
|
+
- { ruby: "4.0", gemfile: gemfiles/rails_7_1.gemfile, appraisal: true }
|
|
49
51
|
|
|
50
52
|
# Rails 7.2
|
|
51
53
|
- { ruby: "3.2", gemfile: gemfiles/rails_7_2.gemfile, appraisal: true }
|
|
52
54
|
- { ruby: "3.4", gemfile: gemfiles/rails_7_2.gemfile, appraisal: true }
|
|
55
|
+
- { ruby: "4.0", gemfile: gemfiles/rails_7_2.gemfile, appraisal: true }
|
|
53
56
|
- { ruby: ruby-head, gemfile: gemfiles/rails_7_2.gemfile, appraisal: true }
|
|
54
57
|
|
|
55
58
|
# Rails 8.0
|
|
56
59
|
- { ruby: "3.2", gemfile: gemfiles/rails_8_0.gemfile, appraisal: true }
|
|
57
60
|
- { ruby: "3.4", gemfile: gemfiles/rails_8_0.gemfile, appraisal: true }
|
|
61
|
+
- { ruby: "4.0", gemfile: gemfiles/rails_8_0.gemfile, appraisal: true }
|
|
58
62
|
- { ruby: ruby-head, gemfile: gemfiles/rails_8_0.gemfile, appraisal: true }
|
|
59
63
|
|
|
60
64
|
# Rails 8.1
|
|
61
65
|
- { ruby: "3.2", gemfile: gemfiles/rails_8_1.gemfile, appraisal: true }
|
|
62
66
|
- { ruby: "3.4", gemfile: gemfiles/rails_8_1.gemfile, appraisal: true }
|
|
67
|
+
- { ruby: "4.0", gemfile: gemfiles/rails_8_1.gemfile, appraisal: true }
|
|
63
68
|
- { ruby: ruby-head, gemfile: gemfiles/rails_8_1.gemfile, appraisal: true }
|
|
64
69
|
|
|
65
70
|
continue-on-error: ${{ matrix.ruby == 'ruby-head' }}
|
data/.github/workflows/docs.yml
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
name: Docs
|
|
2
2
|
|
|
3
|
-
# Publishes the documentation site from beta
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# baseUrl returns to "/" and add website/static/CNAME.
|
|
3
|
+
# Publishes the documentation site from beta. graphiti.dev now points at this
|
|
4
|
+
# repo via website/static/CNAME, so this deploy is the live site: the 1.x
|
|
5
|
+
# Jekyll site is frozen under website/static/1.13 and ships with every build,
|
|
6
|
+
# and graphiti-api.github.io no longer serves the domain.
|
|
8
7
|
on:
|
|
9
8
|
push:
|
|
10
9
|
branches: [beta]
|
|
@@ -39,8 +38,6 @@ jobs:
|
|
|
39
38
|
|
|
40
39
|
- name: Build
|
|
41
40
|
working-directory: website
|
|
42
|
-
env:
|
|
43
|
-
DOCS_BASE_URL: /graphiti/
|
|
44
41
|
run: npm run build
|
|
45
42
|
|
|
46
43
|
- name: Publish to gh-pages
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
graphiti changelog
|
|
2
2
|
|
|
3
|
+
# [2.0.0-beta.6](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.5...v2.0.0-beta.6) (2026-08-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* reject invalid page parameters ([#537](https://github.com/graphiti-api/graphiti/issues/537)) ([bb3698b](https://github.com/graphiti-api/graphiti/commit/bb3698b52911c658c5d023ad8fd55b5106195001)), closes [#347](https://github.com/graphiti-api/graphiti/issues/347)
|
|
9
|
+
* treat empty polymorphic configuration as unset ([#538](https://github.com/graphiti-api/graphiti/issues/538)) ([5c17899](https://github.com/graphiti-api/graphiti/commit/5c1789993fbb4638d4443cf014b8bef358addb80)), closes [#199](https://github.com/graphiti-api/graphiti/issues/199)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* Add graphiti:audit task to audit resources for issues ([84436ea](https://github.com/graphiti-api/graphiti/commit/84436eab7c7d0713ec6f355b62ba8b06913ee31a))
|
|
15
|
+
* raise MissingRelationshipMethod when rendering reads an association the model does not define ([1c41cee](https://github.com/graphiti-api/graphiti/commit/1c41cee787b4f747d2906b2230f8fb926e3d887d))
|
|
16
|
+
* rename always_include_resource_ids to resource_ids, with a belongs_to_resource_ids_by_default setting ([8b11151](https://github.com/graphiti-api/graphiti/commit/8b111516822f6fc6e3675b59941688791cb34e74))
|
|
17
|
+
|
|
18
|
+
# [2.0.0-beta.5](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2026-08-07)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* drop the relationship guard install notice ([c204312](https://github.com/graphiti-api/graphiti/commit/c20431214e126cb8e72c82022ee4c852b10a024b))
|
|
24
|
+
* keep rake task helpers out of the global namespace ([0a41a60](https://github.com/graphiti-api/graphiti/commit/0a41a6022fa4c9151948714c31db367aee330e22)), closes [graphiti-api/graphiti-rails#91](https://github.com/graphiti-api/graphiti-rails/issues/91)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* add rspec matchers for resource relationships and attributes ([70525f7](https://github.com/graphiti-api/graphiti/commit/70525f7e10c511aa0c867ecc873f63f1790b8247)), closes [graphiti-api/graphiti_spec_helpers#14](https://github.com/graphiti-api/graphiti_spec_helpers/issues/14)
|
|
30
|
+
* let the resource generator name the controller ([6ea7141](https://github.com/graphiti-api/graphiti/commit/6ea714181f3da011cae9f42477b4c3681a7a90ff)), closes [graphiti-api/graphiti-rails#53](https://github.com/graphiti-api/graphiti-rails/issues/53)
|
|
31
|
+
|
|
3
32
|
# [2.0.0-beta.4](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2026-08-07)
|
|
4
33
|
|
|
5
34
|
|
data/docs/concepts/links.md
CHANGED
|
@@ -170,11 +170,11 @@ end
|
|
|
170
170
|
|
|
171
171
|
### Pagination Links {#pagination-links}
|
|
172
172
|
|
|
173
|
-
Requesting
|
|
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
174
|
|
|
175
175
|
#### Showing by default {#pagination-links-showing-by-default}
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
Every collection response returns pagination links:
|
|
178
178
|
|
|
179
179
|
```ruby
|
|
180
180
|
Graphiti.configure do |c|
|
|
@@ -184,7 +184,7 @@ end
|
|
|
184
184
|
|
|
185
185
|
#### When requested {#pagination-links-when-requested}
|
|
186
186
|
|
|
187
|
-
|
|
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
188
|
|
|
189
189
|
```ruby
|
|
190
190
|
Graphiti.configure do |c|
|
|
@@ -70,10 +70,20 @@ has_many :positions,
|
|
|
70
70
|
writable: true,
|
|
71
71
|
link: self.autolink, # default true
|
|
72
72
|
single: false, # only allow this sideload when one employee
|
|
73
|
-
|
|
73
|
+
resource_ids: false
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
`
|
|
76
|
+
`resource_ids` is the one whose default depends on the relationship type:
|
|
77
|
+
|
|
78
|
+
| type | renders resource ids by default |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `belongs_to` | yes, when its foreign key already holds the related id |
|
|
81
|
+
| `has_one` | no |
|
|
82
|
+
| `has_many` | no |
|
|
83
|
+
| `many_to_many` | no |
|
|
84
|
+
| `polymorphic_belongs_to` | no |
|
|
85
|
+
|
|
86
|
+
`belongs_to` renders them so a client can see which record a relationship points at without following the link:
|
|
77
87
|
|
|
78
88
|
```json
|
|
79
89
|
"employee": {
|
|
@@ -82,19 +92,109 @@ has_many :positions,
|
|
|
82
92
|
}
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
That costs nothing, because the id is already on the parent as its foreign key.
|
|
95
|
+
That costs nothing, because the id is already on the parent as its foreign key.
|
|
96
|
+
|
|
97
|
+
No other relationship type has a free source for its ids. A collection accepts `resource_ids: true`, but that reads the association on every render of every parent record, whether or not the request wants the relationship. That is the N+1 from [#167](https://github.com/graphiti-api/graphiti/issues/167#issuecomment-686866646) on every response. Leave collections off and let clients `?include=` them.
|
|
86
98
|
|
|
87
|
-
|
|
99
|
+
Not every `belongs_to` can use its foreign key. A `scope` or `params` block or a `base_scope` can filter out the record the key points at, a polymorphic target's type varies per record while rendered ids carry one type for the whole relationship, a remote resource has no local key to read, and a custom `primary_key` points the relationship at some other column. Those load the association instead, so they stay off by default too.
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
<details>
|
|
102
|
+
<summary>Which `belongs_to` declarations render resource ids, and which do not</summary>
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# yes. employee_id is the employee's id, so the payload already has it
|
|
106
|
+
belongs_to :employee
|
|
107
|
+
|
|
108
|
+
# no. nothing renders at all, ids included
|
|
109
|
+
belongs_to :employee, readable: false
|
|
110
|
+
|
|
111
|
+
# no. employee_id holds a name, not the related id
|
|
112
|
+
belongs_to :employee, primary_key: :first_name
|
|
113
|
+
|
|
114
|
+
# no. the base scope can exclude the employee the key points at, and
|
|
115
|
+
# graphiti cannot know whether it does without running it
|
|
116
|
+
belongs_to :employee, base_scope: -> { Employee.all }
|
|
117
|
+
|
|
118
|
+
# no. a remote resource has no local foreign key to read
|
|
119
|
+
belongs_to :employee, remote: "http://foo.com/employees"
|
|
120
|
+
|
|
121
|
+
# no. the record's own class decides its type, so the key gives an id
|
|
122
|
+
# with no type to pair it with
|
|
123
|
+
belongs_to :employee, resource: CreditCardResource
|
|
124
|
+
|
|
125
|
+
# no. the scope can exclude the employee the key points at, and graphiti
|
|
126
|
+
# cannot know whether it does without running it
|
|
127
|
+
belongs_to :employee do
|
|
128
|
+
scope { |ids| {type: :employees, conditions: {id: ids}} }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# no. same, a params filter can exclude the employee the key points at
|
|
132
|
+
belongs_to :employee do
|
|
133
|
+
params { |hash, positions| hash[:filter][:active] = true }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# no. credit_card_type is local, but rendered ids carry one type for the
|
|
137
|
+
# whole relationship and this one's varies per record
|
|
138
|
+
polymorphic_belongs_to :credit_card do
|
|
139
|
+
group_by(:credit_card_type) do
|
|
140
|
+
on(:Visa).belongs_to :visa, resource: VisaResource
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Watch for the `scope`, `params` and `base_scope` cases. Nothing about those declarations looks like it concerns resource ids, so adding a scope block to filter a relationship also stops its ids from rendering.
|
|
146
|
+
|
|
147
|
+
If you keep a `schema.json`, the schema check catches that. A relationship that renders resource ids is marked `linkage: true`, and one that stops rendering them is reported as a breaking change. Gaining them is additive and passes.
|
|
148
|
+
|
|
149
|
+
To render ids anyway, opt in on the relationship and accept the query:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
belongs_to :employee, resource_ids: true do
|
|
153
|
+
scope { |ids| {type: :employees, conditions: {id: ids}} }
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Know what that buys for the `scope`, `params` and `base_scope` cases. Rendering reads the association off the model, which does not apply the block, so if the block narrows what sideloading returns, the ids will disagree with it. Opting in there says you know the two agree. A `primary_key`, polymorphic or remote relationship does resolve to the right id this way.
|
|
158
|
+
|
|
159
|
+
</details>
|
|
160
|
+
|
|
161
|
+
### belongs_to_resource_ids_by_default {#belongs-to-resource-ids}
|
|
162
|
+
|
|
163
|
+
To change how far a `belongs_to` goes, across a whole API, set it on the resource everything inherits from:
|
|
90
164
|
|
|
91
165
|
```ruby
|
|
92
166
|
class ApplicationResource < Graphiti::Resource
|
|
93
|
-
self.
|
|
167
|
+
self.belongs_to_resource_ids_by_default = :foreign_key
|
|
94
168
|
end
|
|
95
169
|
```
|
|
96
170
|
|
|
97
|
-
|
|
171
|
+
| | |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `:foreign_key` | Default. Render resource ids wherever the foreign key already holds the related id, and never run an extra query. |
|
|
174
|
+
| `:always` | Render them for every `belongs_to`, loading the association when the foreign key cannot answer. A query per record, per relationship, on every render. |
|
|
175
|
+
| `:never` | Render none. This is the 1.x payload. |
|
|
176
|
+
|
|
177
|
+
Subclasses inherit it, and a relationship passing `resource_ids` explicitly still wins.
|
|
178
|
+
|
|
179
|
+
All three describe requests that do not include the relationship. A relationship the request does include renders its ids whatever this is set to, `:never` included, because the records are already loaded and sitting in `included`.
|
|
180
|
+
|
|
181
|
+
Before flipping the setting, [`bin/rake graphiti:audit`](/topics/debugging#graphiti-audit) reports how every relationship renders resource ids today and which would start loading the association.
|
|
182
|
+
|
|
183
|
+
#### What a client sees {#relationship-payload-shapes}
|
|
184
|
+
|
|
185
|
+
A client never has to work out which rule applied. The relationship object says what it knows:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
"employee": { "data": { "type": "employees", "id": "1" } } // here is the id
|
|
189
|
+
"employee": { "links": { "related": "..." } } // fetch it yourself
|
|
190
|
+
"employee": { "meta": { "included": false } } // neither
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The last shape appears only when a relationship has no ids **and** no link, which usually means `link: false`. It is not a general "was this sideloaded" flag. Relationships are autolinked by default, so the link shape is the one you normally see.
|
|
194
|
+
|
|
195
|
+
The setting covers `belongs_to` and `polymorphic_belongs_to`, and no collection, deliberately. An API-wide `:always` on collections would be the N+1 from [#167](https://github.com/graphiti-api/graphiti/issues/167#issuecomment-686866646) applied everywhere at once.
|
|
196
|
+
|
|
197
|
+
`:always` renders ids by loading the association, so a relationship naming a method the model does not have raises on every render once you set it.
|
|
98
198
|
|
|
99
199
|
### Conditional Relationships {#conditional-relationships}
|
|
100
200
|
|
|
@@ -221,7 +321,7 @@ Defaults to these common options:
|
|
|
221
321
|
has_many :positions,
|
|
222
322
|
foreign_key: :employee_id,
|
|
223
323
|
primary_key: :id,
|
|
224
|
-
|
|
324
|
+
resource_ids: false,
|
|
225
325
|
resource: PositionResource
|
|
226
326
|
```
|
|
227
327
|
|
|
@@ -265,7 +365,7 @@ Defaults to these common options:
|
|
|
265
365
|
belongs_to :employee,
|
|
266
366
|
foreign_key: :employee_id,
|
|
267
367
|
primary_key: :id,
|
|
268
|
-
|
|
368
|
+
resource_ids: true,
|
|
269
369
|
resource: EmployeeResource
|
|
270
370
|
```
|
|
271
371
|
|
data/docs/topics/debugging.md
CHANGED
|
@@ -172,6 +172,32 @@ Which add Debugger output as well.
|
|
|
172
172
|
The `PATH` should not contain the domain unless you want to hit a live
|
|
173
173
|
API instead of a test server.
|
|
174
174
|
|
|
175
|
+
#### graphiti:audit {#graphiti-audit}
|
|
176
|
+
|
|
177
|
+
> `bin/rake graphiti:audit`
|
|
178
|
+
|
|
179
|
+
Audits every relationship declared across your resources. It reports anything that will raise at request time, relationships that load an association just to render resource ids, and `belongs_to` relationships that render no ids unless included. A checklist at the end shows what was checked:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
ERROR will raise when the relationship is included: the model has no association method
|
|
183
|
+
|
|
184
|
+
EmployeeResource
|
|
185
|
+
has_many :positions Employee has no #positions method
|
|
186
|
+
|
|
187
|
+
fix: define it, point the relationship at the real association with `as:`, or remove the relationship
|
|
188
|
+
|
|
189
|
+
checks
|
|
190
|
+
|
|
191
|
+
✓ all relationships inspectable
|
|
192
|
+
✗ 1 association method missing
|
|
193
|
+
✓ all readable guards defined
|
|
194
|
+
✓ all sideload filters declared
|
|
195
|
+
|
|
196
|
+
graphiti: 12 resources, 40 relationships, 1 error.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The task exits nonzero when there are errors, so it can hold the line in CI. Run it before and after flipping [belongs_to_resource_ids_by_default](/concepts/relationships#belongs-to-resource-ids) to see exactly what the setting changes.
|
|
200
|
+
|
|
175
201
|
#### graphiti:benchmark {#graphiti-benchmark}
|
|
176
202
|
|
|
177
203
|
> `bin/rake graphiti:benchmark[PATH,NUM_REQUESTS]`
|
data/docs/upgrading.md
CHANGED
|
@@ -13,9 +13,8 @@ Four things, and three of them fail loudly if you skip them.
|
|
|
13
13
|
|
|
14
14
|
**1. Drop three gems.** `graphiti-rails`, `graphiti_spec_helpers` and `graphiti_errors` are now part of `graphiti` itself.
|
|
15
15
|
|
|
16
|
-
```diff
|
|
17
|
-
#
|
|
18
|
-
+ gem "graphiti"
|
|
16
|
+
```diff title="Gemfile"
|
|
17
|
+
+ gem "graphiti", "~> 2.0.0.beta" # follows the betas and picks up 2.0 final when it ships
|
|
19
18
|
- gem "graphiti-rails"
|
|
20
19
|
- gem "graphiti_spec_helpers"
|
|
21
20
|
- gem "graphiti_errors"
|
|
@@ -31,18 +30,14 @@ class ApplicationController < ActionController::Base
|
|
|
31
30
|
end
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
If the controller already has `include Graphiti::Rails`, replace that line rather than adding to it. The old spelling still sets the controller up, so nothing breaks mid-upgrade, but it warns and goes away in the next major version.
|
|
33
|
+
If the controller already has `include Graphiti::Rails`, replace it with `include Graphiti::Rails::Controller`.
|
|
37
34
|
|
|
38
35
|
<details>
|
|
39
36
|
<summary>What the include actually brings, and what a controller without it loses</summary>
|
|
40
37
|
|
|
41
38
|
Until 2.0, Graphiti added itself to **every** controller in the application: an `around_action` wrapping each request in a Graphiti context, another wrapping it in the debugger, and a catch-all exception handler, on Devise controllers, admin controllers, HTML pages, everything.
|
|
42
39
|
|
|
43
|
-
`Graphiti::Rails::Controller` now bundles all of it, and
|
|
44
|
-
|
|
45
|
-
A controller without it gets no Graphiti context, no debugger, and none of Graphiti's exception handlers, so if a resource action sees an empty `Graphiti.context`, this include is what is missing.
|
|
40
|
+
`Graphiti::Rails::Controller` now bundles all of it, and including it is required. Including it in `ApplicationController` matches 1.x behavior. Including it in an API base class scopes it and leaves the rest of the app alone. A controller without it gets no Graphiti context, no debugger, and none of Graphiti's exception handlers, so if a resource action sees an empty `Graphiti.context`, this include is what is missing.
|
|
46
41
|
|
|
47
42
|
The class-level DSL travels with it, which is the one failure you see before a request is ever served:
|
|
48
43
|
|
|
@@ -133,9 +128,9 @@ It has to be a request spec. Exceptions are rendered in Rack middleware, which c
|
|
|
133
128
|
Nothing to do here. These change what a client receives or when a callback runs, and none of them warns you, because none of them is a rename.
|
|
134
129
|
|
|
135
130
|
<details>
|
|
136
|
-
<summary>A `belongs_to` renders resource
|
|
131
|
+
<summary>A `belongs_to` renders resource ids when its foreign key already holds them, where 1.x sent only a link</summary>
|
|
137
132
|
|
|
138
|
-
A `belongs_to` now
|
|
133
|
+
A `belongs_to` now renders resource ids in the payload by default, where 1.x sent only a link:
|
|
139
134
|
|
|
140
135
|
```json
|
|
141
136
|
"employee": { "data": { "type": "employees", "id": "1" }, "links": { "related": "..." } }
|
|
@@ -143,27 +138,29 @@ A `belongs_to` now includes resource linkage in the payload by default, where 1.
|
|
|
143
138
|
|
|
144
139
|
The id comes from the foreign key already on the parent, so this costs no extra queries, and clients can resolve the relationship against data they already hold instead of following the link. `has_many` is unchanged, since answering there means a query per record.
|
|
145
140
|
|
|
146
|
-
Not every `belongs_to` qualifies. A
|
|
141
|
+
Not every `belongs_to` qualifies. A remote target or a custom `primary_key` mean the foreign key is not the related id, a polymorphic target means one rendered type cannot cover every record, and a `scope` or `params` block or a `base_scope` mean the key might not survive the filter. Rendering ids for those means loading the association, so they stay opt-in as in 1.x and render nothing until you ask.
|
|
147
142
|
|
|
148
|
-
|
|
143
|
+
Run [`bin/rake graphiti:audit`](/topics/debugging#graphiti-audit) to see where your API stands: it lists every relationship that renders no ids, and why.
|
|
149
144
|
|
|
150
145
|
To go back to the old payload for one relationship:
|
|
151
146
|
|
|
152
147
|
```ruby
|
|
153
|
-
belongs_to :employee,
|
|
148
|
+
belongs_to :employee, resource_ids: false
|
|
154
149
|
```
|
|
155
150
|
|
|
156
|
-
Or for the whole API, on the resource everything inherits from
|
|
151
|
+
Or for the whole API, on the resource everything inherits from:
|
|
157
152
|
|
|
158
153
|
```ruby
|
|
159
154
|
class ApplicationResource < Graphiti::Resource
|
|
160
155
|
self.abstract_class = true
|
|
161
156
|
|
|
162
|
-
self.
|
|
157
|
+
self.belongs_to_resource_ids_by_default = :never
|
|
163
158
|
end
|
|
164
159
|
```
|
|
165
160
|
|
|
166
|
-
|
|
161
|
+
If you carry the `Sideload::BelongsTo` monkey patch from [#167](https://github.com/graphiti-api/graphiti/issues/167), delete it and set nothing. The default now covers the safe cases on its own. To force ids onto the rest the way the patch did, set `self.belongs_to_resource_ids_by_default = :always`, at a query per record for each one.
|
|
162
|
+
|
|
163
|
+
The three settings, and when a `belongs_to` cannot use its foreign key, are covered in [Customizing Relationships](/concepts/relationships#belongs-to-resource-ids).
|
|
167
164
|
|
|
168
165
|
</details>
|
|
169
166
|
|
|
@@ -229,6 +226,7 @@ Every name below still works, warns, and will be removed in the next major. They
|
|
|
229
226
|
| `context_namespace` | `current_action` |
|
|
230
227
|
| `Graphiti::Rails::DEPRECATOR` | `Graphiti::DEPRECATOR` (the old name still resolves) |
|
|
231
228
|
| `require "graphiti_errors"`, `require "graphiti/responders"` | remove / no longer needed |
|
|
229
|
+
| `always_include_resource_ids: true` on a relationship | `resource_ids: true` |
|
|
232
230
|
|
|
233
231
|
`RSpec.describe PostResource, type: :resource` still picks up the resource-testing context automatically. That has not changed.
|
|
234
232
|
|
|
@@ -239,6 +237,9 @@ Every name below still works, warns, and will be removed in the next major. They
|
|
|
239
237
|
| `include GraphitiErrors` | `register_exception` is available on every controller |
|
|
240
238
|
| `GraphitiErrors::ExceptionHandler` | subclass `Graphiti::Rails::ExceptionHandler` |
|
|
241
239
|
| `GraphitiErrors.enable!` / `.disable!` | `handle_request_exceptions` |
|
|
240
|
+
| `self.always_include_resource_ids_by_default` | `self.belongs_to_resource_ids_by_default`, which takes `:foreign_key`, `:always` or `:never` |
|
|
241
|
+
|
|
242
|
+
`always_include_resource_ids_by_default` only ever shipped in `2.0.0.beta.4`, so it is gone rather than deprecated and raises `NoMethodError` at class-definition time. It applied to every relationship type, and only a `belongs_to` can render resource ids without loading an association, so the replacement covers `belongs_to` alone. `= false` becomes `:never`. There is no equivalent of `= true`, because arming every collection API-wide is the behavior it was removed for. Use `:always` for `belongs_to`.
|
|
242
243
|
|
|
243
244
|
## Without Rails {#without-rails}
|
|
244
245
|
|
data/graphiti.gemspec
CHANGED
|
@@ -18,31 +18,13 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.require_paths = ["lib"]
|
|
19
19
|
spec.required_ruby_version = ">= 3.2"
|
|
20
20
|
|
|
21
|
-
# TODO: remove once the 1.12 upgrade window has passed
|
|
22
|
-
spec.post_install_message = <<~MSG
|
|
23
|
-
Graphiti: relationship readable:/writable: guards are now enforced.
|
|
24
|
-
|
|
25
|
-
Symbols, strings, and procs passed to a relationship's readable/writable
|
|
26
|
-
guards were always interpreted as true and never actually called before this
|
|
27
|
-
conditional relationship change. They are now evaluated per-request, where
|
|
28
|
-
unreadable relationships are omitted from responses and includes, and unwritable
|
|
29
|
-
relationships reject writes.
|
|
30
|
-
|
|
31
|
-
To list every affected relationship in your app:
|
|
32
|
-
|
|
33
|
-
bin/rails runner 'puts Graphiti.guarded_relationships'
|
|
34
|
-
|
|
35
|
-
Audit these before deploying. Apps using schema.json will also see affected
|
|
36
|
-
relationships flagged as "became guarded" by the schema check.
|
|
37
|
-
MSG
|
|
38
|
-
|
|
39
21
|
spec.add_dependency "jsonapi-serializable", "~> 0.3.0"
|
|
40
22
|
spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2"
|
|
41
23
|
spec.add_dependency "dry-types", ">= 0.15.0", "< 2.0"
|
|
42
24
|
spec.add_dependency "rescue_registry", "~> 1.0"
|
|
43
25
|
spec.add_dependency "concurrent-ruby", ">= 1.2", "< 2.0"
|
|
44
26
|
spec.add_dependency "activesupport", ">= 7.1"
|
|
45
|
-
# Bundled (no longer default) as of Ruby
|
|
27
|
+
# Bundled (no longer default) as of Ruby 4.0; graphiti uses OpenStruct in lib/
|
|
46
28
|
spec.add_dependency "ostruct", ">= 0.5"
|
|
47
29
|
|
|
48
30
|
spec.add_development_dependency "faraday", "~> 0.15"
|
|
@@ -32,6 +32,12 @@ module Graphiti
|
|
|
32
32
|
aliases: ["--model", "-m"],
|
|
33
33
|
desc: "Specify to use attributes from a particular model"
|
|
34
34
|
|
|
35
|
+
# No short alias: -c is already --omit-comments.
|
|
36
|
+
class_option :controller,
|
|
37
|
+
banner: "Name",
|
|
38
|
+
type: :string,
|
|
39
|
+
desc: "Generate the controller under this name instead of the resource's, e.g. Api::V1::Posts"
|
|
40
|
+
|
|
35
41
|
desc "This generator creates a resource file at app/resources, as well as corresponding controller/specs/route/etc"
|
|
36
42
|
def generate_all
|
|
37
43
|
generate_model
|
|
@@ -125,8 +131,23 @@ module Graphiti
|
|
|
125
131
|
defined?(::Responders)
|
|
126
132
|
end
|
|
127
133
|
|
|
134
|
+
# Api::V1::Posts, Api::V1::PostsController and api/v1/posts all name the
|
|
135
|
+
# same controller. Normalizing here means the class name, the file path and
|
|
136
|
+
# the route all come from one place.
|
|
137
|
+
def controller_class_name
|
|
138
|
+
@controller_class_name ||= begin
|
|
139
|
+
given = options[:controller]
|
|
140
|
+
base = given ? given.sub(/Controller\z/, "").camelize : model_klass.name
|
|
141
|
+
"#{base.pluralize}Controller"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def controller_path
|
|
146
|
+
controller_class_name.sub(/Controller\z/, "").underscore
|
|
147
|
+
end
|
|
148
|
+
|
|
128
149
|
def generate_controller
|
|
129
|
-
to = File.join("app/controllers",
|
|
150
|
+
to = File.join("app/controllers", "#{controller_path}_controller.rb")
|
|
130
151
|
template("controller.rb.erb", to)
|
|
131
152
|
end
|
|
132
153
|
|
|
@@ -142,6 +163,9 @@ module Graphiti
|
|
|
142
163
|
|
|
143
164
|
def generate_route
|
|
144
165
|
code = "resources :#{file_name.pluralize}"
|
|
166
|
+
# Rails would otherwise infer the controller from the route name, which
|
|
167
|
+
# is only right when the controller was named after the resource.
|
|
168
|
+
code << %(, controller: "#{controller_path}") if options[:controller]
|
|
145
169
|
code << %(, only: [#{actions.map { |a| ":#{a}" }.join(", ")}]) if actions.length < 5
|
|
146
170
|
code << "\n"
|
|
147
171
|
inject_into_file "config/routes.rb", after: /ApplicationResource.*$\n/ do
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<% module_namespacing do -%>
|
|
2
|
-
class <%=
|
|
2
|
+
class <%= controller_class_name %> < ApplicationController
|
|
3
3
|
<%- if actions?('index') -%>
|
|
4
4
|
def index
|
|
5
5
|
<%= file_name.pluralize %> = <%= resource_klass %>.all(params)
|