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,219 @@
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/comments`). The `associated_with` scope is automatically available for querying.
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
+ - [Resources Concept](/concepts/resources)
219
+ - [Definition Reference](/reference/definition/)
@@ -0,0 +1,385 @@
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
+
40
+ ```ruby
41
+ def update?
42
+ user # => Current user
43
+ record # => The Post instance
44
+ entity_scope # => Organization for multi-tenant portals
45
+ end
46
+ ```
47
+
48
+ ## Action Permissions
49
+
50
+ ### Core Actions (Must Override)
51
+
52
+ These default to `false` - you must override them:
53
+
54
+ ```ruby
55
+ class PostPolicy < Plutonium::Resource::Policy
56
+ def create?
57
+ user.present?
58
+ end
59
+
60
+ def read?
61
+ true
62
+ end
63
+ end
64
+ ```
65
+
66
+ ### Derived Actions
67
+
68
+ These inherit from core actions by default:
69
+
70
+ | Method | Inherits From | Override When |
71
+ |--------|---------------|---------------|
72
+ | `update?` | `create?` | Different update rules |
73
+ | `destroy?` | `create?` | Different delete rules |
74
+ | `index?` | `read?` | Custom listing rules |
75
+ | `show?` | `read?` | Record-specific read rules |
76
+ | `new?` | `create?` | Rarely needed |
77
+ | `edit?` | `update?` | Rarely needed |
78
+ | `search?` | `index?` | Search-specific rules |
79
+
80
+ ### Example with Ownership
81
+
82
+ ```ruby
83
+ class PostPolicy < Plutonium::Resource::Policy
84
+ def create?
85
+ user.present?
86
+ end
87
+
88
+ def read?
89
+ true
90
+ end
91
+
92
+ def update?
93
+ owner? || admin?
94
+ end
95
+
96
+ def destroy?
97
+ owner? || admin?
98
+ end
99
+
100
+ private
101
+
102
+ def owner?
103
+ record.user_id == user.id
104
+ end
105
+
106
+ def admin?
107
+ user.admin?
108
+ end
109
+ end
110
+ ```
111
+
112
+ ### Custom Action Permissions
113
+
114
+ For custom actions defined in definitions:
115
+
116
+ ```ruby
117
+ def publish?
118
+ owner? && !record.published?
119
+ end
120
+
121
+ def archive?
122
+ owner? || admin?
123
+ end
124
+
125
+ def bulk_delete?
126
+ admin?
127
+ end
128
+ ```
129
+
130
+ Actions are secure by default - undefined methods return `false`.
131
+
132
+ ## Attribute Permissions
133
+
134
+ ### Core Methods (Must Override for Production)
135
+
136
+ ```ruby
137
+ # What users can see (index, show)
138
+ def permitted_attributes_for_read
139
+ %i[title body author created_at]
140
+ end
141
+
142
+ # What users can set (create, update)
143
+ def permitted_attributes_for_create
144
+ %i[title body category_id]
145
+ end
146
+ ```
147
+
148
+ ### Derived Methods
149
+
150
+ | Method | Inherits From |
151
+ |--------|---------------|
152
+ | `permitted_attributes_for_update` | `permitted_attributes_for_create` |
153
+ | `permitted_attributes_for_index` | `permitted_attributes_for_read` |
154
+ | `permitted_attributes_for_show` | `permitted_attributes_for_read` |
155
+ | `permitted_attributes_for_new` | `permitted_attributes_for_create` |
156
+ | `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
157
+
158
+ ### Conditional Attribute Access
159
+
160
+ ```ruby
161
+ def permitted_attributes_for_create
162
+ attrs = %i[title body]
163
+ attrs << :featured if user.admin?
164
+ attrs << :author_id if user.admin?
165
+ attrs
166
+ end
167
+
168
+ def permitted_attributes_for_update
169
+ case record.status
170
+ when 'draft'
171
+ %i[title body category_id]
172
+ when 'published'
173
+ %i[body] # Can only edit body once published
174
+ else
175
+ []
176
+ end
177
+ end
178
+ ```
179
+
180
+ ### Auto-Detection (Development Only)
181
+
182
+ In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly:
183
+
184
+ ```
185
+ 🚨 Resource field auto-detection: PostPolicy#permitted_attributes_for_create
186
+ Auto-detected resource fields result in security holes and will fail outside of development.
187
+ ```
188
+
189
+ ## Association Permissions
190
+
191
+ Control which associations appear in panels and forms:
192
+
193
+ ```ruby
194
+ def permitted_associations
195
+ %i[comments tags author]
196
+ end
197
+ ```
198
+
199
+ Returns an empty array by default.
200
+
201
+ ## Collection Scoping
202
+
203
+ ### relation_scope
204
+
205
+ Filter which records users can see using ActionPolicy's `relation_scope`:
206
+
207
+ ```ruby
208
+ class PostPolicy < Plutonium::Resource::Policy
209
+ relation_scope do |relation|
210
+ if user.admin?
211
+ relation
212
+ else
213
+ relation.where(published: true).or(
214
+ relation.where(user_id: user.id)
215
+ )
216
+ end
217
+ end
218
+ end
219
+ ```
220
+
221
+ ### With Entity Scoping
222
+
223
+ Call `super` to preserve automatic entity scoping for multi-tenancy:
224
+
225
+ ```ruby
226
+ relation_scope do |relation|
227
+ relation = super(relation) # Applies associated_with(entity_scope)
228
+
229
+ if user.admin?
230
+ relation
231
+ else
232
+ relation.where(published: true)
233
+ end
234
+ end
235
+ ```
236
+
237
+ The default `relation_scope` automatically applies `relation.associated_with(entity_scope)` when an entity scope is present.
238
+
239
+ ## Portal-Specific Policies
240
+
241
+ Override policies for specific portals:
242
+
243
+ ```ruby
244
+ # packages/admin_portal/app/policies/admin_portal/post_policy.rb
245
+ module AdminPortal
246
+ class PostPolicy < ::PostPolicy
247
+ def destroy?
248
+ true # Admins can delete any post
249
+ end
250
+
251
+ def permitted_attributes_for_create
252
+ %i[title body featured internal_notes] # More fields
253
+ end
254
+
255
+ relation_scope do |relation|
256
+ relation # No restrictions for admins
257
+ end
258
+ end
259
+ end
260
+ ```
261
+
262
+ ## Custom Authorization Context
263
+
264
+ Add custom context using ActionPolicy's `authorize` directive:
265
+
266
+ ```ruby
267
+ # In policy
268
+ class PostPolicy < Plutonium::Resource::Policy
269
+ authorize :department, allow_nil: true
270
+
271
+ def create?
272
+ department&.allows_posting?
273
+ end
274
+ end
275
+
276
+ # In controller
277
+ class PostsController < ResourceController
278
+ authorize :department, through: :current_department
279
+
280
+ private
281
+
282
+ def current_department
283
+ current_user.department
284
+ end
285
+ end
286
+ ```
287
+
288
+ ## Authorization Errors
289
+
290
+ When authorization fails:
291
+
292
+ ```ruby
293
+ # Raises ActionPolicy::Unauthorized
294
+ ```
295
+
296
+ ### Handling Errors
297
+
298
+ ```ruby
299
+ # app/controllers/application_controller.rb
300
+ rescue_from ActionPolicy::Unauthorized do |exception|
301
+ redirect_to root_path, alert: "Not authorized"
302
+ end
303
+ ```
304
+
305
+ ## Common Patterns
306
+
307
+ ### Role-Based
308
+
309
+ ```ruby
310
+ def update?
311
+ case user.role
312
+ when 'admin' then true
313
+ when 'editor' then true
314
+ when 'author' then owner?
315
+ else false
316
+ end
317
+ end
318
+ ```
319
+
320
+ ### Status-Based
321
+
322
+ ```ruby
323
+ def update?
324
+ return false if record.archived?
325
+ owner? || admin?
326
+ end
327
+ ```
328
+
329
+ ### Time-Based
330
+
331
+ ```ruby
332
+ def update?
333
+ return false if record.created_at < 24.hours.ago
334
+ owner?
335
+ end
336
+ ```
337
+
338
+ ### Hierarchical
339
+
340
+ ```ruby
341
+ def read?
342
+ return true if admin?
343
+ return true if manager_of_department?
344
+ return true if owner?
345
+ record.public?
346
+ end
347
+ ```
348
+
349
+ ## Debugging
350
+
351
+ ### Logging
352
+
353
+ ```ruby
354
+ def update?
355
+ result = owner? || admin?
356
+ Rails.logger.debug { "PostPolicy#update? user=#{user.id} post=#{record.id}: #{result}" }
357
+ result
358
+ end
359
+ ```
360
+
361
+ ### Console Testing
362
+
363
+ ```ruby
364
+ user = User.find(1)
365
+ post = Post.find(1)
366
+
367
+ # Use ActionPolicy's testing helpers
368
+ policy = PostPolicy.new(post, user: user)
369
+ policy.update?
370
+ policy.permitted_attributes_for_update
371
+ ```
372
+
373
+ ## Best Practices
374
+
375
+ 1. **Always override `create?` and `read?`** - They default to `false`
376
+ 2. **Define attributes explicitly** - Auto-detection only works in development
377
+ 3. **Call `super` in `relation_scope`** - Preserves entity scoping
378
+ 4. **Use derived methods** - Let `update?` inherit from `create?` when appropriate
379
+ 5. **Keep policies focused** - Authorization logic only, no business logic
380
+ 6. **Test edge cases** - Archived records, nil associations, role combinations
381
+
382
+ ## Related
383
+
384
+ - [Multi-tenancy Guide](/guides/multi-tenancy)
385
+ - [ActionPolicy Documentation](https://actionpolicy.evilmartians.io/)