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,218 +0,0 @@
1
- # Model Reference
2
-
3
- Complete reference for Plutonium resource models.
4
-
5
- ## Base Class
6
-
7
- All resource models inherit from `ResourceRecord`:
8
-
9
- ```ruby
10
- class Post < ResourceRecord
11
- # Your model code
12
- end
13
- ```
14
-
15
- In packages, models inherit from the package's ResourceRecord:
16
-
17
- ```ruby
18
- module Blogging
19
- class Post < Blogging::ResourceRecord
20
- # Your model code
21
- end
22
- end
23
- ```
24
-
25
- `ResourceRecord` is an abstract class that inherits from `ApplicationRecord` and is created by the Plutonium installer.
26
-
27
- ## Standard ActiveRecord Features
28
-
29
- All standard ActiveRecord features work:
30
-
31
- ```ruby
32
- class Post < ResourceRecord
33
- # Associations
34
- belongs_to :user
35
- has_many :comments, dependent: :destroy
36
- has_one :featured_image
37
- has_many :tags, through: :post_tags
38
-
39
- # Validations
40
- validates :title, presence: true, length: { maximum: 200 }
41
- validates :slug, uniqueness: true
42
- validates :status, inclusion: { in: %w[draft published] }
43
-
44
- # Scopes
45
- scope :published, -> { where(status: 'published') }
46
- scope :recent, -> { order(created_at: :desc) }
47
- scope :by_author, ->(user) { where(user: user) }
48
-
49
- # Callbacks
50
- before_save :generate_slug
51
- after_create :notify_subscribers
52
-
53
- # Methods
54
- def publish!
55
- update!(status: 'published', published_at: Time.current)
56
- end
57
- end
58
- ```
59
-
60
- ## Nested Resources
61
-
62
- Nesting is automatic via `belongs_to` associations:
63
-
64
- ```ruby
65
- class Comment < ResourceRecord
66
- belongs_to :post
67
- end
68
- ```
69
-
70
- When both `Post` and `Comment` are registered in a portal, Plutonium automatically creates nested routes (`/posts/:post_id/nested_comments`). Queries are automatically scoped to the parent via the association.
71
-
72
- See the [Nested Resources Guide](/guides/nested-resources) for details.
73
-
74
- ## Entity Scoping (Multi-tenancy)
75
-
76
- Entity scoping is configured on the **portal engine**, not the model:
77
-
78
- ```ruby
79
- # packages/customer_portal/lib/engine.rb
80
- module CustomerPortal
81
- class Engine < Rails::Engine
82
- include Plutonium::Portal::Engine
83
-
84
- config.after_initialize do
85
- scope_to_entity Organization
86
- end
87
- end
88
- end
89
- ```
90
-
91
- See the [Multi-tenancy Guide](/guides/multi-tenancy) for details.
92
-
93
- ## Plutonium Features
94
-
95
- See [Model Features](./features) for:
96
- - `has_cents` - Store monetary values as integers, expose as decimals
97
- - `to_label` - Human-readable record labels
98
- - `path_parameter` / `dynamic_path_parameter` - Custom URL parameters
99
- - Secure association SGIDs - Auto-generated SGID accessors for associations
100
- - `associated_with` - Scope for nested resource queries
101
- - Field introspection methods
102
-
103
- ## Field Introspection
104
-
105
- Plutonium introspects models to detect:
106
-
107
- ### Column Types
108
-
109
- | Database Type | Detected As |
110
- |--------------|-------------|
111
- | `string` | `:string` |
112
- | `text` | `:text` |
113
- | `integer` | `:integer` |
114
- | `bigint` | `:integer` |
115
- | `float` | `:float` |
116
- | `decimal` | `:decimal` |
117
- | `boolean` | `:boolean` |
118
- | `date` | `:date` |
119
- | `datetime` | `:datetime` |
120
- | `time` | `:time` |
121
- | `json`/`jsonb` | `:json` |
122
-
123
- ### Constraints
124
-
125
- ```ruby
126
- # NULL constraint detected
127
- t.string :title, null: false # Required field
128
- ```
129
-
130
- ### Associations
131
-
132
- ```ruby
133
- belongs_to :user # Detected as association field
134
- has_many :comments # Available for association panels
135
- ```
136
-
137
- ### Validations
138
-
139
- ```ruby
140
- validates :title, presence: true # Required
141
- validates :email, format: { ... } # Format hint
142
- validates :role, inclusion: { in: [...] } # Select options
143
- ```
144
-
145
- ## Model Organization
146
-
147
- ### Feature Package Models
148
-
149
- ```ruby
150
- # packages/blogging/app/models/blogging/post.rb
151
- module Blogging
152
- class Post < ResourceRecord
153
- # Namespaced model
154
- end
155
- end
156
- ```
157
-
158
- ### Table Naming
159
-
160
- Namespaced models use prefixed tables:
161
-
162
- ```ruby
163
- module Blogging
164
- class Post < ResourceRecord
165
- # Table: blogging_posts
166
- end
167
- end
168
- ```
169
-
170
- Override if needed:
171
-
172
- ```ruby
173
- self.table_name = "posts"
174
- ```
175
-
176
- ## Best Practices
177
-
178
- ### Keep Models Thin
179
-
180
- Put complex logic in Interactions:
181
-
182
- ```ruby
183
- # Model: simple validations and associations
184
- class Post < ResourceRecord
185
- validates :title, presence: true
186
- end
187
-
188
- # Interaction: complex logic
189
- class PublishPost < ResourceInteraction
190
- def execute
191
- resource.update!(published: true)
192
- notify_subscribers
193
- update_search_index
194
- succeed(resource)
195
- end
196
- end
197
- ```
198
-
199
- ### Use Meaningful Scopes
200
-
201
- ```ruby
202
- # Good: intention-revealing names
203
- scope :visible_to, ->(user) { where(user: user).or(where(published: true)) }
204
-
205
- # Avoid: generic names
206
- scope :filtered, -> { where(status: 'active') }
207
- ```
208
-
209
- ### Validate at the Right Level
210
-
211
- - **Model**: Data integrity (presence, format, uniqueness)
212
- - **Interaction**: Business rules (can only publish once)
213
- - **Policy**: Authorization (user must own the record)
214
-
215
- ## Related
216
-
217
- - [Model Features](./features)
218
- - [Definition Reference](/reference/definition/)
@@ -1,456 +0,0 @@
1
- # Policy Reference
2
-
3
- Complete reference for authorization policies. Built on [ActionPolicy](https://actionpolicy.evilmartians.io/).
4
-
5
- ## Overview
6
-
7
- Policies control authorization at three levels:
8
- 1. **Action Permissions** - Can user perform this action?
9
- 2. **Attribute Permissions** - Which fields can user access?
10
- 3. **Scope Permissions** - Which records can user see?
11
-
12
- ## Base Class
13
-
14
- ```ruby
15
- class PostPolicy < Plutonium::Resource::Policy
16
- # Policy code
17
- end
18
- ```
19
-
20
- In packages, inherit from the package's ResourcePolicy:
21
-
22
- ```ruby
23
- module AdminPortal
24
- class PostPolicy < ::PostPolicy
25
- # Portal-specific overrides
26
- end
27
- end
28
- ```
29
-
30
- ## Authorization Context
31
-
32
- Inside a policy, you have access to:
33
-
34
- | Variable | Description |
35
- |----------|-------------|
36
- | `user` | Current authenticated user (required) |
37
- | `record` | Resource being authorized |
38
- | `entity_scope` | Current scoped entity (for multi-tenancy) |
39
- | `parent` | Parent record for nested resources (nil if not nested) |
40
- | `parent_association` | Association name on parent (e.g., `:comments`) |
41
-
42
- ```ruby
43
- def update?
44
- user # => Current user
45
- record # => The Post instance
46
- entity_scope # => Organization for multi-tenant portals
47
- parent # => Parent record (for nested routes)
48
- parent_association # => :comments (association name)
49
- end
50
- ```
51
-
52
- ## Action Permissions
53
-
54
- ### Core Actions (Must Override)
55
-
56
- These default to `false` - you must override them:
57
-
58
- ```ruby
59
- class PostPolicy < Plutonium::Resource::Policy
60
- def create?
61
- user.present?
62
- end
63
-
64
- def read?
65
- true
66
- end
67
- end
68
- ```
69
-
70
- ### Derived Actions
71
-
72
- These inherit from core actions by default:
73
-
74
- | Method | Inherits From | Override When |
75
- |--------|---------------|---------------|
76
- | `update?` | `create?` | Different update rules |
77
- | `destroy?` | `create?` | Different delete rules |
78
- | `index?` | `read?` | Custom listing rules |
79
- | `show?` | `read?` | Record-specific read rules |
80
- | `new?` | `create?` | Rarely needed |
81
- | `edit?` | `update?` | Rarely needed |
82
- | `search?` | `index?` | Search-specific rules |
83
-
84
- ### Example with Ownership
85
-
86
- ```ruby
87
- class PostPolicy < Plutonium::Resource::Policy
88
- def create?
89
- user.present?
90
- end
91
-
92
- def read?
93
- true
94
- end
95
-
96
- def update?
97
- owner? || admin?
98
- end
99
-
100
- def destroy?
101
- owner? || admin?
102
- end
103
-
104
- private
105
-
106
- def owner?
107
- record.user_id == user.id
108
- end
109
-
110
- def admin?
111
- user.admin?
112
- end
113
- end
114
- ```
115
-
116
- ### Custom Action Permissions
117
-
118
- For custom actions defined in definitions:
119
-
120
- ```ruby
121
- def publish?
122
- owner? && !record.published?
123
- end
124
-
125
- def archive?
126
- owner? || admin?
127
- end
128
-
129
- def bulk_delete?
130
- admin?
131
- end
132
- ```
133
-
134
- Actions are secure by default - undefined methods return `false`.
135
-
136
- ## Attribute Permissions
137
-
138
- ### Core Methods (Must Override for Production)
139
-
140
- ```ruby
141
- # What users can see (index, show)
142
- def permitted_attributes_for_read
143
- %i[title body author created_at]
144
- end
145
-
146
- # What users can set (create, update)
147
- def permitted_attributes_for_create
148
- %i[title body category_id]
149
- end
150
- ```
151
-
152
- ### Derived Methods
153
-
154
- | Method | Inherits From |
155
- |--------|---------------|
156
- | `permitted_attributes_for_update` | `permitted_attributes_for_create` |
157
- | `permitted_attributes_for_index` | `permitted_attributes_for_read` |
158
- | `permitted_attributes_for_show` | `permitted_attributes_for_read` |
159
- | `permitted_attributes_for_new` | `permitted_attributes_for_create` |
160
- | `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
161
-
162
- ### Conditional Attribute Access
163
-
164
- ```ruby
165
- def permitted_attributes_for_create
166
- attrs = %i[title body]
167
- attrs << :featured if user.admin?
168
- attrs << :author_id if user.admin?
169
- attrs
170
- end
171
-
172
- def permitted_attributes_for_update
173
- case record.status
174
- when 'draft'
175
- %i[title body category_id]
176
- when 'published'
177
- %i[body] # Can only edit body once published
178
- else
179
- []
180
- end
181
- end
182
- ```
183
-
184
- ### Auto-Detection (Development Only)
185
-
186
- In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly:
187
-
188
- ```
189
- 🚨 Resource field auto-detection: PostPolicy#permitted_attributes_for_create
190
- Auto-detected resource fields result in security holes and will fail outside of development.
191
- ```
192
-
193
- ## Association Permissions
194
-
195
- Control which associations appear in panels and forms:
196
-
197
- ```ruby
198
- def permitted_associations
199
- %i[comments tags author]
200
- end
201
- ```
202
-
203
- Returns an empty array by default.
204
-
205
- ## Collection Scoping
206
-
207
- ### relation_scope
208
-
209
- Filter which records users can see using ActionPolicy's `relation_scope`:
210
-
211
- ```ruby
212
- class PostPolicy < Plutonium::Resource::Policy
213
- relation_scope do |relation|
214
- if user.admin?
215
- relation
216
- else
217
- relation.where(published: true).or(
218
- relation.where(user_id: user.id)
219
- )
220
- end
221
- end
222
- end
223
- ```
224
-
225
- ### With Parent Scoping (Nested Resources)
226
-
227
- Call `super` to apply automatic parent scoping for nested resources:
228
-
229
- ```ruby
230
- relation_scope do |relation|
231
- relation = super(relation) # Applies parent scoping automatically
232
-
233
- if user.admin?
234
- relation
235
- else
236
- relation.where(approved: true)
237
- end
238
- end
239
- ```
240
-
241
- **Parent scoping takes precedence over entity scoping.** When a parent is present:
242
- - For `has_many` associations: scopes via `parent.association_name`
243
- - For `has_one` associations: scopes via `where(foreign_key: parent.id)`
244
-
245
- ### With Entity Scoping (Multi-tenancy)
246
-
247
- When no parent is present, `super` applies entity scoping:
248
-
249
- ```ruby
250
- relation_scope do |relation|
251
- relation = super(relation) # Applies associated_with(entity_scope)
252
-
253
- if user.admin?
254
- relation
255
- else
256
- relation.where(published: true)
257
- end
258
- end
259
- ```
260
-
261
- The default `relation_scope` automatically applies `relation.associated_with(entity_scope)` when an entity scope is present and no parent is set.
262
-
263
- ### default_relation_scope is Required
264
-
265
- Plutonium verifies that `default_relation_scope` is called in every `relation_scope`. This prevents accidental multi-tenancy leaks when overriding scopes.
266
-
267
- ```ruby
268
- # ❌ This will raise an error
269
- relation_scope do |relation|
270
- relation.where(published: true) # Missing default_relation_scope!
271
- end
272
-
273
- # ✅ Correct - call default_relation_scope
274
- relation_scope do |relation|
275
- default_relation_scope(relation).where(published: true)
276
- end
277
-
278
- # ✅ Also correct - super calls default_relation_scope
279
- relation_scope do |relation|
280
- super(relation).where(published: true)
281
- end
282
- ```
283
-
284
- When overriding an inherited scope:
285
-
286
- ```ruby
287
- class AdminPostPolicy < PostPolicy
288
- relation_scope do |relation|
289
- # Replace inherited scope but keep Plutonium's parent/entity scoping
290
- default_relation_scope(relation)
291
- end
292
- end
293
- ```
294
-
295
- This method applies parent scoping (for nested resources) or entity scoping (for multi-tenancy) directly, bypassing any inherited scope customizations.
296
-
297
- ### Skipping Default Scoping
298
-
299
- If you intentionally need to bypass scoping, call `skip_default_relation_scope!`:
300
-
301
- ```ruby
302
- relation_scope do |relation|
303
- skip_default_relation_scope!
304
- relation # No parent/entity scoping applied
305
- end
306
- ```
307
-
308
- This should be rare - consider using a separate portal with different scoping rules instead.
309
-
310
- ## Portal-Specific Policies
311
-
312
- Override policies for specific portals:
313
-
314
- ```ruby
315
- # packages/admin_portal/app/policies/admin_portal/post_policy.rb
316
- module AdminPortal
317
- class PostPolicy < ::PostPolicy
318
- def destroy?
319
- true # Admins can delete any post
320
- end
321
-
322
- def permitted_attributes_for_create
323
- %i[title body featured internal_notes] # More fields
324
- end
325
-
326
- relation_scope do |relation|
327
- relation # No restrictions for admins
328
- end
329
- end
330
- end
331
- ```
332
-
333
- ## Custom Authorization Context
334
-
335
- Add custom context using ActionPolicy's `authorize` directive:
336
-
337
- ```ruby
338
- # In policy
339
- class PostPolicy < Plutonium::Resource::Policy
340
- authorize :department, allow_nil: true
341
-
342
- def create?
343
- department&.allows_posting?
344
- end
345
- end
346
-
347
- # In controller
348
- class PostsController < ResourceController
349
- authorize :department, through: :current_department
350
-
351
- private
352
-
353
- def current_department
354
- current_user.department
355
- end
356
- end
357
- ```
358
-
359
- ## Authorization Errors
360
-
361
- When authorization fails:
362
-
363
- ```ruby
364
- # Raises ActionPolicy::Unauthorized
365
- ```
366
-
367
- ### Handling Errors
368
-
369
- ```ruby
370
- # app/controllers/application_controller.rb
371
- rescue_from ActionPolicy::Unauthorized do |exception|
372
- redirect_to root_path, alert: "Not authorized"
373
- end
374
- ```
375
-
376
- ## Common Patterns
377
-
378
- ### Role-Based
379
-
380
- ```ruby
381
- def update?
382
- case user.role
383
- when 'admin' then true
384
- when 'editor' then true
385
- when 'author' then owner?
386
- else false
387
- end
388
- end
389
- ```
390
-
391
- ### Status-Based
392
-
393
- ```ruby
394
- def update?
395
- return false if record.archived?
396
- owner? || admin?
397
- end
398
- ```
399
-
400
- ### Time-Based
401
-
402
- ```ruby
403
- def update?
404
- return false if record.created_at < 24.hours.ago
405
- owner?
406
- end
407
- ```
408
-
409
- ### Hierarchical
410
-
411
- ```ruby
412
- def read?
413
- return true if admin?
414
- return true if manager_of_department?
415
- return true if owner?
416
- record.public?
417
- end
418
- ```
419
-
420
- ## Debugging
421
-
422
- ### Logging
423
-
424
- ```ruby
425
- def update?
426
- result = owner? || admin?
427
- Rails.logger.debug { "PostPolicy#update? user=#{user.id} post=#{record.id}: #{result}" }
428
- result
429
- end
430
- ```
431
-
432
- ### Console Testing
433
-
434
- ```ruby
435
- user = User.find(1)
436
- post = Post.find(1)
437
-
438
- # Use ActionPolicy's testing helpers
439
- policy = PostPolicy.new(post, user: user)
440
- policy.update?
441
- policy.permitted_attributes_for_update
442
- ```
443
-
444
- ## Best Practices
445
-
446
- 1. **Always override `create?` and `read?`** - They default to `false`
447
- 2. **Define attributes explicitly** - Auto-detection only works in development
448
- 3. **Call `super` in `relation_scope`** - Preserves entity scoping
449
- 4. **Use derived methods** - Let `update?` inherit from `create?` when appropriate
450
- 5. **Keep policies focused** - Authorization logic only, no business logic
451
- 6. **Test edge cases** - Archived records, nil associations, role combinations
452
-
453
- ## Related
454
-
455
- - [Multi-tenancy Guide](/guides/multi-tenancy)
456
- - [ActionPolicy Documentation](https://actionpolicy.evilmartians.io/)