plutonium 0.50.0 → 0.51.0

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
@@ -1,449 +0,0 @@
1
- # Interaction Reference
2
-
3
- Complete reference for business logic Interactions.
4
-
5
- ## Overview
6
-
7
- Interactions encapsulate business logic for custom actions. They:
8
- - Accept input from users
9
- - Validate that input
10
- - Execute business logic
11
- - Return success or failure outcomes
12
-
13
- ## Base Class
14
-
15
- ```ruby
16
- # app/interactions/resource_interaction.rb (generated during install)
17
- class ResourceInteraction < Plutonium::Resource::Interaction
18
- end
19
-
20
- # app/interactions/publish_post_interaction.rb
21
- class PublishPostInteraction < ResourceInteraction
22
- # Interaction code
23
- end
24
- ```
25
-
26
- ## Presentation
27
-
28
- Configure how the action appears in the UI:
29
-
30
- ```ruby
31
- class PublishPost < Plutonium::Resource::Interaction
32
- presents label: "Publish Post",
33
- icon: Phlex::TablerIcons::Send,
34
- description: "Make this post visible to the public"
35
- end
36
- ```
37
-
38
- Access presentation metadata:
39
-
40
- ```ruby
41
- PublishPost.label # => "Publish Post"
42
- PublishPost.icon # => Phlex::TablerIcons::Send
43
- PublishPost.description # => "Make this post visible..."
44
- ```
45
-
46
- ## Attributes
47
-
48
- Define inputs using ActiveModel attributes:
49
-
50
- ### Basic Types
51
-
52
- ```ruby
53
- attribute :title, :string
54
- attribute :count, :integer
55
- attribute :price, :decimal
56
- attribute :active, :boolean
57
- attribute :published_at, :datetime
58
- ```
59
-
60
- ### With Defaults
61
-
62
- ```ruby
63
- attribute :status, :string, default: "pending"
64
- attribute :notify, :boolean, default: true
65
- attribute :count, :integer, default: 1
66
- attribute :created_at, :datetime, default: -> { Time.current }
67
- ```
68
-
69
- ### The resource Attribute
70
-
71
- For record actions, declare a `resource` attribute:
72
-
73
- ```ruby
74
- class PublishPost < Plutonium::Resource::Interaction
75
- attribute :resource # The record being acted upon
76
-
77
- private
78
-
79
- def execute
80
- resource.update!(published: true)
81
- succeed(resource)
82
- end
83
- end
84
- ```
85
-
86
- ### The resources Attribute
87
-
88
- For bulk actions, declare a `resources` attribute:
89
-
90
- ```ruby
91
- class BulkArchive < Plutonium::Resource::Interaction
92
- attribute :resources # Collection of records
93
-
94
- private
95
-
96
- def execute
97
- resources.update_all(archived: true)
98
- succeed(resources)
99
- end
100
- end
101
- ```
102
-
103
- ## Form Inputs
104
-
105
- Define how attributes render in forms using the `input` method:
106
-
107
- ```ruby
108
- class InviteUser < Plutonium::Resource::Interaction
109
- attribute :resource
110
- attribute :email, :string
111
- attribute :role, :string
112
-
113
- input :email, as: :email
114
- input :role, as: :select, choices: %w[admin member viewer]
115
- end
116
- ```
117
-
118
- See [Fields Reference](/reference/definition/fields) for all input types and options.
119
-
120
- ## Validation
121
-
122
- Use standard ActiveModel validations:
123
-
124
- ```ruby
125
- class SchedulePost < Plutonium::Resource::Interaction
126
- attribute :resource
127
- attribute :publish_at, :datetime
128
-
129
- validates :publish_at, presence: true
130
- validate :publish_at_in_future
131
-
132
- private
133
-
134
- def publish_at_in_future
135
- if publish_at.present? && publish_at <= Time.current
136
- errors.add(:publish_at, "must be in the future")
137
- end
138
- end
139
- end
140
- ```
141
-
142
- Validations run automatically before `execute`. If invalid, returns a failure outcome.
143
-
144
- ## The execute Method
145
-
146
- Main logic goes here. Must return an outcome using `succeed()` or `failed()`:
147
-
148
- ```ruby
149
- private
150
-
151
- def execute
152
- resource.update!(published: true)
153
- succeed(resource).with_message("Published!")
154
- rescue ActiveRecord::RecordInvalid => e
155
- failed(e.record.errors)
156
- end
157
- ```
158
-
159
- ::: warning Handle RecordInvalid
160
- `ActiveRecord::RecordInvalid` is **not** rescued automatically. Always rescue it when using bang methods (`create!`, `update!`, `save!`).
161
- :::
162
-
163
- ## Constructor
164
-
165
- Interactions require `view_context:` and accept attributes as keyword arguments:
166
-
167
- ```ruby
168
- interaction = PublishPost.new(
169
- view_context: view_context,
170
- resource: post,
171
- notify: true
172
- )
173
- ```
174
-
175
- The controller handles this automatically for interactive actions.
176
-
177
- ## Calling Interactions
178
-
179
- ### Via call Class Method
180
-
181
- ```ruby
182
- outcome = PublishPost.call(view_context: view_context, resource: post)
183
-
184
- if outcome.success?
185
- # Handle success
186
- else
187
- # Handle failure
188
- end
189
- ```
190
-
191
- ### Via call Instance Method
192
-
193
- ```ruby
194
- interaction = PublishPost.new(view_context: view_context, resource: post)
195
- outcome = interaction.call
196
- ```
197
-
198
- ## Success Outcomes
199
-
200
- ::: tip Automatic Redirect
201
- On success, the controller automatically redirects to the resource.
202
- :::
203
-
204
- ### Basic Success
205
-
206
- ```ruby
207
- succeed(resource) # Redirects to resource automatically
208
- ```
209
-
210
- ### With Message
211
-
212
- ```ruby
213
- succeed(resource).with_message("Post published!")
214
- succeed(resource).with_message("Warning: limited visibility", :alert)
215
- ```
216
-
217
- ### With Custom Redirect
218
-
219
- Useful when redirecting somewhere other than the default:
220
-
221
- ```ruby
222
- succeed(resource).with_redirect_response(custom_dashboard_path)
223
- ```
224
-
225
- ### With File Download
226
-
227
- ```ruby
228
- succeed(resource).with_file_response(file_path, filename: "report.pdf")
229
- ```
230
-
231
- ### With Render
232
-
233
- ```ruby
234
- succeed(resource).with_render_response(:custom_template)
235
- ```
236
-
237
- ### Chaining
238
-
239
- ```ruby
240
- succeed(resource)
241
- .with_message("Created!")
242
- .with_redirect_response(edit_post_path(resource))
243
- ```
244
-
245
- ## Failure Outcomes
246
-
247
- ### Simple Failure
248
-
249
- ```ruby
250
- failed("Cannot publish draft posts")
251
- ```
252
-
253
- ### With Attribute
254
-
255
- ```ruby
256
- failed("is invalid", :email)
257
- ```
258
-
259
- ### With Hash of Errors
260
-
261
- ```ruby
262
- failed(email: "is invalid", name: "is required")
263
- ```
264
-
265
- ### With ActiveModel Errors
266
-
267
- ```ruby
268
- failed(resource.errors)
269
- ```
270
-
271
- ### Manual Error Addition
272
-
273
- ```ruby
274
- def execute
275
- errors.add(:base, "Post must have content")
276
- return failure if errors.any?
277
-
278
- # Continue...
279
- end
280
- ```
281
-
282
- ## Chaining Interactions
283
-
284
- Use `and_then` to chain operations. On failure, the chain short-circuits:
285
-
286
- ```ruby
287
- def execute
288
- CreateUserInteraction.call(view_context:, **user_params)
289
- .and_then { |result| SendWelcomeEmail.call(view_context:, user: result.value) }
290
- .and_then { |result| LogActivity.call(view_context:, user: result.value) }
291
- .with_message("User created and welcomed!")
292
- end
293
- ```
294
-
295
- ## Accessing Current User
296
-
297
- ```ruby
298
- def execute
299
- resource.update!(updated_by: current_user)
300
- succeed(resource)
301
- end
302
-
303
- # current_user is provided by the base class:
304
- # def current_user
305
- # view_context.controller.helpers.current_user
306
- # end
307
- ```
308
-
309
- ## Complete Example
310
-
311
- ```ruby
312
- class Company::InviteUserInteraction < Plutonium::Resource::Interaction
313
- presents label: "Invite User",
314
- icon: Phlex::TablerIcons::UserPlus,
315
- description: "Send an invitation email"
316
-
317
- attribute :resource # The company
318
- attribute :email, :string
319
- attribute :role, :string
320
-
321
- input :email, as: :email
322
- input :role, as: :select, choices: -> { UserInvite.roles.keys }
323
-
324
- validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
325
- validates :role, presence: true, inclusion: { in: UserInvite.roles.keys }
326
- validate :not_already_invited
327
-
328
- private
329
-
330
- def execute
331
- invite = UserInvite.create!(
332
- company: resource,
333
- email: email,
334
- role: role,
335
- invited_by: current_user
336
- )
337
- UserInviteMailer.invitation(invite).deliver_later
338
-
339
- succeed(resource).with_message("Invitation sent to #{email}")
340
- rescue ActiveRecord::RecordInvalid => e
341
- failed(e.record.errors)
342
- end
343
-
344
- def not_already_invited
345
- return unless email.present?
346
-
347
- if UserInvite.exists?(company: resource, email: email, state: :pending)
348
- errors.add(:email, "already has a pending invitation")
349
- end
350
- end
351
- end
352
- ```
353
-
354
- ## Connecting to Definitions
355
-
356
- Register interactions as actions in definitions:
357
-
358
- ```ruby
359
- class PostDefinition < Plutonium::Resource::Definition
360
- action :publish, interaction: PublishPostInteraction
361
- action :invite_user, interaction: InviteUserInteraction
362
-
363
- action :archive,
364
- interaction: ArchiveInteraction,
365
- confirmation: "Are you sure?",
366
- category: :danger,
367
- position: 100
368
- end
369
- ```
370
-
371
- ## Immediate vs Form Actions
372
-
373
- Plutonium determines if an action needs a form based on whether inputs are defined:
374
-
375
- **Shows form first** (has inputs):
376
-
377
- ```ruby
378
- class InviteUserInteraction < Plutonium::Resource::Interaction
379
- attribute :resource
380
- attribute :email
381
- input :email # This triggers form display
382
- end
383
- ```
384
-
385
- **Executes immediately** (no inputs):
386
-
387
- ```ruby
388
- class ArchiveInteraction < Plutonium::Resource::Interaction
389
- attribute :resource
390
- # No inputs = immediate execution with confirmation
391
- end
392
- ```
393
-
394
- ## Policy Integration
395
-
396
- Control access with policy methods matching the action name:
397
-
398
- ```ruby
399
- class PostPolicy < Plutonium::Resource::Policy
400
- def publish?
401
- update? && record.draft?
402
- end
403
-
404
- def archive?
405
- destroy? && !record.archived?
406
- end
407
- end
408
- ```
409
-
410
- ## Testing
411
-
412
- ```ruby
413
- RSpec.describe PublishPost do
414
- let(:view_context) { double("view_context", controller: double(helpers: double(current_user: user))) }
415
- let(:user) { create(:user) }
416
- let(:post) { create(:post, user: user, published: false) }
417
-
418
- describe '#call' do
419
- it 'publishes the post' do
420
- interaction = described_class.new(view_context: view_context, resource: post)
421
- outcome = interaction.call
422
-
423
- expect(outcome).to be_success
424
- expect(post.reload).to be_published
425
- end
426
-
427
- context 'when validation fails' do
428
- it 'returns failure outcome' do
429
- interaction = described_class.new(view_context: view_context, resource: nil)
430
- outcome = interaction.call
431
-
432
- expect(outcome).to be_failure
433
- end
434
- end
435
- end
436
- end
437
- ```
438
-
439
- ## Best Practices
440
-
441
- 1. **Keep interactions focused** - One action per interaction
442
- 2. **Use validations** - Validate all inputs before execution
443
- 3. **Handle errors gracefully** - Rescue exceptions and return `failed()`
444
-
445
- ## Related
446
-
447
- - [Actions Reference](/reference/definition/actions) - Connecting interactions to definitions
448
- - [Fields Reference](/reference/definition/fields) - Input configuration
449
- - [Policy Reference](/reference/policy/) - Authorization
@@ -1,248 +0,0 @@
1
- # Model Features
2
-
3
- Features provided by `Plutonium::Resource::Record`.
4
-
5
- ## has_cents
6
-
7
- Store monetary values as integers (cents) while exposing decimal accessors.
8
-
9
- ```ruby
10
- class Product < ResourceRecord
11
- # Column: price_cents (integer)
12
- # Generates: price (decimal accessor)
13
- has_cents :price_cents
14
- end
15
- ```
16
-
17
- ### Usage
18
-
19
- ```ruby
20
- product = Product.new
21
- product.price = 19.99
22
- product.price_cents # => 1999
23
-
24
- product.price_cents = 2500
25
- product.price # => 25.0
26
- ```
27
-
28
- ### Options
29
-
30
- ```ruby
31
- class Order < ResourceRecord
32
- # Default: rate 100 (cents to dollars)
33
- has_cents :subtotal_cents
34
-
35
- # Custom name for the accessor
36
- has_cents :cost_cents, name: :wholesale_price
37
- # cost_cents column, wholesale_price accessor
38
-
39
- # Yen or other currencies without subunits (rate: 1)
40
- has_cents :price_yen, name: :price_jpy, rate: 1
41
-
42
- # Higher precision (e.g., 1000 units per dollar)
43
- has_cents :amount_cents, rate: 1000
44
-
45
- # Custom suffix when name matches column pattern
46
- has_cents :total_cents, suffix: "value"
47
- # Generates: total_value accessor
48
- end
49
- ```
50
-
51
- ### Validation Inheritance
52
-
53
- Validations on the cents column propagate to the decimal accessor:
54
-
55
- ```ruby
56
- class Product < ResourceRecord
57
- has_cents :price_cents
58
- validates :price_cents, numericality: { greater_than_or_equal_to: 0 }
59
- end
60
-
61
- product = Product.new(price: -10)
62
- product.valid? # => false
63
- product.errors[:price_cents] # => ["must be greater than or equal to 0"]
64
- product.errors[:price] # => ["is invalid"]
65
- ```
66
-
67
- ### Reflection
68
-
69
- ```ruby
70
- Product.has_cents_attributes
71
- # => { price_cents: { name: :price, rate: 100 } }
72
-
73
- Product.has_cents_attribute?(:price_cents) # => true
74
- Product.has_cents_attribute?(:name) # => false
75
- ```
76
-
77
- ## Labeling
78
-
79
- The `to_label` method provides a human-readable representation for dropdowns and displays:
80
-
81
- ```ruby
82
- post.to_label # => "My Post Title"
83
- user.to_label # => "John Doe"
84
- ```
85
-
86
- ### Resolution Order
87
-
88
- 1. Returns `name` attribute if present
89
- 2. Returns `title` attribute if present
90
- 3. Falls back to `"ModelName #id"`
91
-
92
- ```ruby
93
- class Post < ResourceRecord
94
- # Has title column
95
- end
96
-
97
- post = Post.new(title: "Hello World")
98
- post.to_label # => "Hello World"
99
-
100
- post.title = nil
101
- post.to_label # => "Post #123"
102
- ```
103
-
104
- ## Route Parameters
105
-
106
- Customize how records appear in URLs.
107
-
108
- ### Static Parameter
109
-
110
- Use a specific column for URLs:
111
-
112
- ```ruby
113
- class Post < ResourceRecord
114
- path_parameter :slug
115
- end
116
- ```
117
-
118
- ```ruby
119
- post = Post.create(slug: "hello-world")
120
- post.to_param # => "hello-world"
121
-
122
- # URL: /posts/hello-world
123
- Post.from_path_param("hello-world") # Finds by slug
124
- ```
125
-
126
- ### Dynamic Parameter
127
-
128
- Combine ID with a readable slug:
129
-
130
- ```ruby
131
- class Post < ResourceRecord
132
- dynamic_path_parameter :title
133
- end
134
- ```
135
-
136
- ```ruby
137
- post = Post.create(id: 42, title: "Hello World")
138
- post.to_param # => "42-hello-world"
139
-
140
- # URL: /posts/42-hello-world
141
- Post.from_path_param("42-hello-world") # Extracts ID, finds by id
142
- ```
143
-
144
- ## Secure Association SGIDs
145
-
146
- Associations automatically get Signed Global ID accessors for secure form handling.
147
-
148
- ### Singular Associations (belongs_to, has_one)
149
-
150
- ```ruby
151
- class Post < ResourceRecord
152
- belongs_to :author
153
- end
154
- ```
155
-
156
- Generates:
157
-
158
- ```ruby
159
- post.author_sgid # => SignedGlobalID for the author
160
- post.author_sgid = sgid # Locates and assigns author from SGID
161
- ```
162
-
163
- ### Collection Associations (has_many)
164
-
165
- ```ruby
166
- class Post < ResourceRecord
167
- has_many :tags
168
- end
169
- ```
170
-
171
- Generates:
172
-
173
- ```ruby
174
- post.tag_sgids # => Array of SignedGlobalIDs
175
- post.tag_sgids = [sgid1, sgid2] # Locates and assigns tags from SGIDs
176
- post.add_tag_sgid(sgid) # Add a single tag by SGID
177
- post.remove_tag_sgid(sgid) # Remove a single tag by SGID
178
- ```
179
-
180
- ### Use Case
181
-
182
- These methods enable secure association inputs in forms without exposing database IDs:
183
-
184
- ```ruby
185
- # In form
186
- f.secure_association_tag # Uses SGIDs instead of IDs
187
- ```
188
-
189
- ## associated_with Scope
190
-
191
- Finds records associated with a given parent. Used internally for nested resource scoping.
192
-
193
- ```ruby
194
- Comment.associated_with(post) # Comments belonging to post
195
- ```
196
-
197
- ### Resolution Order
198
-
199
- 1. Checks for custom scope: `associated_with_#{model_name}`
200
- 2. Finds direct association from self to record
201
- 3. Finds reverse association from record to self (with performance warning)
202
- 4. Raises error with helpful message
203
-
204
- ### Custom Scope
205
-
206
- For complex relationships, define a named scope:
207
-
208
- ```ruby
209
- class Comment < ResourceRecord
210
- # Comments belong to posts, which belong to organizations
211
- scope :associated_with_organization, ->(org) {
212
- joins(:post).where(posts: { organization_id: org.id })
213
- }
214
- end
215
- ```
216
-
217
- ## Field Name Introspection
218
-
219
- Class methods for discovering model fields by type:
220
-
221
- ```ruby
222
- Post.resource_field_names # All fields suitable for forms/displays
223
- Post.content_column_field_names # Database content columns
224
- Post.belongs_to_association_field_names # belongs_to associations
225
- Post.has_one_association_field_names # has_one associations (excluding attachments)
226
- Post.has_many_association_field_names # has_many associations (excluding attachments)
227
- Post.has_one_attached_field_names # Single file attachments
228
- Post.has_many_attached_field_names # Multiple file attachments
229
- ```
230
-
231
- These methods are cached in non-local environments for performance.
232
-
233
- ## Nested Attributes Introspection
234
-
235
- ```ruby
236
- Post.all_nested_attributes_options
237
- # => {
238
- # comments: { allow_destroy: true, limit: 10, macro: :has_many, class: Comment },
239
- # metadata: { update_only: true, macro: :has_one, class: PostMetadata }
240
- # }
241
- ```
242
-
243
- Returns configuration for all associations with `accepts_nested_attributes_for`.
244
-
245
- ## Related
246
-
247
- - [Model Reference](./index)
248
- - [Nested Resources Guide](/guides/nested-resources)