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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +5 -0
  3. data/.github/workflows/docs.yml +4 -7
  4. data/CHANGELOG.md +29 -0
  5. data/docs/concepts/links.md +3 -3
  6. data/docs/concepts/relationships.md +109 -9
  7. data/docs/topics/debugging.md +26 -0
  8. data/docs/upgrading.md +18 -17
  9. data/graphiti.gemspec +1 -19
  10. data/lib/generators/graphiti/resource_generator.rb +25 -1
  11. data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
  12. data/lib/graphiti/audit/report.rb +226 -0
  13. data/lib/graphiti/audit.rb +231 -0
  14. data/lib/graphiti/errors.rb +47 -0
  15. data/lib/graphiti/rails/rake_helpers.rb +42 -0
  16. data/lib/graphiti/request_validators/validator.rb +10 -1
  17. data/lib/graphiti/resource/configuration.rb +16 -1
  18. data/lib/graphiti/resource/polymorphism.rb +10 -0
  19. data/lib/graphiti/schema.rb +1 -1
  20. data/lib/graphiti/sideload/belongs_to.rb +22 -29
  21. data/lib/graphiti/sideload.rb +48 -13
  22. data/lib/graphiti/spec_helpers/matchers.rb +196 -0
  23. data/lib/graphiti/spec_helpers/rspec.rb +1 -0
  24. data/lib/graphiti/spec_helpers.rb +1 -0
  25. data/lib/graphiti/util/serializer_relationships.rb +27 -4
  26. data/lib/graphiti/version.rb +1 -1
  27. data/lib/graphiti.rb +2 -0
  28. data/lib/tasks/graphiti.rake +16 -32
  29. data/website/docusaurus.config.js +67 -11
  30. data/website/src/css/custom.css +29 -2
  31. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  32. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  33. data/website/static/1.13/assets/img/sunrise.png +0 -0
  34. data/website/static/1.13/assets/main.css +4 -4
  35. data/website/static/CNAME +1 -0
  36. metadata +9 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8f1b385d0a7623788b2a492d98d2175d1964d320ea48f988f73bffb30ab9439
4
- data.tar.gz: a95f11de0a07753144d5c4d057ae0b4bb90e2f61c5906996bd29f7fa09998657
3
+ metadata.gz: e108e5f71103c3fb7f38f5328a890ff131b264201c583509081cfd3106a7b255
4
+ data.tar.gz: 18edd07b0ab429ec89309ec94164a2324a21bbbfbd6dd37f6e49bb6f9707f1c9
5
5
  SHA512:
6
- metadata.gz: 7f00afaae7e44289e89d5d29e56e3539953e2c13befcc6c2d5dbb95c9b8904ca6103c3651ebabb6227f9e43f50f22ee4b845d1d9235244e363a1c08089363c47
7
- data.tar.gz: a6f82ed9556a22d2ddd76710877af8f39cfca470cad5a12a05d99ef5d54fc6d03a5c0e549026307cd71b6a6323c51e4889164acd94a38a0d371e0c74fe54760a
6
+ metadata.gz: f2affdd800c2966ef15c5d1765b9d1acc48a032c3ee143b9283d69b9545290e227454d77559e25c94b4cd7b0131d0cdc2addbb031a4177155d8085086d818b3b
7
+ data.tar.gz: 79c294b775b8be269194cae7e61cf179a16ec8dd01e8b0b24e7a141d3333683bfbd54032e4c07623f6d2dc3fbe3ce6c721884658da89cbe24bfd9dac935f8e07
@@ -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' }}
@@ -1,10 +1,9 @@
1
1
  name: Docs
2
2
 
3
- # Publishes the documentation site from beta so the deploy is exercised well
4
- # before it matters. Pages serves this repo under /graphiti/, and the root of
5
- # graphiti.dev comes from graphiti-api.github.io, so nothing here can affect
6
- # the live 1.x site. When the domain moves to this repo, drop DOCS_BASE_URL so
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
 
@@ -170,11 +170,11 @@ end
170
170
 
171
171
  ### Pagination Links {#pagination-links}
172
172
 
173
- Requesting big collections can result into slow responses sometimes. In order to avoid this, you could use [pagination](https://jsonapi.org/format/#fetching-pagination). It'll break your response into smaller pieces that will make your server responds faster. Paginations links can be present in your response in the following ways:
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
- With this configuration, all the responses will return the pagination links
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
- You can showing the pagination links when it was requested in the URL with `?pagination_links=true`
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
- 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]`
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
- # Gemfile
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
- Skipping this fails at boot rather than at request time: a controller calling `sideload_allowlist` raises `NoMethodError` while its class body loads, so the app stops starting.
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 where you include it decides the blast radius. `ApplicationController` matches 1.x behavior. An API base class scopes it and leaves the rest of the app alone.
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 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>
137
132
 
138
- 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:
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 `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.
147
142
 
148
- 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.
149
144
 
150
145
  To go back to the old payload for one relationship:
151
146
 
152
147
  ```ruby
153
- belongs_to :employee, always_include_resource_ids: false
148
+ belongs_to :employee, resource_ids: false
154
149
  ```
155
150
 
156
- 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:
157
152
 
158
153
  ```ruby
159
154
  class ApplicationResource < Graphiti::Resource
160
155
  self.abstract_class = true
161
156
 
162
- self.always_include_resource_ids_by_default = false
157
+ self.belongs_to_resource_ids_by_default = :never
163
158
  end
164
159
  ```
165
160
 
166
- 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).
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 3.5; graphiti uses OpenStruct in lib/
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", class_path, "#{file_name.pluralize}_controller.rb")
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 <%= model_klass.name.pluralize %>Controller < ApplicationController
2
+ class <%= controller_class_name %> < ApplicationController
3
3
  <%- if actions?('index') -%>
4
4
  def index
5
5
  <%= file_name.pluralize %> = <%= resource_klass %>.all(params)