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,382 @@
1
+ ---
2
+ name: interaction
3
+ description: Plutonium interactions - encapsulated business logic for custom actions
4
+ ---
5
+
6
+ # Plutonium Interactions
7
+
8
+ Interactions encapsulate business logic into reusable, testable units. They handle input validation, execution, and outcomes.
9
+
10
+ ## Basic Structure
11
+
12
+ ```ruby
13
+ # app/interactions/resource_interaction.rb (generated during install)
14
+ class ResourceInteraction < Plutonium::Resource::Interaction
15
+ end
16
+
17
+ # app/interactions/publish_post_interaction.rb
18
+ class PublishPostInteraction < ResourceInteraction
19
+ # Presentation
20
+ presents label: "Publish",
21
+ icon: Phlex::TablerIcons::Send,
22
+ description: "Make this post public"
23
+
24
+ # Attributes (inputs)
25
+ attribute :resource # The record being acted upon
26
+ attribute :publish_date, :datetime, default: -> { Time.current }
27
+
28
+ # Form inputs (what user sees)
29
+ input :publish_date, as: :datetime
30
+
31
+ # Validations
32
+ validates :publish_date, presence: true
33
+
34
+ private
35
+
36
+ def execute
37
+ resource.update!(published_at: publish_date)
38
+ succeed(resource).with_message("Post published!")
39
+ rescue ActiveRecord::RecordInvalid => e
40
+ failed(e.record.errors)
41
+ end
42
+ end
43
+ ```
44
+
45
+ ## Attributes
46
+
47
+ Define inputs using ActiveModel attributes:
48
+
49
+ ```ruby
50
+ attribute :resource # Record (for record actions)
51
+ attribute :resources # Collection (for bulk actions)
52
+ attribute :email, :string # String input
53
+ attribute :count, :integer, default: 1 # With default
54
+ attribute :active, :boolean, default: -> { true } # Callable default
55
+ attribute :tags, :array # Array
56
+ attribute :metadata, :hash # Hash
57
+ attribute :date, :datetime # DateTime
58
+ ```
59
+
60
+ ## Form Inputs
61
+
62
+ Define form fields with the `input` method (same as definitions):
63
+
64
+ ```ruby
65
+ input :email
66
+ input :role, as: :select, choices: %w[admin user]
67
+ input :content, as: :text
68
+ input :date, as: :date
69
+ ```
70
+
71
+ See `definition-fields` skill for all input types and options.
72
+
73
+ ## Presentation
74
+
75
+ Configure how the action appears in the UI:
76
+
77
+ ```ruby
78
+ presents label: "Archive Record",
79
+ icon: Phlex::TablerIcons::Archive,
80
+ description: "Move to archive for later reference"
81
+ ```
82
+
83
+ Access presentation:
84
+ ```ruby
85
+ MyInteraction.label # => "Archive Record"
86
+ MyInteraction.icon # => Phlex::TablerIcons::Archive
87
+ MyInteraction.description # => "Move to archive..."
88
+ ```
89
+
90
+ ## Execution and Outcomes
91
+
92
+ ### The execute Method
93
+
94
+ ```ruby
95
+ private
96
+
97
+ def execute
98
+ # Your business logic here
99
+ # Must return succeed() or failed()
100
+ end
101
+ ```
102
+
103
+ ### Success Outcomes
104
+
105
+ ```ruby
106
+ # Basic success
107
+ succeed(resource)
108
+
109
+ # With message
110
+ succeed(resource).with_message("Done!")
111
+ succeed(resource).with_message("Warning!", :alert)
112
+
113
+ # With redirect
114
+ succeed(resource).with_redirect_response(posts_path)
115
+
116
+ # With file download
117
+ succeed(resource).with_file_response(file_path, filename: "report.pdf")
118
+
119
+ # Chaining
120
+ succeed(resource)
121
+ .with_message("Created!")
122
+ .with_redirect_response(resource_path(resource))
123
+ ```
124
+
125
+ ### Failure Outcomes
126
+
127
+ ```ruby
128
+ # Basic failure
129
+ failed("Something went wrong")
130
+
131
+ # With ActiveModel errors
132
+ failed(resource.errors)
133
+
134
+ # With hash of errors
135
+ failed(email: "is invalid", name: "is required")
136
+ ```
137
+
138
+ ### Chaining Interactions
139
+
140
+ ```ruby
141
+ def execute
142
+ CreateUserInteraction.call(view_context:, **user_params)
143
+ .and_then { |result| SendWelcomeEmail.call(view_context:, user: result.value) }
144
+ .and_then { |result| LogActivity.call(view_context:, user: result.value) }
145
+ .with_message("User created and welcomed!")
146
+ end
147
+ ```
148
+
149
+ On failure, the chain short-circuits and returns the failure immediately.
150
+
151
+ ## Validations
152
+
153
+ Use standard ActiveModel validations:
154
+
155
+ ```ruby
156
+ validates :email, presence: true
157
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
158
+ validates :role, inclusion: { in: %w[admin user guest] }
159
+
160
+ validate :custom_validation
161
+
162
+ private
163
+
164
+ def custom_validation
165
+ if resource.archived?
166
+ errors.add(:resource, "cannot be modified when archived")
167
+ end
168
+ end
169
+ ```
170
+
171
+ Validations run automatically before `execute`. If invalid, returns `failed()` with errors.
172
+
173
+ ## Interaction Types
174
+
175
+ ### Record Actions
176
+
177
+ Act on a single record:
178
+
179
+ ```ruby
180
+ class ArchiveInteraction < Plutonium::Resource::Interaction
181
+ attribute :resource # Single record
182
+
183
+ def execute
184
+ resource.update!(archived: true)
185
+ succeed(resource)
186
+ end
187
+ end
188
+ ```
189
+
190
+ ### Resource Actions
191
+
192
+ Act at the collection/class level (no specific record):
193
+
194
+ ```ruby
195
+ class ImportInteraction < Plutonium::Resource::Interaction
196
+ # No :resource attribute
197
+ attribute :file
198
+
199
+ input :file, as: :file
200
+
201
+ def execute
202
+ records = CSV.parse(file)
203
+ Post.import(records)
204
+ succeed(records)
205
+ end
206
+ end
207
+ ```
208
+
209
+ ### Bulk Actions (Multiple Records)
210
+
211
+ Act on multiple selected records:
212
+
213
+ ```ruby
214
+ class BulkArchiveInteraction < Plutonium::Resource::Interaction
215
+ attribute :resources # Collection of records
216
+
217
+ def execute
218
+ resources.update_all(archived: true)
219
+ succeed(resources).with_message("Archived #{resources.count} records")
220
+ end
221
+ end
222
+ ```
223
+
224
+ ## Connecting to Definitions
225
+
226
+ Register interactions as actions:
227
+
228
+ ```ruby
229
+ class PostDefinition < ResourceDefinition
230
+ # Record action (shows on individual records)
231
+ action :publish, interaction: PublishPostInteraction
232
+
233
+ # Resource action (shows at collection level)
234
+ action :import, interaction: ImportInteraction
235
+
236
+ # With options
237
+ action :archive,
238
+ interaction: ArchiveInteraction,
239
+ confirmation: "Are you sure?",
240
+ category: :danger,
241
+ position: 100
242
+ end
243
+ ```
244
+
245
+ ### Action Options
246
+
247
+ | Option | Description |
248
+ |--------|-------------|
249
+ | `interaction:` | The interaction class |
250
+ | `confirmation:` | Confirmation message before execution |
251
+ | `category:` | `:primary`, `:secondary`, `:danger` |
252
+ | `position:` | Display order (lower = first) |
253
+ | `turbo_frame:` | Turbo frame target (default: `remote_modal`) |
254
+ | `icon:` | Override interaction icon |
255
+ | `label:` | Override interaction label |
256
+
257
+ ## Policy Integration
258
+
259
+ Control access with policy methods:
260
+
261
+ ```ruby
262
+ class PostPolicy < ResourcePolicy
263
+ def publish?
264
+ update? && record.draft?
265
+ end
266
+
267
+ def archive?
268
+ destroy? && !record.archived?
269
+ end
270
+
271
+ def import?
272
+ create? # Resource-level action
273
+ end
274
+ end
275
+ ```
276
+
277
+ The policy method name matches the action name with `?`.
278
+
279
+ ## Accessing Context
280
+
281
+ Inside interactions:
282
+
283
+ ```ruby
284
+ def execute
285
+ # Access current user via view_context
286
+ current_user = view_context.controller.helpers.current_user
287
+
288
+ # Access the resource
289
+ resource.update!(updated_by: current_user)
290
+
291
+ succeed(resource)
292
+ end
293
+ ```
294
+
295
+ ## Immediate vs Form Actions
296
+
297
+ Plutonium automatically determines if an action needs a form:
298
+
299
+ - **Has inputs defined** → Shows form first (GET), then executes (POST)
300
+ - **No inputs** → Executes immediately (POST with confirmation)
301
+
302
+ ```ruby
303
+ # Shows form (has inputs)
304
+ class InviteUserInteraction < Plutonium::Resource::Interaction
305
+ attribute :resource
306
+ attribute :email
307
+ input :email # This triggers form display
308
+ end
309
+
310
+ # Immediate execution (no inputs)
311
+ class ArchiveInteraction < Plutonium::Resource::Interaction
312
+ attribute :resource
313
+ # No inputs = immediate with confirmation
314
+ end
315
+ ```
316
+
317
+ ## Complete Example
318
+
319
+ ```ruby
320
+ class Company::InviteUserInteraction < Plutonium::Resource::Interaction
321
+ presents label: "Invite User",
322
+ icon: Phlex::TablerIcons::UserPlus,
323
+ description: "Send an invitation email"
324
+
325
+ attribute :resource # The company
326
+ attribute :email, :string
327
+ attribute :role, :string
328
+
329
+ input :email
330
+ input :role, as: :select, choices: -> { UserInvite.roles.keys }
331
+
332
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
333
+ validates :role, presence: true, inclusion: { in: UserInvite.roles.keys }
334
+ validate :not_already_invited
335
+
336
+ private
337
+
338
+ def execute
339
+ invite = UserInvite.create!(
340
+ company: resource,
341
+ email: email,
342
+ role: role,
343
+ invited_by: current_user
344
+ )
345
+ UserInviteMailer.invitation(invite).deliver_later
346
+
347
+ succeed(resource)
348
+ .with_message("Invitation sent to #{email}")
349
+ .with_redirect_response(resource)
350
+ rescue ActiveRecord::RecordInvalid => e
351
+ failed(e.record.errors)
352
+ end
353
+
354
+ def not_already_invited
355
+ return unless email.present?
356
+
357
+ if UserInvite.exists?(company: resource, email: email, state: :pending)
358
+ errors.add(:email, "already has a pending invitation")
359
+ end
360
+ end
361
+
362
+ def current_user
363
+ view_context.controller.helpers.current_user
364
+ end
365
+ end
366
+ ```
367
+
368
+ ## Best Practices
369
+
370
+ 1. **Keep interactions focused** - One action per interaction
371
+ 2. **Use validations** - Validate all inputs before execution
372
+ 3. **Handle errors gracefully** - Rescue exceptions and return `failed()`
373
+ 4. **Return meaningful messages** - Help users understand what happened
374
+ 5. **Use `and_then` for chains** - Compose complex workflows from simple interactions
375
+ 6. **Test independently** - Interactions are easy to unit test
376
+
377
+ ## Related Skills
378
+
379
+ - `definition-actions` - Declaring actions in definitions
380
+ - `forms` - Custom interaction form templates
381
+ - `policy` - Controlling access to actions
382
+ - `resource` - How interactions fit in the architecture
@@ -0,0 +1,267 @@
1
+ ---
2
+ name: model
3
+ description: Overview of Plutonium resource models - structure, setup, and best practices
4
+ ---
5
+
6
+ # Plutonium Resource Models
7
+
8
+ A model becomes a Plutonium resource by including `Plutonium::Resource::Record`. This provides enhanced ActiveRecord functionality for routing, labeling, field introspection, associations, and monetary handling.
9
+
10
+ ## Setup
11
+
12
+ ### Standard Setup
13
+
14
+ ```ruby
15
+ # app/models/application_record.rb
16
+ class ApplicationRecord < ActiveRecord::Base
17
+ include Plutonium::Resource::Record
18
+ primary_abstract_class
19
+ end
20
+
21
+ # app/models/resource_record.rb (optional abstract class)
22
+ class ResourceRecord < ApplicationRecord
23
+ self.abstract_class = true
24
+ end
25
+
26
+ # app/models/property.rb
27
+ class Property < ResourceRecord
28
+ # Now has access to all Plutonium features
29
+ end
30
+ ```
31
+
32
+ ### What's Included
33
+
34
+ `Plutonium::Resource::Record` includes six modules:
35
+
36
+ | Module | Purpose |
37
+ |--------|---------|
38
+ | `HasCents` | Monetary value handling (cents → decimal) |
39
+ | `Routes` | URL parameters, path customization |
40
+ | `Labeling` | Human-readable `to_label` method |
41
+ | `FieldNames` | Field introspection and categorization |
42
+ | `Associations` | SGID support for secure serialization |
43
+ | `AssociatedWith` | Entity scoping for multi-tenant apps |
44
+
45
+ ## Model Structure
46
+
47
+ Follow the template structure (comment markers indicate where to add code):
48
+
49
+ ```ruby
50
+ class Property < ResourceRecord
51
+ # add concerns above.
52
+
53
+ TYPES = {apartment: "Apartment", house: "House"}.freeze
54
+ # add constants above.
55
+
56
+ enum :state, archived: 0, active: 1
57
+ enum :property_class, residential: 0, commercial: 1
58
+ # add enums above.
59
+
60
+ has_cents :market_value_cents
61
+ # add model configurations above.
62
+
63
+ belongs_to :company
64
+ # add belongs_to associations above.
65
+
66
+ has_one :address
67
+ # add has_one associations above.
68
+
69
+ has_many :units
70
+ has_many :amenities, class_name: "PropertyAmenity"
71
+ # add has_many associations above.
72
+
73
+ has_one_attached :photo
74
+ has_many_attached :documents
75
+ # add attachments above.
76
+
77
+ scope :active, -> { where(state: :active) }
78
+ scope :by_company, ->(company) { where(company: company) }
79
+ # add scopes above.
80
+
81
+ validates :name, presence: true
82
+ validates :property_code, presence: true, uniqueness: {scope: :company_id}
83
+ # add validations above.
84
+
85
+ before_validation :generate_code, on: :create
86
+ # add callbacks above.
87
+
88
+ delegate :name, to: :company, prefix: true
89
+ # add delegations above.
90
+
91
+ has_rich_text :description
92
+ # add misc attribute macros above.
93
+
94
+ def full_address
95
+ address&.to_s
96
+ end
97
+
98
+ # add methods above. add private methods below.
99
+
100
+ private
101
+
102
+ def generate_code
103
+ self.property_code ||= SecureRandom.hex(4).upcase
104
+ end
105
+ end
106
+ ```
107
+
108
+ ### Section Order
109
+
110
+ 1. **Concerns** - `include` statements
111
+ 2. **Constants** - `TYPES = {...}.freeze`, etc.
112
+ 3. **Enums** - `enum :state, ...`
113
+ 4. **Model configurations** - `has_cents`
114
+ 5. **belongs_to associations**
115
+ 6. **has_one associations**
116
+ 7. **has_many associations**
117
+ 8. **Attachments** - `has_one_attached`, `has_many_attached`
118
+ 9. **Scopes**
119
+ 10. **Validations**
120
+ 11. **Callbacks**
121
+ 12. **Delegations**
122
+ 13. **Misc attribute macros** - `has_rich_text`, `has_secure_token`, `has_secure_password`
123
+ 14. **Methods** - Public methods above, private methods below
124
+
125
+ ## Common Patterns
126
+
127
+ ### Archiving (State-Based)
128
+
129
+ ```ruby
130
+ class Property < ResourceRecord
131
+ enum :state, archived: 0, active: 1
132
+
133
+ scope :active, -> { where(state: :active) }
134
+ scope :archived, -> { where(state: :archived) }
135
+
136
+ def archive!
137
+ update!(state: :archived)
138
+ end
139
+
140
+ def restore!
141
+ update!(state: :active)
142
+ end
143
+ end
144
+ ```
145
+
146
+ ### Multi-Tenant Scoping
147
+
148
+ ```ruby
149
+ class Property < ResourceRecord
150
+ belongs_to :company
151
+
152
+ # Compound uniqueness for multi-tenant
153
+ validates :property_code, uniqueness: {scope: :company_id}
154
+
155
+ # Custom scope for entity scoping
156
+ scope :associated_with_company, ->(company) { where(company: company) }
157
+ end
158
+ ```
159
+
160
+ ### Custom Validation
161
+
162
+ ```ruby
163
+ class Contact < ResourceRecord
164
+ validates :contact_type, presence: true
165
+
166
+ validate :ensure_contact_provided
167
+
168
+ private
169
+
170
+ def ensure_contact_provided
171
+ return unless [email, phone, website].all?(&:blank?)
172
+ errors.add(:base, "Please provide at least one contact method")
173
+ end
174
+ end
175
+ ```
176
+
177
+ ### One-to-One Relationships
178
+
179
+ ```ruby
180
+ # Parent side
181
+ class Tenant < ResourceRecord
182
+ has_one :residential_profile, class_name: "ResidentialTenantProfile"
183
+ has_one :commercial_profile, class_name: "CommercialTenantProfile"
184
+ end
185
+
186
+ # Child side (unique index on foreign key)
187
+ class ResidentialTenantProfile < ResourceRecord
188
+ belongs_to :tenant
189
+ # Migration: t.index :tenant_id, unique: true
190
+ end
191
+ ```
192
+
193
+ ### Polymorphic Associations
194
+
195
+ ```ruby
196
+ class Comment < ResourceRecord
197
+ belongs_to :commentable, polymorphic: true
198
+ end
199
+
200
+ class Post < ResourceRecord
201
+ has_many :comments, as: :commentable
202
+ end
203
+
204
+ class Photo < ResourceRecord
205
+ has_many :comments, as: :commentable
206
+ end
207
+ ```
208
+
209
+ ## Labeling
210
+
211
+ The `to_label` method provides human-readable record labels:
212
+
213
+ ```ruby
214
+ # Automatic - checks :name, then :title, then fallback
215
+ user = User.new(name: "John Doe")
216
+ user.to_label # => "John Doe"
217
+
218
+ user = User.create(id: 1)
219
+ user.to_label # => "User #1"
220
+
221
+ # Custom override
222
+ class Product < ResourceRecord
223
+ def to_label
224
+ "#{name} (#{sku})"
225
+ end
226
+ end
227
+ ```
228
+
229
+ ## Field Introspection
230
+
231
+ Access field information programmatically:
232
+
233
+ ```ruby
234
+ # All resource fields
235
+ User.resource_field_names
236
+ # => [:id, :name, :email, :company, :avatar, ...]
237
+
238
+ # By category
239
+ User.content_column_field_names # Database columns
240
+ User.belongs_to_association_field_names # belongs_to associations
241
+ User.has_one_association_field_names # has_one associations
242
+ User.has_many_association_field_names # has_many associations
243
+ User.has_one_attached_field_names # Active Storage single
244
+ User.has_many_attached_field_names # Active Storage multiple
245
+ ```
246
+
247
+ ## Best Practices
248
+
249
+ 1. **Use enums for state** - `enum :state, archived: 0, active: 1` instead of soft-delete
250
+ 2. **Compound uniqueness** - Always scope uniqueness to tenant/parent
251
+ 3. **Organize with comments** - Use section headers for readability
252
+ 4. **Keep models focused** - Business logic in interactions, not models
253
+ 5. **Validate at boundaries** - Validate user input, trust internal code
254
+ 6. **Use scopes** - Define commonly used queries as scopes
255
+
256
+ ## Integration
257
+
258
+ Models integrate with:
259
+ - **Policies** - `resource_field_names` for auto-detection
260
+ - **Definitions** - Field introspection for forms/displays
261
+ - **Controllers** - `from_path_param` for lookups
262
+ - **Query Objects** - Association detection for sorting
263
+
264
+ ## Related Skills
265
+
266
+ - `model-features` - has_cents, associations, scopes, routes
267
+ - `create-resource` - Scaffold generator for new resources