graphiti 2.0.0.beta.5 → 2.0.0.beta.7
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/CHANGELOG.md +24 -0
- data/docs/concepts/relationships.md +109 -9
- data/docs/topics/debugging.md +26 -0
- data/docs/topics/testing.md +44 -22
- data/docs/upgrading.md +15 -8
- data/graphiti.gemspec +1 -1
- data/lib/generators/graphiti/install_generator.rb +0 -1
- data/lib/generators/graphiti/templates/index_request_spec.rb.erb +2 -2
- data/lib/generators/graphiti/templates/resource_reads_spec.rb.erb +4 -4
- data/lib/generators/graphiti/templates/show_request_spec.rb.erb +2 -2
- 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/query.rb +24 -5
- data/lib/graphiti/request_validators/validator.rb +10 -1
- data/lib/graphiti/resource/configuration.rb +16 -1
- data/lib/graphiti/resource/interface.rb +3 -3
- data/lib/graphiti/resource/polymorphism.rb +10 -0
- data/lib/graphiti/runner.rb +8 -3
- data/lib/graphiti/schema.rb +1 -1
- data/lib/graphiti/scope.rb +13 -0
- data/lib/graphiti/sideload/belongs_to.rb +22 -29
- data/lib/graphiti/sideload.rb +48 -13
- data/lib/graphiti/spec_helpers.rb +5 -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 +9 -0
- data/package.json +1 -0
- data/website/sidebars.js +4 -4
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acced292af02bc2826f89f6e1d9388bcc7fdedabada42e4cc65d1bfaf9f713f5
|
|
4
|
+
data.tar.gz: 79f966dc31ccb9b3aee687e7dc31a92e943e17845b6017258e366bebee066446
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22584fd2a83e017219fb8202c965d3d295ac341fed01b2e543355c32722cae741866f3188970ee480018d6063c78b06281045f22934c42b9645de72c6cc9562f
|
|
7
|
+
data.tar.gz: 502371f38ad71f5e901b57ba187be70bb4d7c85c94544324e825207bc03f120fa652d0859fe56744efbdbcf826376dad84a4917444c62a7e30193482bca47700
|
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
graphiti changelog
|
|
2
2
|
|
|
3
|
+
# [2.0.0-beta.7](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.6...v2.0.0-beta.7) (2026-08-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* dedupe under concurrency and across distinct resources ([800b1a2](https://github.com/graphiti-api/graphiti/commit/800b1a2bf9bb59702efd00afe8e4f54ae06b500e))
|
|
9
|
+
* deduplicate sideloaded entities across include paths ([0c2829a](https://github.com/graphiti-api/graphiti/commit/0c2829a634e9b96e2403ff168be9a48c6a663c78))
|
|
10
|
+
* deprecate SpecHelpers::Sugar in favor of the full helper names ([b155849](https://github.com/graphiti-api/graphiti/commit/b155849f55fe84b2d0911fa4681d82682dfa39ae))
|
|
11
|
+
|
|
12
|
+
# [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)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* 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)
|
|
18
|
+
* 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)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* Add graphiti:audit task to audit resources for issues ([84436ea](https://github.com/graphiti-api/graphiti/commit/84436eab7c7d0713ec6f355b62ba8b06913ee31a))
|
|
24
|
+
* raise MissingRelationshipMethod when rendering reads an association the model does not define ([1c41cee](https://github.com/graphiti-api/graphiti/commit/1c41cee787b4f747d2906b2230f8fb926e3d887d))
|
|
25
|
+
* 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))
|
|
26
|
+
|
|
3
27
|
# [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)
|
|
4
28
|
|
|
5
29
|
|
|
@@ -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/topics/testing.md
CHANGED
|
@@ -134,7 +134,6 @@ require 'graphiti/spec_helpers/rspec'
|
|
|
134
134
|
RSpec.configure do |config|
|
|
135
135
|
config.include FactoryBot::Syntax::Methods
|
|
136
136
|
config.include Graphiti::SpecHelpers::RSpec
|
|
137
|
-
config.include Graphiti::SpecHelpers::Sugar
|
|
138
137
|
config.include Graphiti::Rails::TestHelpers, type: :request
|
|
139
138
|
|
|
140
139
|
# Clean your DB between test runs
|
|
@@ -163,8 +162,6 @@ These helpers ship with Graphiti, under `Graphiti::SpecHelpers`.
|
|
|
163
162
|
|
|
164
163
|
### #jsonapi_data {#jsonapi-data}
|
|
165
164
|
|
|
166
|
-
> Note: for brevity, this method is aliased to `d`
|
|
167
|
-
|
|
168
165
|
The `jsonapi_data` method will parse response data and return a normalized object (`Graphiti::SpecHelpers::Node`). Assert against this the same way you assert against JSON:
|
|
169
166
|
|
|
170
167
|
```ruby
|
|
@@ -183,7 +180,7 @@ expect(data.first_name).to eq('Jane')
|
|
|
183
180
|
To grab a relationship:
|
|
184
181
|
|
|
185
182
|
```ruby
|
|
186
|
-
sideload =
|
|
183
|
+
sideload = jsonapi_data[0].sideload(:comments)
|
|
187
184
|
expect(sideload.id).to eq(123)
|
|
188
185
|
expect(sideload.jsonapi_type).to eq('comments')
|
|
189
186
|
expect(sideload.body).to eq('body')
|
|
@@ -196,7 +193,7 @@ The `sideload` method accepts the *name of the relationship*. It returns a norma
|
|
|
196
193
|
To grab a Link:
|
|
197
194
|
|
|
198
195
|
```ruby
|
|
199
|
-
|
|
196
|
+
jsonapi_data[0].link(:comments, :related)
|
|
200
197
|
```
|
|
201
198
|
|
|
202
199
|
This accepts the relationship name and the link type. It will return the link URL.
|
|
@@ -205,28 +202,26 @@ This accepts the relationship name and the link type. It will return the link UR
|
|
|
205
202
|
|
|
206
203
|
To see the raw JSON response, use `json`.
|
|
207
204
|
|
|
208
|
-
### #
|
|
205
|
+
### #json_date and #json_datetime {#date-and-datetime}
|
|
209
206
|
|
|
210
207
|
In Graphiti, datetimes are rendered in [ISO 8601 format](https://www.iso.org/iso-8601-date-and-time-format.html). This means that straight date comparisons will fail:
|
|
211
208
|
|
|
212
209
|
```ruby
|
|
213
210
|
# WRONG
|
|
214
|
-
expect(
|
|
211
|
+
expect(jsonapi_data[0].created_at).to eq(post.created_at)
|
|
215
212
|
```
|
|
216
213
|
|
|
217
|
-
Instead, use the `
|
|
214
|
+
Instead, use the `json_datetime` helper to convert to ISO 8601 and compare apples to apples:
|
|
218
215
|
|
|
219
216
|
```ruby
|
|
220
217
|
# RIGHT
|
|
221
|
-
expect(
|
|
218
|
+
expect(jsonapi_data[0].created_at).to eq(json_datetime(post.created_at))
|
|
222
219
|
```
|
|
223
220
|
|
|
224
|
-
Similarly, there's a `
|
|
221
|
+
Similarly, there's a `json_date` helper as well.
|
|
225
222
|
|
|
226
223
|
### #jsonapi_errors {#jsonapi-errors}
|
|
227
224
|
|
|
228
|
-
> This method is aliased to `errors` for brevity
|
|
229
|
-
|
|
230
225
|
To parse an [Errors Payload](http://jsonapi.org/format/#errors):
|
|
231
226
|
|
|
232
227
|
```ruby
|
|
@@ -261,7 +256,7 @@ Resource tests have two helpers, both different ways to execute a query.
|
|
|
261
256
|
```ruby
|
|
262
257
|
it 'works' do
|
|
263
258
|
render
|
|
264
|
-
expect(
|
|
259
|
+
expect(jsonapi_data[0].first_name).to eq('Jane')
|
|
265
260
|
json # => { data: { type: 'employees', ... } }
|
|
266
261
|
end
|
|
267
262
|
```
|
|
@@ -275,6 +270,33 @@ it 'works' do
|
|
|
275
270
|
end
|
|
276
271
|
```
|
|
277
272
|
|
|
273
|
+
### Resource Matchers {#resource-matchers}
|
|
274
|
+
|
|
275
|
+
For one-line assertions about a Resource's shape, use the built-in matchers. They're included automatically in `type: :resource` specs and expect a Resource instance as the subject:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
RSpec.describe PostResource, type: :resource do
|
|
279
|
+
subject { described_class.new }
|
|
280
|
+
|
|
281
|
+
it { is_expected.to belong_to_resource(:author) }
|
|
282
|
+
it { is_expected.to have_many_resources(:comments) }
|
|
283
|
+
it { is_expected.to have_one_resource(:detail) }
|
|
284
|
+
it { is_expected.to expose_attribute(:title, :string) }
|
|
285
|
+
it { is_expected.to filter_attribute(:title, :string) }
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Each matcher accepts `with_options` to assert configuration:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
it do
|
|
293
|
+
is_expected.to belong_to_resource(:author)
|
|
294
|
+
.with_options(foreign_key: :author_id, resource: AuthorResource)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it { is_expected.to expose_attribute(:title, :string).with_options(writable: false) }
|
|
298
|
+
```
|
|
299
|
+
|
|
278
300
|
### API Test Helpers {#api-test-helpers}
|
|
279
301
|
|
|
280
302
|
When executing an API test request, always use the `jsonapi_` doppelgänger:
|
|
@@ -378,7 +400,7 @@ describe 'filtering' do
|
|
|
378
400
|
|
|
379
401
|
it 'works' do
|
|
380
402
|
render
|
|
381
|
-
expect(
|
|
403
|
+
expect(jsonapi_data.map(&:id)).to eq([employee2.id])
|
|
382
404
|
end
|
|
383
405
|
end
|
|
384
406
|
end
|
|
@@ -401,7 +423,7 @@ describe 'sorting' do
|
|
|
401
423
|
|
|
402
424
|
it 'works' do
|
|
403
425
|
render
|
|
404
|
-
expect(
|
|
426
|
+
expect(jsonapi_data.map(&:id)).to eq([
|
|
405
427
|
employee1.id,
|
|
406
428
|
employee2.id
|
|
407
429
|
])
|
|
@@ -415,7 +437,7 @@ describe 'sorting' do
|
|
|
415
437
|
|
|
416
438
|
it 'works' do
|
|
417
439
|
render
|
|
418
|
-
expect(
|
|
440
|
+
expect(jsonapi_data.map(&:id)).to eq([
|
|
419
441
|
employee2.id,
|
|
420
442
|
employee1.id
|
|
421
443
|
])
|
|
@@ -447,7 +469,7 @@ describe 'sideloading' do
|
|
|
447
469
|
|
|
448
470
|
it 'returns position with historical index == 1' do
|
|
449
471
|
render
|
|
450
|
-
sl =
|
|
472
|
+
sl = jsonapi_data[0].sideload(:current_position)
|
|
451
473
|
expect(sl.jsonapi_type).to eq('positions')
|
|
452
474
|
expect(sl.id).to eq(pos2.id)
|
|
453
475
|
end
|
|
@@ -669,9 +691,9 @@ RSpec.describe "employees#index", type: :request do
|
|
|
669
691
|
expect(EmployeeResource).to receive(:all).and_call_original
|
|
670
692
|
make_request
|
|
671
693
|
expect(response.status).to eq(200)
|
|
672
|
-
expect(
|
|
694
|
+
expect(jsonapi_data.map(&:jsonapi_type).uniq)
|
|
673
695
|
.to match_array(['employees'])
|
|
674
|
-
expect(
|
|
696
|
+
expect(jsonapi_data.map(&:id))
|
|
675
697
|
.to match_array([employee1.id, employee2.id])
|
|
676
698
|
end
|
|
677
699
|
end
|
|
@@ -694,8 +716,8 @@ describe 'basic fetch' do
|
|
|
694
716
|
expect(EmployeeResource).to receive(:find).and_call_original
|
|
695
717
|
make_request
|
|
696
718
|
expect(response.status).to eq(200)
|
|
697
|
-
expect(
|
|
698
|
-
expect(
|
|
719
|
+
expect(jsonapi_data.jsonapi_type).to eq('employees')
|
|
720
|
+
expect(jsonapi_data.id).to eq(employee.id)
|
|
699
721
|
end
|
|
700
722
|
end
|
|
701
723
|
```
|
|
@@ -843,7 +865,7 @@ it 'works' do
|
|
|
843
865
|
Graphiti.with_context ctx do
|
|
844
866
|
render
|
|
845
867
|
end
|
|
846
|
-
expect(
|
|
868
|
+
expect(jsonapi_data[0].salary).to eq(100_000)
|
|
847
869
|
end
|
|
848
870
|
```
|
|
849
871
|
|
data/docs/upgrading.md
CHANGED
|
@@ -128,9 +128,9 @@ It has to be a request spec. Exceptions are rendered in Rack middleware, which c
|
|
|
128
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.
|
|
129
129
|
|
|
130
130
|
<details>
|
|
131
|
-
<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>
|
|
132
132
|
|
|
133
|
-
A `belongs_to` now
|
|
133
|
+
A `belongs_to` now renders resource ids in the payload by default, where 1.x sent only a link:
|
|
134
134
|
|
|
135
135
|
```json
|
|
136
136
|
"employee": { "data": { "type": "employees", "id": "1" }, "links": { "related": "..." } }
|
|
@@ -138,27 +138,29 @@ A `belongs_to` now includes resource linkage in the payload by default, where 1.
|
|
|
138
138
|
|
|
139
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.
|
|
140
140
|
|
|
141
|
-
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.
|
|
142
142
|
|
|
143
|
-
|
|
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.
|
|
144
144
|
|
|
145
145
|
To go back to the old payload for one relationship:
|
|
146
146
|
|
|
147
147
|
```ruby
|
|
148
|
-
belongs_to :employee,
|
|
148
|
+
belongs_to :employee, resource_ids: false
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
Or for the whole API, on the resource everything inherits from
|
|
151
|
+
Or for the whole API, on the resource everything inherits from:
|
|
152
152
|
|
|
153
153
|
```ruby
|
|
154
154
|
class ApplicationResource < Graphiti::Resource
|
|
155
155
|
self.abstract_class = true
|
|
156
156
|
|
|
157
|
-
self.
|
|
157
|
+
self.belongs_to_resource_ids_by_default = :never
|
|
158
158
|
end
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
|
|
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).
|
|
162
164
|
|
|
163
165
|
</details>
|
|
164
166
|
|
|
@@ -212,6 +214,7 @@ Every name below still works, warns, and will be removed in the next major. They
|
|
|
212
214
|
| --- | --- |
|
|
213
215
|
| `require "graphiti_spec_helpers/rspec"` | `require "graphiti/spec_helpers/rspec"` |
|
|
214
216
|
| `GraphitiSpecHelpers::RSpec` / `::Sugar` / `::Errors::*` | `Graphiti::SpecHelpers::*` |
|
|
217
|
+
| `include Graphiti::SpecHelpers::Sugar` (`d`, `included`, `errors`, `dt`) | call `jsonapi_data`, `jsonapi_included`, `jsonapi_errors`, `json_datetime` directly |
|
|
215
218
|
| `require "graphiti-rails"` | remove / no longer needed |
|
|
216
219
|
| `include Graphiti::Rails` | `include Graphiti::Rails::Controller`|
|
|
217
220
|
| `include Graphiti::Responders` | `include Graphiti::Rails::Responders` |
|
|
@@ -224,6 +227,7 @@ Every name below still works, warns, and will be removed in the next major. They
|
|
|
224
227
|
| `context_namespace` | `current_action` |
|
|
225
228
|
| `Graphiti::Rails::DEPRECATOR` | `Graphiti::DEPRECATOR` (the old name still resolves) |
|
|
226
229
|
| `require "graphiti_errors"`, `require "graphiti/responders"` | remove / no longer needed |
|
|
230
|
+
| `always_include_resource_ids: true` on a relationship | `resource_ids: true` |
|
|
227
231
|
|
|
228
232
|
`RSpec.describe PostResource, type: :resource` still picks up the resource-testing context automatically. That has not changed.
|
|
229
233
|
|
|
@@ -234,6 +238,9 @@ Every name below still works, warns, and will be removed in the next major. They
|
|
|
234
238
|
| `include GraphitiErrors` | `register_exception` is available on every controller |
|
|
235
239
|
| `GraphitiErrors::ExceptionHandler` | subclass `Graphiti::Rails::ExceptionHandler` |
|
|
236
240
|
| `GraphitiErrors.enable!` / `.disable!` | `handle_request_exceptions` |
|
|
241
|
+
| `self.always_include_resource_ids_by_default` | `self.belongs_to_resource_ids_by_default`, which takes `:foreign_key`, `:always` or `:never` |
|
|
242
|
+
|
|
243
|
+
`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`.
|
|
237
244
|
|
|
238
245
|
## Without Rails {#without-rails}
|
|
239
246
|
|
data/graphiti.gemspec
CHANGED
|
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
spec.add_dependency "rescue_registry", "~> 1.0"
|
|
25
25
|
spec.add_dependency "concurrent-ruby", ">= 1.2", "< 2.0"
|
|
26
26
|
spec.add_dependency "activesupport", ">= 7.1"
|
|
27
|
-
# Bundled (no longer default) as of Ruby
|
|
27
|
+
# Bundled (no longer default) as of Ruby 4.0; graphiti uses OpenStruct in lib/
|
|
28
28
|
spec.add_dependency "ostruct", ">= 0.5"
|
|
29
29
|
|
|
30
30
|
spec.add_development_dependency "faraday", "~> 0.15"
|
|
@@ -15,8 +15,8 @@ RSpec.describe "<%= type %>#index", type: :request do
|
|
|
15
15
|
expect(<%= resource_class %>).to receive(:all).and_call_original
|
|
16
16
|
make_request
|
|
17
17
|
expect(response.status).to eq(200), response.body
|
|
18
|
-
expect(
|
|
19
|
-
expect(
|
|
18
|
+
expect(jsonapi_data.map(&:jsonapi_type).uniq).to match_array(['<%= type %>'])
|
|
19
|
+
expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to match_array([<%= var %>1.id, <%= var %>2.id])
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -11,7 +11,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
|
|
|
11
11
|
expect(data.jsonapi_type).to eq('<%= type %>')
|
|
12
12
|
<%- attributes.each do |a| -%>
|
|
13
13
|
<%- if [:created_at, :updated_at].include?(a.name.to_sym) -%>
|
|
14
|
-
expect(data.<%= a.name %>).to eq(
|
|
14
|
+
expect(data.<%= a.name %>).to eq(json_datetime(<%= file_name %>.<%= a.name %>))
|
|
15
15
|
<%- else -%>
|
|
16
16
|
expect(data.<%= a.name %>).to eq(<%= file_name %>.<%= a.name %>)
|
|
17
17
|
<%- end -%>
|
|
@@ -31,7 +31,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
|
|
|
31
31
|
|
|
32
32
|
it 'works' do
|
|
33
33
|
render
|
|
34
|
-
expect(
|
|
34
|
+
expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([<%= var %>2.id])
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -48,7 +48,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
|
|
|
48
48
|
|
|
49
49
|
it 'works' do
|
|
50
50
|
render
|
|
51
|
-
expect(
|
|
51
|
+
expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([
|
|
52
52
|
<%= var %>1.id,
|
|
53
53
|
<%= var %>2.id
|
|
54
54
|
]<%= sort_raw_ids %>)
|
|
@@ -62,7 +62,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
|
|
|
62
62
|
|
|
63
63
|
it 'works' do
|
|
64
64
|
render
|
|
65
|
-
expect(
|
|
65
|
+
expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([
|
|
66
66
|
<%= var %>2.id,
|
|
67
67
|
<%= var %>1.id
|
|
68
68
|
]<%= sort_raw_ids_descending %>)
|
|
@@ -14,8 +14,8 @@ RSpec.describe "<%= type %>#show", type: :request do
|
|
|
14
14
|
expect(<%= resource_class %>).to receive(:find).and_call_original
|
|
15
15
|
make_request
|
|
16
16
|
expect(response.status).to eq(200)
|
|
17
|
-
expect(
|
|
18
|
-
expect(
|
|
17
|
+
expect(jsonapi_data.jsonapi_type).to eq('<%= type %>')
|
|
18
|
+
expect(jsonapi_data.<%= id_or_rawid %>).to eq(<%= var %>.id)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|