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,286 @@
1
+ ---
2
+ name: model-features
3
+ description: Plutonium model features - has_cents, associations, scopes, and routing
4
+ ---
5
+
6
+ # Plutonium Model Features
7
+
8
+ Advanced features available in Plutonium resource models.
9
+
10
+ ## Monetary Handling (has_cents)
11
+
12
+ Store monetary values as integers (cents) while exposing decimal interfaces.
13
+
14
+ ### Basic Usage
15
+
16
+ ```ruby
17
+ class Product < ResourceRecord
18
+ has_cents :price_cents # Creates price getter/setter
19
+ has_cents :cost_cents, name: :wholesale # Custom accessor name
20
+ has_cents :tax_cents, rate: 1000 # 3 decimal places
21
+ has_cents :quantity_cents, rate: 1 # Whole numbers only
22
+ end
23
+
24
+ product = Product.new
25
+ product.price = 19.99
26
+ product.price_cents # => 1999
27
+ product.price # => 19.99
28
+
29
+ # Truncates (doesn't round)
30
+ product.price = 10.999
31
+ product.price_cents # => 1099
32
+ ```
33
+
34
+ ### Options
35
+
36
+ ```ruby
37
+ has_cents :field_cents,
38
+ name: :custom_name, # Accessor name (default: field without _cents)
39
+ rate: 100, # Conversion rate (default: 100)
40
+ suffix: "amount" # Suffix for generated name (default: "amount")
41
+ ```
42
+
43
+ ### Validation
44
+
45
+ ```ruby
46
+ class Product < ResourceRecord
47
+ has_cents :price_cents
48
+
49
+ # Validate the cents field
50
+ validates :price_cents, numericality: {greater_than: 0}
51
+ end
52
+
53
+ product = Product.new(price: -10)
54
+ product.valid? # => false
55
+ product.errors[:price_cents] # => ["must be greater than 0"]
56
+ product.errors[:price] # => ["is invalid"] (propagated)
57
+ ```
58
+
59
+ ### Introspection
60
+
61
+ ```ruby
62
+ Product.has_cents_attributes
63
+ # => {price_cents: {name: :price, rate: 100}, ...}
64
+
65
+ Product.has_cents_attribute?(:price_cents) # => true
66
+ ```
67
+
68
+ ## Association SGID Support
69
+
70
+ All associations get Signed Global ID (SGID) methods for secure serialization.
71
+
72
+ ### Singular Associations (belongs_to, has_one)
73
+
74
+ ```ruby
75
+ class Post < ResourceRecord
76
+ belongs_to :user
77
+ has_one :featured_image
78
+ end
79
+
80
+ post = Post.first
81
+
82
+ # Get SGID
83
+ post.user_sgid # => "BAh7CEkiCG..."
84
+ post.featured_image_sgid # => "BAh7CEkiCG..."
85
+
86
+ # Set by SGID (finds and assigns)
87
+ post.user_sgid = "BAh7CEkiCG..."
88
+ post.featured_image_sgid = "BAh7CEkiCG..."
89
+ ```
90
+
91
+ ### Collection Associations (has_many, has_and_belongs_to_many)
92
+
93
+ ```ruby
94
+ class User < ResourceRecord
95
+ has_many :posts
96
+ has_and_belongs_to_many :roles
97
+ end
98
+
99
+ user = User.first
100
+
101
+ # Get SGIDs
102
+ user.post_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
103
+ user.role_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
104
+
105
+ # Bulk assignment
106
+ user.post_sgids = ["BAh7CEkiCG...", ...]
107
+
108
+ # Individual manipulation
109
+ user.add_post_sgid("BAh7CEkiCG...") # Add to collection
110
+ user.remove_post_sgid("BAh7CEkiCG...") # Remove from collection
111
+ ```
112
+
113
+ ### Use Cases
114
+
115
+ - Secure form submissions without exposing internal IDs
116
+ - API responses with portable references
117
+ - Caching and serialization
118
+
119
+ ## Entity Scoping (associated_with)
120
+
121
+ Query records associated with another record. Essential for multi-tenant apps.
122
+
123
+ ### Basic Usage
124
+
125
+ ```ruby
126
+ class Comment < ResourceRecord
127
+ belongs_to :post
128
+ end
129
+
130
+ # Find comments for a post
131
+ Comment.associated_with(post)
132
+ # => Comment.where(post: post)
133
+ ```
134
+
135
+ ### Association Detection
136
+
137
+ Works with:
138
+ - `belongs_to` - Uses WHERE clause (most efficient)
139
+ - `has_one` - Uses JOIN + WHERE
140
+ - `has_many` - Uses JOIN + WHERE
141
+
142
+ ```ruby
143
+ # Direct association (preferred)
144
+ Comment.associated_with(post) # WHERE post_id = ?
145
+
146
+ # Reverse association (less efficient, logs warning)
147
+ Post.associated_with(comment) # JOIN comments WHERE comments.id = ?
148
+ ```
149
+
150
+ ### Custom Scopes
151
+
152
+ For optimal performance, define custom scopes:
153
+
154
+ ```ruby
155
+ class Comment < ResourceRecord
156
+ # Custom scope naming: associated_with_{model_name}
157
+ scope :associated_with_user, ->(user) do
158
+ joins(:post).where(posts: {user_id: user.id})
159
+ end
160
+ end
161
+
162
+ # Automatically uses custom scope
163
+ Comment.associated_with(user)
164
+ ```
165
+
166
+ ### Error Handling
167
+
168
+ ```ruby
169
+ # When no association exists
170
+ UnrelatedModel.associated_with(user)
171
+ # Raises: Could not resolve the association between 'UnrelatedModel' and 'User'
172
+ #
173
+ # Define:
174
+ # 1. the associations between the models
175
+ # 2. a named scope on UnrelatedModel e.g.
176
+ #
177
+ # scope :associated_with_user, ->(user) { do_something_here }
178
+ ```
179
+
180
+ ## URL Routing
181
+
182
+ ### Default Behavior
183
+
184
+ ```ruby
185
+ user = User.find(1)
186
+ user.to_param # => "1"
187
+ ```
188
+
189
+ ### Custom Path Parameters
190
+
191
+ Use a stable, unique field instead of ID:
192
+
193
+ ```ruby
194
+ class User < ResourceRecord
195
+ private
196
+
197
+ def path_parameter(param_name)
198
+ :username # Must be unique
199
+ end
200
+ end
201
+
202
+ user = User.create(username: "john_doe")
203
+ user.to_param # => "john_doe"
204
+ # URLs: /users/john_doe
205
+ ```
206
+
207
+ ### Dynamic Path Parameters (SEO-friendly)
208
+
209
+ Include ID prefix for uniqueness with human-readable suffix:
210
+
211
+ ```ruby
212
+ class Article < ResourceRecord
213
+ private
214
+
215
+ def dynamic_path_parameter(param_name)
216
+ :title
217
+ end
218
+ end
219
+
220
+ article = Article.create(id: 1, title: "My Great Article")
221
+ article.to_param # => "1-my-great-article"
222
+ # URLs: /articles/1-my-great-article
223
+ ```
224
+
225
+ ### Path Parameter Lookup
226
+
227
+ ```ruby
228
+ # Scope for finding by path parameter
229
+ User.from_path_param("john_doe")
230
+ Article.from_path_param("1-my-great-article") # Extracts ID
231
+ ```
232
+
233
+ ## Association Route Discovery
234
+
235
+ ```ruby
236
+ class User < ResourceRecord
237
+ has_many :posts
238
+ has_many :comments
239
+ accepts_nested_attributes_for :posts
240
+ end
241
+
242
+ # Get has_many association names
243
+ User.has_many_association_routes
244
+ # => ["posts", "comments"]
245
+
246
+ # Get nested attributes config
247
+ User.all_nested_attributes_options
248
+ # => {posts: {allow_destroy: false, update_only: false, macro: :has_many, class: Post}}
249
+ ```
250
+
251
+ ## Performance Tips
252
+
253
+ ### Field Introspection
254
+
255
+ ```ruby
256
+ # Cached in production, fresh in development
257
+ User.resource_field_names # First call queries, subsequent cached
258
+ ```
259
+
260
+ ### Association Queries
261
+
262
+ ```ruby
263
+ # Efficient: Direct belongs_to
264
+ Comment.associated_with(post) # Simple WHERE
265
+
266
+ # Less efficient: Reverse has_many (logs warning)
267
+ Post.associated_with(comment) # JOIN required
268
+
269
+ # Optimal: Custom scope when direct isn't possible
270
+ scope :associated_with_user, ->(user) { where(user_id: user.id) }
271
+ ```
272
+
273
+ ### SGID Operations
274
+
275
+ ```ruby
276
+ # Efficient: Batch assignment
277
+ user.post_sgids = sgid_array # Single operation
278
+
279
+ # Inefficient: Individual adds
280
+ sgid_array.each { |sgid| user.add_post_sgid(sgid) }
281
+ ```
282
+
283
+ ## Related Skills
284
+
285
+ - `model` - Model overview and structure
286
+ - `create-resource` - Scaffold generator
@@ -0,0 +1,274 @@
1
+ ---
2
+ name: nested-resources
3
+ description: Plutonium nested resources - parent/child routes, scoping, and URL generation
4
+ ---
5
+
6
+ # Nested Resources
7
+
8
+ Plutonium automatically creates nested routes for `has_many` associations, scopes queries to the parent, and handles URL generation.
9
+
10
+ ## How It Works
11
+
12
+ When you register resources with parent-child relationships:
13
+
14
+ ```ruby
15
+ # In portal routes
16
+ register_resource ::Company
17
+ register_resource ::Property # has belongs_to :company
18
+ ```
19
+
20
+ Plutonium automatically creates nested routes:
21
+ - `/companies/:company_id/properties` - Properties scoped to company
22
+ - `/companies/:company_id/properties/new` - New property for company
23
+ - `/companies/:company_id/properties/:id` - Property in company context
24
+
25
+ ## Automatic Behavior
26
+
27
+ When accessing nested routes, Plutonium automatically:
28
+
29
+ 1. **Resolves the parent** via `current_parent`
30
+ 2. **Scopes queries** to only show records belonging to parent
31
+ 3. **Assigns parent** to new records on create
32
+ 4. **Hides parent field** in forms (already determined by URL)
33
+ 5. **Authorizes parent access** before proceeding
34
+
35
+ ## Controller Methods
36
+
37
+ ### current_parent
38
+
39
+ Returns the parent record from the URL:
40
+
41
+ ```ruby
42
+ # URL: /companies/123/properties
43
+ current_parent # => Company.find(123)
44
+ ```
45
+
46
+ ### parent_route_param
47
+
48
+ The URL parameter containing the parent ID:
49
+
50
+ ```ruby
51
+ parent_route_param # => :company_id
52
+ ```
53
+
54
+ ### parent_input_param
55
+
56
+ The association name on the child model:
57
+
58
+ ```ruby
59
+ parent_input_param # => :company
60
+ ```
61
+
62
+ ## Presentation Hooks
63
+
64
+ Control whether parent field appears in views/forms:
65
+
66
+ ```ruby
67
+ class PropertiesController < ResourceController
68
+ private
69
+
70
+ # Show parent field in displays (default: false)
71
+ def present_parent?
72
+ true
73
+ end
74
+
75
+ # Allow changing parent in forms (default: same as present_parent?)
76
+ def submit_parent?
77
+ false # Parent is set from URL, don't allow changing
78
+ end
79
+ end
80
+ ```
81
+
82
+ ## Query Scoping
83
+
84
+ Collections are automatically scoped to the parent via policies:
85
+
86
+ ```ruby
87
+ class PropertyPolicy < ResourcePolicy
88
+ relation_scope do |relation|
89
+ relation = super(relation) # Applies associated_with(entity_scope)
90
+ # entity_scope is the current_parent
91
+ relation
92
+ end
93
+ end
94
+ ```
95
+
96
+ The `associated_with` scope finds records belonging to the parent:
97
+
98
+ ```ruby
99
+ # Automatic detection via belongs_to
100
+ Property.associated_with(company)
101
+ # => Property.where(company: company)
102
+ ```
103
+
104
+ ### Custom Association Scope
105
+
106
+ For complex relationships, define a custom scope:
107
+
108
+ ```ruby
109
+ class Property < ResourceRecord
110
+ scope :associated_with_organization, ->(org) {
111
+ joins(:company).where(companies: { organization_id: org.id })
112
+ }
113
+ end
114
+ ```
115
+
116
+ ## URL Generation
117
+
118
+ Use `resource_url_for` with the `parent:` option:
119
+
120
+ ```ruby
121
+ # Child collection
122
+ resource_url_for(Property, parent: company)
123
+ # => /companies/123/properties
124
+
125
+ # Child record
126
+ resource_url_for(property, parent: company)
127
+ # => /companies/123/properties/456
128
+
129
+ # New child form
130
+ resource_url_for(Property, action: :new, parent: company)
131
+ # => /companies/123/properties/new
132
+
133
+ # Edit child
134
+ resource_url_for(property, action: :edit, parent: company)
135
+ # => /companies/123/properties/456/edit
136
+ ```
137
+
138
+ ## Association Panels
139
+
140
+ On the parent's show page, child resources are displayed via association panels:
141
+
142
+ ```ruby
143
+ class CompanyPolicy < ResourcePolicy
144
+ def permitted_associations
145
+ %i[properties contacts] # Shows panels for these
146
+ end
147
+ end
148
+ ```
149
+
150
+ The panel loads children via the nested route automatically.
151
+
152
+ ## Authorization
153
+
154
+ ### Parent Authorization
155
+
156
+ The parent is authorized for `:read?` before `current_parent` returns:
157
+
158
+ ```ruby
159
+ def current_parent
160
+ # ... resolution logic ...
161
+ authorize! parent, to: :read?
162
+ parent
163
+ end
164
+ ```
165
+
166
+ ### Policy Context
167
+
168
+ The parent is passed to child policies as `entity_scope`:
169
+
170
+ ```ruby
171
+ class PropertyPolicy < ResourcePolicy
172
+ def create?
173
+ # entity_scope is the parent company
174
+ entity_scope.present? && user.member_of?(entity_scope)
175
+ end
176
+
177
+ def read?
178
+ entity_scope.present? && record.company == entity_scope
179
+ end
180
+ end
181
+ ```
182
+
183
+ ## Parameter Handling
184
+
185
+ Parent is automatically injected into resource params:
186
+
187
+ ```ruby
188
+ # When creating a property under /companies/123/properties
189
+ resource_params
190
+ # => { name: "...", company: <Company:123>, company_id: 123 }
191
+ ```
192
+
193
+ You don't need to include hidden fields for the parent in forms.
194
+
195
+ ## Nesting Limitations
196
+
197
+ Plutonium supports **one level of nesting**:
198
+
199
+ - ✅ `/companies/:company_id/properties` (parent → child)
200
+ - ❌ `/companies/:company_id/properties/:property_id/units` (grandparent → parent → child)
201
+
202
+ ## Common Patterns
203
+
204
+ ### Scoped Uniqueness
205
+
206
+ Validate uniqueness within parent:
207
+
208
+ ```ruby
209
+ class Property < ResourceRecord
210
+ belongs_to :company
211
+ validates :code, uniqueness: { scope: :company_id }
212
+ end
213
+ ```
214
+
215
+ ### Conditional Parent Display
216
+
217
+ Show parent only in certain contexts:
218
+
219
+ ```ruby
220
+ class PropertiesController < ResourceController
221
+ private
222
+
223
+ def present_parent?
224
+ # Show parent when accessed standalone, hide when nested
225
+ current_parent.nil?
226
+ end
227
+ end
228
+ ```
229
+
230
+ ### Custom Parent Resolution
231
+
232
+ Override parent lookup:
233
+
234
+ ```ruby
235
+ class PropertiesController < ResourceController
236
+ private
237
+
238
+ def current_parent
239
+ @current_parent ||= Company.friendly.find(params[:company_id])
240
+ end
241
+ end
242
+ ```
243
+
244
+ ### Breadcrumbs
245
+
246
+ Breadcrumbs automatically include the parent:
247
+
248
+ ```
249
+ Companies > Acme Corp > Properties > Property #123
250
+ ```
251
+
252
+ ## Route Registration with Custom Routes
253
+
254
+ Add custom member/collection routes to nested resources:
255
+
256
+ ```ruby
257
+ register_resource ::Property do
258
+ member do
259
+ get :analytics
260
+ post :archive
261
+ end
262
+ end
263
+ ```
264
+
265
+ Generates nested routes:
266
+ - `/companies/:company_id/properties/:id/analytics`
267
+ - `/companies/:company_id/properties/:id/archive`
268
+
269
+ ## Related Skills
270
+
271
+ - `portal` - Route registration
272
+ - `policy` - Authorization and scoping
273
+ - `controller` - Presentation hooks
274
+ - `model-features` - associated_with scope