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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +230 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +56 -49
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +117 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +11 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plutonium-model
|
|
3
|
-
description: Use BEFORE editing a Plutonium resource model, adding associations, has_cents, SGID, or routing helpers. For tenancy / associated_with / relation_scope, also load plutonium-entity-scoping.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Plutonium Resource Models
|
|
7
|
-
|
|
8
|
-
## 🚨 Critical (read first)
|
|
9
|
-
- **Use `pu:res:scaffold`.** Never hand-write resource model files — the scaffold sets up `Plutonium::Resource::Record`, associations, and the expected section layout.
|
|
10
|
-
- **Declare associations for the entity.** For multi-tenant apps, add `belongs_to`, `has_one :through`, or an `associated_with_<entity>` scope so `associated_with` can resolve. Fix the model, not the policy.
|
|
11
|
-
- **Compound uniqueness** — in multi-tenant models, scope unique constraints to the tenant FK (`uniqueness: {scope: :organization_id}`), or you leak across tenants.
|
|
12
|
-
- **Keep business logic out of the model.** Use interactions for multi-step ops, policies for authorization.
|
|
13
|
-
- **Related skills:** `plutonium-entity-scoping` (tenancy mechanics), `plutonium-create-resource` (scaffold), `plutonium-definition` (UI), `plutonium-policy` (authorization).
|
|
14
|
-
|
|
15
|
-
## Quick checklist
|
|
16
|
-
|
|
17
|
-
Adding/editing a Plutonium model:
|
|
18
|
-
|
|
19
|
-
1. Use `pu:res:scaffold` for new models; include `Plutonium::Resource::Record` on existing ones.
|
|
20
|
-
2. Place associations/enums/validations in the right section (enums → belongs_to → has_one → has_many → scopes → validations → callbacks).
|
|
21
|
-
3. For monetary fields, use `has_cents :field_cents`.
|
|
22
|
-
4. For multi-tenancy, declare an association path to the entity (`belongs_to`, `has_one :through`, or a custom `associated_with_<entity>` scope).
|
|
23
|
-
5. Add compound uniqueness scoped to the tenant FK.
|
|
24
|
-
6. For SEO URLs, override `path_parameter` or `dynamic_path_parameter`.
|
|
25
|
-
7. Override `to_label` if `:name`/`:title` isn't meaningful.
|
|
26
|
-
8. Verify with `rails runner "puts Model.first.associated_with(entity).count"`.
|
|
27
|
-
|
|
28
|
-
**Always use generators to create models** - never create model files manually:
|
|
29
|
-
```bash
|
|
30
|
-
rails g pu:res:scaffold Post title:string content:text --dest=main_app
|
|
31
|
-
```
|
|
32
|
-
See `plutonium-create-resource` for full field type syntax and generator options.
|
|
33
|
-
|
|
34
|
-
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.
|
|
35
|
-
|
|
36
|
-
## Setup
|
|
37
|
-
|
|
38
|
-
### Standard Setup
|
|
39
|
-
|
|
40
|
-
```ruby
|
|
41
|
-
# app/models/application_record.rb
|
|
42
|
-
class ApplicationRecord < ActiveRecord::Base
|
|
43
|
-
include Plutonium::Resource::Record
|
|
44
|
-
primary_abstract_class
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# app/models/resource_record.rb (optional abstract class)
|
|
48
|
-
class ResourceRecord < ApplicationRecord
|
|
49
|
-
self.abstract_class = true
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# app/models/property.rb
|
|
53
|
-
class Property < ResourceRecord
|
|
54
|
-
# Now has access to all Plutonium features
|
|
55
|
-
end
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### What's Included
|
|
59
|
-
|
|
60
|
-
`Plutonium::Resource::Record` includes six modules:
|
|
61
|
-
|
|
62
|
-
| Module | Purpose |
|
|
63
|
-
|--------|---------|
|
|
64
|
-
| `HasCents` | Monetary value handling (cents → decimal) |
|
|
65
|
-
| `Routes` | URL parameters, path customization |
|
|
66
|
-
| `Labeling` | Human-readable `to_label` method |
|
|
67
|
-
| `FieldNames` | Field introspection and categorization |
|
|
68
|
-
| `Associations` | SGID support for secure serialization |
|
|
69
|
-
| `AssociatedWith` | Entity scoping for multi-tenant apps |
|
|
70
|
-
|
|
71
|
-
## Model Structure
|
|
72
|
-
|
|
73
|
-
Follow the template structure (comment markers indicate where to add code):
|
|
74
|
-
|
|
75
|
-
```ruby
|
|
76
|
-
class Property < ResourceRecord
|
|
77
|
-
# add concerns above.
|
|
78
|
-
|
|
79
|
-
TYPES = {apartment: "Apartment", house: "House"}.freeze
|
|
80
|
-
# add constants above.
|
|
81
|
-
|
|
82
|
-
enum :state, archived: 0, active: 1
|
|
83
|
-
enum :property_class, residential: 0, commercial: 1
|
|
84
|
-
# add enums above.
|
|
85
|
-
|
|
86
|
-
has_cents :market_value_cents
|
|
87
|
-
# add model configurations above.
|
|
88
|
-
|
|
89
|
-
belongs_to :company
|
|
90
|
-
# add belongs_to associations above.
|
|
91
|
-
|
|
92
|
-
has_one :address
|
|
93
|
-
# add has_one associations above.
|
|
94
|
-
|
|
95
|
-
has_many :units
|
|
96
|
-
has_many :amenities, class_name: "PropertyAmenity"
|
|
97
|
-
# add has_many associations above.
|
|
98
|
-
|
|
99
|
-
has_one_attached :photo
|
|
100
|
-
has_many_attached :documents
|
|
101
|
-
# add attachments above.
|
|
102
|
-
|
|
103
|
-
scope :active, -> { where(state: :active) }
|
|
104
|
-
scope :by_company, ->(company) { where(company: company) }
|
|
105
|
-
# add scopes above.
|
|
106
|
-
|
|
107
|
-
validates :name, presence: true
|
|
108
|
-
validates :property_code, presence: true, uniqueness: {scope: :company_id}
|
|
109
|
-
# add validations above.
|
|
110
|
-
|
|
111
|
-
before_validation :generate_code, on: :create
|
|
112
|
-
# add callbacks above.
|
|
113
|
-
|
|
114
|
-
delegate :name, to: :company, prefix: true
|
|
115
|
-
# add delegations above.
|
|
116
|
-
|
|
117
|
-
has_rich_text :description
|
|
118
|
-
# add misc attribute macros above.
|
|
119
|
-
|
|
120
|
-
def full_address
|
|
121
|
-
address&.to_s
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# add methods above. add private methods below.
|
|
125
|
-
|
|
126
|
-
private
|
|
127
|
-
|
|
128
|
-
def generate_code
|
|
129
|
-
self.property_code ||= SecureRandom.hex(4).upcase
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### Section Order
|
|
135
|
-
|
|
136
|
-
1. **Concerns** - `include` statements
|
|
137
|
-
2. **Constants** - `TYPES = {...}.freeze`, etc.
|
|
138
|
-
3. **Enums** - `enum :state, ...`
|
|
139
|
-
4. **Model configurations** - `has_cents`
|
|
140
|
-
5. **belongs_to associations**
|
|
141
|
-
6. **has_one associations**
|
|
142
|
-
7. **has_many associations**
|
|
143
|
-
8. **Attachments** - `has_one_attached`, `has_many_attached`
|
|
144
|
-
9. **Scopes**
|
|
145
|
-
10. **Validations**
|
|
146
|
-
11. **Callbacks**
|
|
147
|
-
12. **Delegations**
|
|
148
|
-
13. **Misc attribute macros** - `has_rich_text`, `has_secure_token`, `has_secure_password`
|
|
149
|
-
14. **Methods** - Public methods above, private methods below
|
|
150
|
-
|
|
151
|
-
## Monetary Handling (has_cents)
|
|
152
|
-
|
|
153
|
-
Store monetary values as integers (cents) while exposing decimal interfaces.
|
|
154
|
-
|
|
155
|
-
### Basic Usage
|
|
156
|
-
|
|
157
|
-
```ruby
|
|
158
|
-
class Product < ResourceRecord
|
|
159
|
-
has_cents :price_cents # Creates price getter/setter
|
|
160
|
-
has_cents :cost_cents, name: :wholesale # Custom accessor name
|
|
161
|
-
has_cents :tax_cents, rate: 1000 # 3 decimal places
|
|
162
|
-
has_cents :quantity_cents, rate: 1 # Whole numbers only
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
product = Product.new
|
|
166
|
-
product.price = 19.99
|
|
167
|
-
product.price_cents # => 1999
|
|
168
|
-
product.price # => 19.99
|
|
169
|
-
|
|
170
|
-
# Truncates (doesn't round)
|
|
171
|
-
product.price = 10.999
|
|
172
|
-
product.price_cents # => 1099
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Options
|
|
176
|
-
|
|
177
|
-
```ruby
|
|
178
|
-
has_cents :field_cents,
|
|
179
|
-
name: :custom_name, # Accessor name (default: field without _cents)
|
|
180
|
-
rate: 100, # Conversion rate (default: 100)
|
|
181
|
-
suffix: "amount" # Suffix for generated name (default: "amount")
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Using `has_cents` fields in policies and definitions
|
|
185
|
-
|
|
186
|
-
**Always reference the virtual accessor (`:price`), never the underlying column (`:price_cents`).**
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
# Model
|
|
190
|
-
class Product < ResourceRecord
|
|
191
|
-
has_cents :price_cents # exposes virtual :price
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# ✅ Policy — use the virtual name
|
|
195
|
-
class ProductPolicy < ResourcePolicy
|
|
196
|
-
def permitted_attributes_for_create
|
|
197
|
-
%i[name price] # NOT :price_cents
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# ✅ Definition — use the virtual name
|
|
202
|
-
class ProductDefinition < ResourceDefinition
|
|
203
|
-
field :price, as: :decimal
|
|
204
|
-
end
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
The virtual accessor handles form input, validation, and display as a decimal. Using `:price_cents` directly in a policy or definition forces users to enter integer cents and bypasses the conversion. Generators sometimes emit the `_cents` name in the policy — fix it by hand if you see it (and add `has_cents` if it's missing from the model).
|
|
208
|
-
|
|
209
|
-
### Validation
|
|
210
|
-
|
|
211
|
-
```ruby
|
|
212
|
-
class Product < ResourceRecord
|
|
213
|
-
has_cents :price_cents
|
|
214
|
-
|
|
215
|
-
# Validate the cents field
|
|
216
|
-
validates :price_cents, numericality: {greater_than: 0}
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
product = Product.new(price: -10)
|
|
220
|
-
product.valid? # => false
|
|
221
|
-
product.errors[:price_cents] # => ["must be greater than 0"]
|
|
222
|
-
product.errors[:price] # => ["is invalid"] (propagated)
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Introspection
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
Product.has_cents_attributes
|
|
229
|
-
# => {price_cents: {name: :price, rate: 100}, ...}
|
|
230
|
-
|
|
231
|
-
Product.has_cents_attribute?(:price_cents) # => true
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Association SGID Support
|
|
235
|
-
|
|
236
|
-
All associations get Signed Global ID (SGID) methods for secure serialization.
|
|
237
|
-
|
|
238
|
-
### Singular Associations (belongs_to, has_one)
|
|
239
|
-
|
|
240
|
-
```ruby
|
|
241
|
-
class Post < ResourceRecord
|
|
242
|
-
belongs_to :user
|
|
243
|
-
has_one :featured_image
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
post = Post.first
|
|
247
|
-
|
|
248
|
-
# Get SGID
|
|
249
|
-
post.user_sgid # => "BAh7CEkiCG..."
|
|
250
|
-
post.featured_image_sgid # => "BAh7CEkiCG..."
|
|
251
|
-
|
|
252
|
-
# Set by SGID (finds and assigns)
|
|
253
|
-
post.user_sgid = "BAh7CEkiCG..."
|
|
254
|
-
post.featured_image_sgid = "BAh7CEkiCG..."
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### Collection Associations (has_many, has_and_belongs_to_many)
|
|
258
|
-
|
|
259
|
-
```ruby
|
|
260
|
-
class User < ResourceRecord
|
|
261
|
-
has_many :posts
|
|
262
|
-
has_and_belongs_to_many :roles
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
user = User.first
|
|
266
|
-
|
|
267
|
-
# Get SGIDs
|
|
268
|
-
user.post_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
|
|
269
|
-
user.role_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
|
|
270
|
-
|
|
271
|
-
# Bulk assignment
|
|
272
|
-
user.post_sgids = ["BAh7CEkiCG...", ...]
|
|
273
|
-
|
|
274
|
-
# Individual manipulation
|
|
275
|
-
user.add_post_sgid("BAh7CEkiCG...") # Add to collection
|
|
276
|
-
user.remove_post_sgid("BAh7CEkiCG...") # Remove from collection
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
## Entity Scoping (associated_with)
|
|
280
|
-
|
|
281
|
-
`Plutonium::Resource::Record` provides `Model.associated_with(entity)` for multi-tenant queries. It resolves via a custom `associated_with_<entity>` scope, a direct `belongs_to`, or an auto-detected `has_one :through` chain.
|
|
282
|
-
|
|
283
|
-
Quick example:
|
|
284
|
-
|
|
285
|
-
```ruby
|
|
286
|
-
class Comment < ResourceRecord
|
|
287
|
-
belongs_to :post
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
Comment.associated_with(post) # => Comment.where(post: post)
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
> **For entity scoping details — the three model shapes (direct child, join table, grandchild), `has_one :through` patterns, custom scopes, `default_relation_scope`, and how it fits with policies and portals — see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth.**
|
|
294
|
-
|
|
295
|
-
## URL Routing
|
|
296
|
-
|
|
297
|
-
### Default Behavior
|
|
298
|
-
|
|
299
|
-
```ruby
|
|
300
|
-
user = User.find(1)
|
|
301
|
-
user.to_param # => "1"
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Custom Path Parameters
|
|
305
|
-
|
|
306
|
-
Use a stable, unique field instead of ID:
|
|
307
|
-
|
|
308
|
-
```ruby
|
|
309
|
-
class User < ResourceRecord
|
|
310
|
-
private
|
|
311
|
-
|
|
312
|
-
def path_parameter(param_name)
|
|
313
|
-
:username # Must be unique
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
user = User.create(username: "john_doe")
|
|
318
|
-
user.to_param # => "john_doe"
|
|
319
|
-
# URLs: /users/john_doe
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Dynamic Path Parameters (SEO-friendly)
|
|
323
|
-
|
|
324
|
-
Include ID prefix for uniqueness with human-readable suffix:
|
|
325
|
-
|
|
326
|
-
```ruby
|
|
327
|
-
class Article < ResourceRecord
|
|
328
|
-
private
|
|
329
|
-
|
|
330
|
-
def dynamic_path_parameter(param_name)
|
|
331
|
-
:title
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
article = Article.create(id: 1, title: "My Great Article")
|
|
336
|
-
article.to_param # => "1-my-great-article"
|
|
337
|
-
# URLs: /articles/1-my-great-article
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
### Path Parameter Lookup
|
|
341
|
-
|
|
342
|
-
```ruby
|
|
343
|
-
User.from_path_param("john_doe")
|
|
344
|
-
Article.from_path_param("1-my-great-article") # Extracts ID
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
## Labeling
|
|
348
|
-
|
|
349
|
-
The `to_label` method provides human-readable record labels:
|
|
350
|
-
|
|
351
|
-
```ruby
|
|
352
|
-
# Automatic - checks :name, then :title, then fallback
|
|
353
|
-
user = User.new(name: "John Doe")
|
|
354
|
-
user.to_label # => "John Doe"
|
|
355
|
-
|
|
356
|
-
user = User.create(id: 1)
|
|
357
|
-
user.to_label # => "User #1"
|
|
358
|
-
|
|
359
|
-
# Custom override
|
|
360
|
-
class Product < ResourceRecord
|
|
361
|
-
def to_label
|
|
362
|
-
"#{name} (#{sku})"
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
## Field Introspection
|
|
368
|
-
|
|
369
|
-
Access field information programmatically:
|
|
370
|
-
|
|
371
|
-
```ruby
|
|
372
|
-
# All resource fields
|
|
373
|
-
User.resource_field_names
|
|
374
|
-
# => [:id, :name, :email, :company, :avatar, ...]
|
|
375
|
-
|
|
376
|
-
# By category
|
|
377
|
-
User.content_column_field_names # Database columns
|
|
378
|
-
User.belongs_to_association_field_names # belongs_to associations
|
|
379
|
-
User.has_one_association_field_names # has_one associations
|
|
380
|
-
User.has_many_association_field_names # has_many associations
|
|
381
|
-
User.has_one_attached_field_names # Active Storage single
|
|
382
|
-
User.has_many_attached_field_names # Active Storage multiple
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
## Common Patterns
|
|
386
|
-
|
|
387
|
-
### Archiving (State-Based)
|
|
388
|
-
|
|
389
|
-
```ruby
|
|
390
|
-
class Property < ResourceRecord
|
|
391
|
-
enum :state, archived: 0, active: 1
|
|
392
|
-
|
|
393
|
-
scope :active, -> { where(state: :active) }
|
|
394
|
-
scope :archived, -> { where(state: :archived) }
|
|
395
|
-
end
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
### Multi-Tenant Scoping
|
|
399
|
-
|
|
400
|
-
```ruby
|
|
401
|
-
class Property < ResourceRecord
|
|
402
|
-
belongs_to :company
|
|
403
|
-
|
|
404
|
-
# Compound uniqueness for multi-tenant
|
|
405
|
-
validates :property_code, uniqueness: {scope: :company_id}
|
|
406
|
-
|
|
407
|
-
# Custom scope for entity scoping
|
|
408
|
-
scope :associated_with_company, ->(company) { where(company: company) }
|
|
409
|
-
end
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
## Performance Tips
|
|
413
|
-
|
|
414
|
-
```ruby
|
|
415
|
-
# Efficient: Direct belongs_to
|
|
416
|
-
Comment.associated_with(post) # Simple WHERE
|
|
417
|
-
|
|
418
|
-
# Less efficient: Reverse has_many (logs warning)
|
|
419
|
-
Post.associated_with(comment) # JOIN required
|
|
420
|
-
|
|
421
|
-
# Optimal: Custom scope when direct isn't possible
|
|
422
|
-
scope :associated_with_user, ->(user) { where(user_id: user.id) }
|
|
423
|
-
|
|
424
|
-
# SGID: Batch assignment over individual adds
|
|
425
|
-
user.post_sgids = sgid_array # Single operation
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
## Best Practices
|
|
429
|
-
|
|
430
|
-
1. **Use enums for state** - `enum :state, archived: 0, active: 1` instead of soft-delete
|
|
431
|
-
2. **Compound uniqueness** - Always scope uniqueness to tenant/parent
|
|
432
|
-
3. **Organize with comments** - Use section headers for readability
|
|
433
|
-
4. **Keep models focused** - Business logic in interactions, not models
|
|
434
|
-
5. **Validate at boundaries** - Validate user input, trust internal code
|
|
435
|
-
6. **Use scopes** - Define commonly used queries as scopes
|
|
436
|
-
|
|
437
|
-
## Related Skills
|
|
438
|
-
|
|
439
|
-
- `plutonium-create-resource` - Scaffold generator for new resources
|
|
440
|
-
- `plutonium-definition` - Definition overview, fields, inputs, displays
|