plutonium 0.33.1 → 0.34.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -0,0 +1,445 @@
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
+ ## Constructor
160
+
161
+ Interactions require `view_context:` and accept attributes as keyword arguments:
162
+
163
+ ```ruby
164
+ interaction = PublishPost.new(
165
+ view_context: view_context,
166
+ resource: post,
167
+ notify: true
168
+ )
169
+ ```
170
+
171
+ The controller handles this automatically for interactive actions.
172
+
173
+ ## Calling Interactions
174
+
175
+ ### Via call Class Method
176
+
177
+ ```ruby
178
+ outcome = PublishPost.call(view_context: view_context, resource: post)
179
+
180
+ if outcome.success?
181
+ # Handle success
182
+ else
183
+ # Handle failure
184
+ end
185
+ ```
186
+
187
+ ### Via call Instance Method
188
+
189
+ ```ruby
190
+ interaction = PublishPost.new(view_context: view_context, resource: post)
191
+ outcome = interaction.call
192
+ ```
193
+
194
+ ## Success Outcomes
195
+
196
+ ### Basic Success
197
+
198
+ ```ruby
199
+ succeed(resource)
200
+ ```
201
+
202
+ ### With Message
203
+
204
+ ```ruby
205
+ succeed(resource).with_message("Post published!")
206
+ succeed(resource).with_message("Warning: limited visibility", :alert)
207
+ ```
208
+
209
+ ### With Redirect
210
+
211
+ ```ruby
212
+ succeed(resource).with_redirect_response(posts_path)
213
+ succeed(resource).with_redirect_response(resource, status: :see_other)
214
+ ```
215
+
216
+ ### With File Download
217
+
218
+ ```ruby
219
+ succeed(resource).with_file_response(file_path, filename: "report.pdf")
220
+ ```
221
+
222
+ ### With Render
223
+
224
+ ```ruby
225
+ succeed(resource).with_render_response(:custom_template)
226
+ ```
227
+
228
+ ### Chaining
229
+
230
+ ```ruby
231
+ succeed(resource)
232
+ .with_message("Created!")
233
+ .with_redirect_response(edit_post_path(resource))
234
+ ```
235
+
236
+ ## Failure Outcomes
237
+
238
+ ### Simple Failure
239
+
240
+ ```ruby
241
+ failed("Cannot publish draft posts")
242
+ ```
243
+
244
+ ### With Attribute
245
+
246
+ ```ruby
247
+ failed("is invalid", :email)
248
+ ```
249
+
250
+ ### With Hash of Errors
251
+
252
+ ```ruby
253
+ failed(email: "is invalid", name: "is required")
254
+ ```
255
+
256
+ ### With ActiveModel Errors
257
+
258
+ ```ruby
259
+ failed(resource.errors)
260
+ ```
261
+
262
+ ### Manual Error Addition
263
+
264
+ ```ruby
265
+ def execute
266
+ errors.add(:base, "Post must have content")
267
+ return failure if errors.any?
268
+
269
+ # Continue...
270
+ end
271
+ ```
272
+
273
+ ## Chaining Interactions
274
+
275
+ Use `and_then` to chain operations. On failure, the chain short-circuits:
276
+
277
+ ```ruby
278
+ def execute
279
+ CreateUserInteraction.call(view_context:, **user_params)
280
+ .and_then { |result| SendWelcomeEmail.call(view_context:, user: result.value) }
281
+ .and_then { |result| LogActivity.call(view_context:, user: result.value) }
282
+ .with_message("User created and welcomed!")
283
+ end
284
+ ```
285
+
286
+ ## Accessing Current User
287
+
288
+ ```ruby
289
+ def execute
290
+ resource.update!(updated_by: current_user)
291
+ succeed(resource)
292
+ end
293
+
294
+ # current_user is provided by the base class:
295
+ # def current_user
296
+ # view_context.controller.helpers.current_user
297
+ # end
298
+ ```
299
+
300
+ ## Complete Example
301
+
302
+ ```ruby
303
+ class Company::InviteUserInteraction < Plutonium::Resource::Interaction
304
+ presents label: "Invite User",
305
+ icon: Phlex::TablerIcons::UserPlus,
306
+ description: "Send an invitation email"
307
+
308
+ attribute :resource # The company
309
+ attribute :email, :string
310
+ attribute :role, :string
311
+
312
+ input :email, as: :email
313
+ input :role, as: :select, choices: -> { UserInvite.roles.keys }
314
+
315
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
316
+ validates :role, presence: true, inclusion: { in: UserInvite.roles.keys }
317
+ validate :not_already_invited
318
+
319
+ private
320
+
321
+ def execute
322
+ invite = UserInvite.create!(
323
+ company: resource,
324
+ email: email,
325
+ role: role,
326
+ invited_by: current_user
327
+ )
328
+ UserInviteMailer.invitation(invite).deliver_later
329
+
330
+ succeed(resource)
331
+ .with_message("Invitation sent to #{email}")
332
+ .with_redirect_response(resource)
333
+ rescue ActiveRecord::RecordInvalid => e
334
+ failed(e.record.errors)
335
+ end
336
+
337
+ def not_already_invited
338
+ return unless email.present?
339
+
340
+ if UserInvite.exists?(company: resource, email: email, state: :pending)
341
+ errors.add(:email, "already has a pending invitation")
342
+ end
343
+ end
344
+ end
345
+ ```
346
+
347
+ ## Connecting to Definitions
348
+
349
+ Register interactions as actions in definitions:
350
+
351
+ ```ruby
352
+ class PostDefinition < Plutonium::Resource::Definition
353
+ action :publish, interaction: PublishPostInteraction
354
+ action :invite_user, interaction: InviteUserInteraction
355
+
356
+ action :archive,
357
+ interaction: ArchiveInteraction,
358
+ confirmation: "Are you sure?",
359
+ category: :danger,
360
+ position: 100
361
+ end
362
+ ```
363
+
364
+ ## Immediate vs Form Actions
365
+
366
+ Plutonium determines if an action needs a form based on whether inputs are defined:
367
+
368
+ **Shows form first** (has inputs):
369
+
370
+ ```ruby
371
+ class InviteUserInteraction < Plutonium::Resource::Interaction
372
+ attribute :resource
373
+ attribute :email
374
+ input :email # This triggers form display
375
+ end
376
+ ```
377
+
378
+ **Executes immediately** (no inputs):
379
+
380
+ ```ruby
381
+ class ArchiveInteraction < Plutonium::Resource::Interaction
382
+ attribute :resource
383
+ # No inputs = immediate execution with confirmation
384
+ end
385
+ ```
386
+
387
+ ## Policy Integration
388
+
389
+ Control access with policy methods matching the action name:
390
+
391
+ ```ruby
392
+ class PostPolicy < Plutonium::Resource::Policy
393
+ def publish?
394
+ update? && record.draft?
395
+ end
396
+
397
+ def archive?
398
+ destroy? && !record.archived?
399
+ end
400
+ end
401
+ ```
402
+
403
+ ## Testing
404
+
405
+ ```ruby
406
+ RSpec.describe PublishPost do
407
+ let(:view_context) { double("view_context", controller: double(helpers: double(current_user: user))) }
408
+ let(:user) { create(:user) }
409
+ let(:post) { create(:post, user: user, published: false) }
410
+
411
+ describe '#call' do
412
+ it 'publishes the post' do
413
+ interaction = described_class.new(view_context: view_context, resource: post)
414
+ outcome = interaction.call
415
+
416
+ expect(outcome).to be_success
417
+ expect(post.reload).to be_published
418
+ end
419
+
420
+ context 'when validation fails' do
421
+ it 'returns failure outcome' do
422
+ interaction = described_class.new(view_context: view_context, resource: nil)
423
+ outcome = interaction.call
424
+
425
+ expect(outcome).to be_failure
426
+ end
427
+ end
428
+ end
429
+ end
430
+ ```
431
+
432
+ ## Best Practices
433
+
434
+ 1. **Keep interactions focused** - One action per interaction
435
+ 2. **Use validations** - Validate all inputs before execution
436
+ 3. **Handle errors gracefully** - Rescue exceptions and return `failed()`
437
+ 4. **Return meaningful messages** - Help users understand what happened
438
+ 5. **Use `and_then` for chains** - Compose complex workflows from simple interactions
439
+ 6. **Declare attributes explicitly** - Always declare `resource` or `resources` attributes
440
+
441
+ ## Related
442
+
443
+ - [Actions Reference](/reference/definition/actions) - Connecting interactions to definitions
444
+ - [Fields Reference](/reference/definition/fields) - Input configuration
445
+ - [Policy Reference](/reference/policy/) - Authorization
@@ -0,0 +1,248 @@
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)