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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdd882c646fa88d0ac6522b7725ad6132b7ad890043822f5e2a2ce8af3f0fc4c
4
- data.tar.gz: 687c64e9c5e6d2931a09bde9f5595b965e36f1a090d863fa17b9edf11559533f
3
+ metadata.gz: acced292af02bc2826f89f6e1d9388bcc7fdedabada42e4cc65d1bfaf9f713f5
4
+ data.tar.gz: 79f966dc31ccb9b3aee687e7dc31a92e943e17845b6017258e366bebee066446
5
5
  SHA512:
6
- metadata.gz: 1907226aa19ad065b0affe66f5ea22bb7149fd11aef92d225d7b433a5c67111bde2d7ef5911e633c5ea9059559ff903654c21f835099b5e15e72fdf7ab59cc9b
7
- data.tar.gz: 91fd8e83a1452d29ffb45076b94a9eeb316c4e508a2f6565c5e079e6c54a184eede79da45ecb9d25f340f395551d907e10eb152e05666387792c434ed5fb4361
6
+ metadata.gz: 22584fd2a83e017219fb8202c965d3d295ac341fed01b2e543355c32722cae741866f3188970ee480018d6063c78b06281045f22934c42b9645de72c6cc9562f
7
+ data.tar.gz: 502371f38ad71f5e901b57ba187be70bb4d7c85c94544324e825207bc03f120fa652d0859fe56744efbdbcf826376dad84a4917444c62a7e30193482bca47700
@@ -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
- always_include_resource_ids: true # default: true for belongs_to, false otherwise
73
+ resource_ids: false
74
74
  ```
75
75
 
76
- `belongs_to` renders resource linkage by default, so a client can see which record a relationship points at without following the link:
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. A `has_many` would have to run a query per record to answer the same question, so it stays off unless you ask for it.
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
- A `belongs_to` falls back to loading the association when the foreign key cannot answer for it: a `params` block or `base_scope` can filter out the record the key points at, a polymorphic target takes its type from the record rather than the relationship, and a remote resource has no local key to read. Turning linkage on for those, or for a `has_many`, brings back the 1+N described in [#167](https://github.com/graphiti-api/graphiti/issues/167#issuecomment-686866646).
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
- Set the default for every relationship on a resource, whatever its type:
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.always_include_resource_ids_by_default = false
167
+ self.belongs_to_resource_ids_by_default = :foreign_key
94
168
  end
95
169
  ```
96
170
 
97
- Subclasses inherit it, and a relationship passing `always_include_resource_ids` explicitly still wins.
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
- always_include_resource_ids: false,
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
- always_include_resource_ids: false,
368
+ resource_ids: true,
269
369
  resource: EmployeeResource
270
370
  ```
271
371
 
@@ -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]`
@@ -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 = d[0].sideload(:comments)
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
- d[0].link(:comments, :related)
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
- ### #date and #datetime {#date-and-datetime}
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(d[0].created_at).to eq(post.created_at)
211
+ expect(jsonapi_data[0].created_at).to eq(post.created_at)
215
212
  ```
216
213
 
217
- Instead, use the `datetime` helper to convert to ISO 8601 and compare apples to apples:
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(d[0].created_at).to eq(datetime(post.created_at))
218
+ expect(jsonapi_data[0].created_at).to eq(json_datetime(post.created_at))
222
219
  ```
223
220
 
224
- Similarly, there's a `date` helper as well.
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(d[0].first_name).to eq('Jane')
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(d.map(&:id)).to eq([employee2.id])
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(d.map(&:id)).to eq([
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(d.map(&:id)).to eq([
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 = d[0].sideload(:current_position)
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(d.map(&:jsonapi_type).uniq)
694
+ expect(jsonapi_data.map(&:jsonapi_type).uniq)
673
695
  .to match_array(['employees'])
674
- expect(d.map(&:id))
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(d.jsonapi_type).to eq('employees')
698
- expect(d.id).to eq(employee.id)
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(d[0].salary).to eq(100_000)
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 linkage in every payload, where 1.x sent only a link</summary>
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 includes resource linkage in the payload by default, where 1.x sent only a link:
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 `scope` or `params` block, a `base_scope`, a polymorphic or remote target or a custom `primary_key` all mean the foreign key is not the related id, and those keep loading the association, so they stay opt-in as in 1.x.
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
- Relationships are marked `linkage: true` in `schema.json`, and the schema check reports a relationship that stops including it.
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, always_include_resource_ids: false
148
+ belongs_to :employee, resource_ids: false
149
149
  ```
150
150
 
151
- Or for the whole API, on the resource everything inherits from. The same setting with `true` is the 2.0 replacement for the `Sideload::BelongsTo` monkey patch that [#167](https://github.com/graphiti-api/graphiti/issues/167) has been recommending, and it now covers every relationship type rather than only `belongs_to`:
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.always_include_resource_ids_by_default = false
157
+ self.belongs_to_resource_ids_by_default = :never
158
158
  end
159
159
  ```
160
160
 
161
- How linkage is configured, and when a `belongs_to` cannot use its foreign key, is covered in [Customizing Relationships](/concepts/relationships#customizing-relationships).
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 3.5; graphiti uses OpenStruct in lib/
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"
@@ -49,7 +49,6 @@ module Graphiti
49
49
 
50
50
  RSpec.configure do |config|
51
51
  config.include Graphiti::SpecHelpers::RSpec
52
- config.include Graphiti::SpecHelpers::Sugar
53
52
  end
54
53
 
55
54
  Graphiti::SpecHelpers::RSpec.schema!
@@ -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(d.map(&:jsonapi_type).uniq).to match_array(['<%= type %>'])
19
- expect(d.map(&:<%= id_or_rawid %>)).to match_array([<%= var %>1.id, <%= var %>2.id])
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(datetime(<%= file_name %>.<%= a.name %>))
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(d.map(&:<%= id_or_rawid %>)).to eq([<%= var %>2.id])
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(d.map(&:<%= id_or_rawid %>)).to eq([
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(d.map(&:<%= id_or_rawid %>)).to eq([
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(d.jsonapi_type).to eq('<%= type %>')
18
- expect(d.<%= id_or_rawid %>).to eq(<%= var %>.id)
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