plutonium 0.23.4 → 0.23.5

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/plutonium.css +2 -2
  3. data/config/initializers/sqlite_json_alias.rb +1 -1
  4. data/docs/.vitepress/config.ts +60 -19
  5. data/docs/guide/cursor-rules.md +75 -0
  6. data/docs/guide/deep-dive/authorization.md +189 -0
  7. data/docs/guide/{getting-started → deep-dive}/resources.md +137 -0
  8. data/docs/guide/getting-started/{installation.md → 01-installation.md} +0 -105
  9. data/docs/guide/index.md +28 -0
  10. data/docs/guide/introduction/02-core-concepts.md +440 -0
  11. data/docs/guide/tutorial/01-project-setup.md +75 -0
  12. data/docs/guide/tutorial/02-creating-a-feature-package.md +45 -0
  13. data/docs/guide/tutorial/03-defining-resources.md +90 -0
  14. data/docs/guide/tutorial/04-creating-a-portal.md +101 -0
  15. data/docs/guide/tutorial/05-customizing-the-ui.md +128 -0
  16. data/docs/guide/tutorial/06-adding-custom-actions.md +101 -0
  17. data/docs/guide/tutorial/07-implementing-authorization.md +90 -0
  18. data/docs/index.md +24 -31
  19. data/docs/modules/action.md +190 -0
  20. data/docs/modules/authentication.md +236 -0
  21. data/docs/modules/configuration.md +599 -0
  22. data/docs/modules/controller.md +398 -0
  23. data/docs/modules/core.md +316 -0
  24. data/docs/modules/definition.md +876 -0
  25. data/docs/modules/display.md +759 -0
  26. data/docs/modules/form.md +605 -0
  27. data/docs/modules/generator.md +288 -0
  28. data/docs/modules/index.md +167 -0
  29. data/docs/modules/interaction.md +470 -0
  30. data/docs/modules/package.md +151 -0
  31. data/docs/modules/policy.md +176 -0
  32. data/docs/modules/portal.md +710 -0
  33. data/docs/modules/query.md +287 -0
  34. data/docs/modules/resource_record.md +618 -0
  35. data/docs/modules/routing.md +641 -0
  36. data/docs/modules/table.md +293 -0
  37. data/docs/modules/ui.md +631 -0
  38. data/docs/public/plutonium.mdc +667 -0
  39. data/lib/generators/pu/core/assets/assets_generator.rb +0 -5
  40. data/lib/plutonium/ui/display/resource.rb +7 -2
  41. data/lib/plutonium/ui/table/resource.rb +8 -3
  42. data/lib/plutonium/version.rb +1 -1
  43. metadata +36 -9
  44. data/docs/guide/getting-started/authorization.md +0 -296
  45. data/docs/guide/getting-started/core-concepts.md +0 -432
  46. data/docs/guide/getting-started/index.md +0 -21
  47. data/docs/guide/tutorial.md +0 -401
  48. /data/docs/guide/{what-is-plutonium.md → introduction/01-what-is-plutonium.md} +0 -0
@@ -0,0 +1,618 @@
1
+ ---
2
+ title: Resource Record Module
3
+ ---
4
+
5
+ # Resource Record Module
6
+
7
+ The Resource Record module (`Plutonium::Resource::Record`) provides enhanced ActiveRecord functionality specifically designed for Plutonium resources. It includes monetary handling, routing enhancements, labeling, field introspection, association management, and entity scoping capabilities.
8
+
9
+ ::: tip Usage
10
+ Include this module in your ActiveRecord models to gain access to all Plutonium resource functionality:
11
+
12
+ ```ruby
13
+ class Product < ApplicationRecord
14
+ include Plutonium::Resource::Record
15
+ end
16
+ ```
17
+
18
+ Or inherit from your base resource record class:
19
+
20
+ ```ruby
21
+ class Product < MyApp::ResourceRecord
22
+ # MyApp::ResourceRecord includes Plutonium::Resource::Record
23
+ end
24
+ ```
25
+ :::
26
+
27
+ ## Included Modules
28
+
29
+ The Resource Record module automatically includes six specialized modules:
30
+
31
+ 1. **Plutonium::Models::HasCents** - Monetary value handling
32
+ 2. **Plutonium::Resource::Record::Routes** - URL parameter and routing enhancements
33
+ 3. **Plutonium::Resource::Record::Labeling** - Human-readable record labels
34
+ 4. **Plutonium::Resource::Record::FieldNames** - Field introspection and categorization
35
+ 5. **Plutonium::Resource::Record::Associations** - Enhanced association methods with SGID support
36
+ 6. **Plutonium::Resource::Record::AssociatedWith** - Entity scoping and association queries
37
+
38
+ ---
39
+
40
+ ## Monetary Handling (HasCents)
41
+
42
+ The `HasCents` module provides sophisticated monetary value handling, storing amounts as integers (cents) while exposing decimal interfaces for easy manipulation.
43
+
44
+ ### Basic Usage
45
+
46
+ ```ruby
47
+ class Product < ApplicationRecord
48
+ include Plutonium::Resource::Record
49
+
50
+ # Define monetary fields
51
+ has_cents :price_cents # Creates price getter/setter
52
+ has_cents :cost_cents, name: :wholesale # Custom name
53
+ has_cents :tax_cents, rate: 1000 # Custom rate (1000 = 3 decimal places)
54
+ has_cents :total_cents, suffix: "amount" # Custom suffix
55
+ end
56
+
57
+ # Usage
58
+ product = Product.new
59
+ product.price = 19.99
60
+ product.price_cents # => 1999
61
+ product.price # => 19.99
62
+
63
+ product.wholesale = 12.50
64
+ product.cost_cents # => 1250
65
+ ```
66
+
67
+ ### Advanced Features
68
+
69
+ **Precision and Truncation**
70
+ ```ruby
71
+ product.price = 10.999 # Truncates, doesn't round
72
+ product.price_cents # => 1099
73
+ product.price # => 10.99
74
+ ```
75
+
76
+ **Custom Conversion Rates**
77
+ ```ruby
78
+ class Product < ApplicationRecord
79
+ has_cents :weight_cents, name: :weight, rate: 1000 # 3 decimal places
80
+ has_cents :quantity_cents, name: :quantity, rate: 1 # Whole numbers only
81
+ end
82
+
83
+ product.weight = 1.234
84
+ product.weight_cents # => 1234
85
+
86
+ product.quantity = 5
87
+ product.quantity_cents # => 5
88
+ ```
89
+
90
+ **Validation Integration**
91
+ ```ruby
92
+ class Product < ApplicationRecord
93
+ has_cents :price_cents
94
+
95
+ validates :price_cents, numericality: { greater_than: 0 }
96
+ end
97
+
98
+ product = Product.new(price: -10)
99
+ product.valid? # => false
100
+ product.errors[:price_cents] # => ["must be greater than 0"]
101
+ product.errors[:price] # => ["is invalid"]
102
+ ```
103
+
104
+ ### Class Methods
105
+
106
+ **Introspection**
107
+ ```ruby
108
+ Product.has_cents_attributes
109
+ # => {
110
+ # price_cents: { name: :price, rate: 100 },
111
+ # cost_cents: { name: :wholesale, rate: 100 }
112
+ # }
113
+
114
+ Product.has_cents_attribute?(:price_cents) # => true
115
+ Product.has_cents_attribute?(:name) # => false
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Routing Enhancements
121
+
122
+ The Routes module provides flexible URL parameter handling and association route discovery.
123
+
124
+ ### URL Parameters
125
+
126
+ **Default Behavior**
127
+ ```ruby
128
+ # Uses :id by default
129
+ user = User.find(1)
130
+ user.to_param # => "1"
131
+ ```
132
+
133
+ **Custom Path Parameters**
134
+ ```ruby
135
+ class User < ApplicationRecord
136
+ include Plutonium::Resource::Record
137
+
138
+ private
139
+
140
+ def path_parameter(param_name)
141
+ # Uses specified field as URL parameter
142
+ path_parameter :username
143
+ end
144
+ end
145
+
146
+ user = User.create(username: "john_doe")
147
+ user.to_param # => "john_doe"
148
+ # URLs become /users/john_doe instead of /users/1
149
+ ```
150
+
151
+ **Dynamic Path Parameters**
152
+ ```ruby
153
+ class Article < ApplicationRecord
154
+ include Plutonium::Resource::Record
155
+
156
+ private
157
+
158
+ def dynamic_path_parameter(param_name)
159
+ # Creates SEO-friendly URLs with ID prefix
160
+ dynamic_path_parameter :title
161
+ end
162
+ end
163
+
164
+ article = Article.create(title: "My Great Article")
165
+ article.to_param # => "1-my-great-article"
166
+ # URLs become /articles/1-my-great-article
167
+ ```
168
+
169
+ ### Association Route Discovery
170
+
171
+ **Has Many Routes**
172
+ ```ruby
173
+ class User < ApplicationRecord
174
+ has_many :posts
175
+ has_many :comments
176
+ end
177
+
178
+ User.has_many_association_routes
179
+ # => ["posts", "comments"]
180
+ ```
181
+
182
+ **Nested Attributes Detection**
183
+ ```ruby
184
+ class User < ApplicationRecord
185
+ has_many :posts
186
+ accepts_nested_attributes_for :posts
187
+ end
188
+
189
+ User.all_nested_attributes_options
190
+ # => {
191
+ # posts: {
192
+ # allow_destroy: false,
193
+ # update_only: false,
194
+ # macro: :has_many,
195
+ # class: Post
196
+ # }
197
+ # }
198
+ ```
199
+
200
+ ### Scopes
201
+
202
+ **Path Parameter Lookup**
203
+ ```ruby
204
+ # Automatically included scope for parameter-based lookups
205
+ User.from_path_param("john_doe") # Uses configured parameter field
206
+ Article.from_path_param("1-my-great-article") # Extracts ID from dynamic parameter
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Record Labeling
212
+
213
+ The Labeling module provides intelligent human-readable labels for records.
214
+
215
+ ### Automatic Label Generation
216
+
217
+ ```ruby
218
+ class User < ApplicationRecord
219
+ include Plutonium::Resource::Record
220
+
221
+ # Will try :name first, then :title, then fallback
222
+ end
223
+
224
+ user = User.new(name: "John Doe")
225
+ user.to_label # => "John Doe"
226
+
227
+ user_without_name = User.create(id: 1)
228
+ user_without_name.to_label # => "User #1"
229
+ ```
230
+
231
+ ### Label Priority
232
+
233
+ The `to_label` method checks fields in this order:
234
+ 1. `:name` attribute (if present and not blank)
235
+ 2. `:title` attribute (if present and not blank)
236
+ 3. Fallback: `"#{model_name.human} ##{to_param}"`
237
+
238
+ ### Custom Labels
239
+
240
+ ```ruby
241
+ class Product < ApplicationRecord
242
+ include Plutonium::Resource::Record
243
+
244
+ # Override to_label for custom behavior
245
+ def to_label
246
+ "#{name} (#{sku})"
247
+ end
248
+ end
249
+
250
+ product = Product.new(name: "Widget", sku: "W123")
251
+ product.to_label # => "Widget (W123)"
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Field Introspection
257
+
258
+ The FieldNames module provides comprehensive field categorization and introspection capabilities.
259
+
260
+ ### Field Categories
261
+
262
+ **Resource Fields**
263
+ ```ruby
264
+ class User < ApplicationRecord
265
+ include Plutonium::Resource::Record
266
+ end
267
+
268
+ User.resource_field_names
269
+ # => [:id, :name, :email, :created_at, :updated_at, ...]
270
+ ```
271
+
272
+ **Association Fields**
273
+ ```ruby
274
+ class Post < ApplicationRecord
275
+ belongs_to :user
276
+ has_many :comments
277
+ has_one :featured_image
278
+ end
279
+
280
+ Post.belongs_to_association_field_names # => [:user]
281
+ Post.has_one_association_field_names # => [:featured_image]
282
+ Post.has_many_association_field_names # => [:comments]
283
+ ```
284
+
285
+ **Attachment Fields**
286
+ ```ruby
287
+ class User < ApplicationRecord
288
+ has_one_attached :avatar
289
+ has_many_attached :documents
290
+ end
291
+
292
+ User.has_one_attached_field_names # => [:avatar]
293
+ User.has_many_attached_field_names # => [:documents]
294
+ ```
295
+
296
+ ### Field Filtering
297
+
298
+ The module automatically filters out Rails internal associations:
299
+ - `*_attachment` and `*_blob` associations are excluded from has_one results
300
+ - `*_attachments` and `*_blobs` associations are excluded from has_many results
301
+
302
+ ---
303
+
304
+ ## Enhanced Associations
305
+
306
+ The Associations module enhances standard Rails associations with Signed Global ID (SGID) support for secure serialization.
307
+
308
+ ### SGID Methods
309
+
310
+ **Singular Associations (belongs_to, has_one)**
311
+ ```ruby
312
+ class Post < ApplicationRecord
313
+ include Plutonium::Resource::Record
314
+ belongs_to :user
315
+ has_one :featured_image
316
+ end
317
+
318
+ post = Post.first
319
+
320
+ # SGID getters
321
+ post.user_sgid # => "BAh7CEkiCG..."
322
+ post.featured_image_sgid # => "BAh7CEkiCG..."
323
+
324
+ # SGID setters
325
+ post.user_sgid = "BAh7CEkiCG..." # Finds and assigns user
326
+ post.featured_image_sgid = "BAh7CEkiCG..."
327
+ ```
328
+
329
+ **Collection Associations (has_many, has_and_belongs_to_many)**
330
+ ```ruby
331
+ class User < ApplicationRecord
332
+ include Plutonium::Resource::Record
333
+ has_many :posts
334
+ has_and_belongs_to_many :roles
335
+ end
336
+
337
+ user = User.first
338
+
339
+ # Collection SGID methods
340
+ user.post_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
341
+ user.role_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
342
+
343
+ # Collection SGID assignment
344
+ user.post_sgids = ["BAh7CEkiCG...", "BAh7CEkiCG..."]
345
+ user.role_sgids = ["BAh7CEkiCG...", "BAh7CEkiCG..."]
346
+
347
+ # Individual manipulation
348
+ user.add_post_sgid("BAh7CEkiCG...") # Adds post to collection
349
+ user.remove_post_sgid("BAh7CEkiCG...") # Removes post from collection
350
+ ```
351
+
352
+ ### Security Benefits
353
+
354
+ SGID methods provide:
355
+ - **Secure serialization**: Records can be safely serialized without exposing internal IDs
356
+
357
+ ---
358
+
359
+ ## Entity Scoping (AssociatedWith)
360
+
361
+ The AssociatedWith module provides sophisticated entity scoping for multi-tenant applications and complex association queries.
362
+
363
+ ### Basic Usage
364
+
365
+ ```ruby
366
+ class Document < ApplicationRecord
367
+ include Plutonium::Resource::Record
368
+ belongs_to :user
369
+ end
370
+
371
+ class User < ApplicationRecord
372
+ has_many :documents
373
+ end
374
+
375
+ # Find all documents associated with a specific user
376
+ user = User.first
377
+ Document.associated_with(user)
378
+ # Equivalent to: Document.where(user: user)
379
+ ```
380
+
381
+ ### Automatic Association Detection
382
+
383
+ The module automatically detects associations in both directions:
384
+
385
+ **Direct Association (Preferred)**
386
+ ```ruby
387
+ class Comment < ApplicationRecord
388
+ belongs_to :post # Direct association
389
+ end
390
+
391
+ # Automatically uses the direct association
392
+ Comment.associated_with(post) # => Comment.where(post: post)
393
+ ```
394
+
395
+ **Reverse Association (With Performance Warning)**
396
+ ```ruby
397
+ class Post < ApplicationRecord
398
+ has_many :comments # Reverse association
399
+ end
400
+
401
+ # Uses reverse association with performance warning
402
+ Comment.associated_with(post)
403
+ # Warning: Using indirect association from Post to Comment
404
+ # via 'comments'. This may result in poor query performance...
405
+ ```
406
+
407
+ ### Custom Scopes
408
+
409
+ For optimal performance, where a direct association is not possible, define custom scopes:
410
+
411
+ ```ruby
412
+ class Comment < ApplicationRecord
413
+ include Plutonium::Resource::Record
414
+
415
+ # Custom scope for better performance
416
+ scope :associated_with_post, ->(post) { where(post_id: post.id) }
417
+ end
418
+
419
+ # Automatically uses the custom scope
420
+ Comment.associated_with(post) # Uses :associated_with_post scope
421
+ ```
422
+
423
+ ### Association Query Types
424
+
425
+ **Belongs To**
426
+ ```ruby
427
+ class Comment < ApplicationRecord
428
+ belongs_to :post
429
+ end
430
+
431
+ Comment.associated_with(post)
432
+ # Generates: Comment.where(post: post)
433
+ ```
434
+
435
+ **Has One**
436
+ ```ruby
437
+ class Profile < ApplicationRecord
438
+ has_one :user
439
+ end
440
+
441
+ Profile.associated_with(user)
442
+ # Generates: Profile.joins(:user).where(user: {id: user.id})
443
+ ```
444
+
445
+ **Has Many**
446
+ ```ruby
447
+ class Post < ApplicationRecord
448
+ has_many :comments
449
+ end
450
+
451
+ # When finding posts associated with a comment
452
+ Post.associated_with(comment)
453
+ # Generates: Post.joins(:comments).where(comments: {id: comment.id})
454
+ ```
455
+
456
+ ### Error Handling
457
+
458
+ When associations cannot be resolved:
459
+
460
+ ```ruby
461
+ class UnrelatedModel < ApplicationRecord
462
+ include Plutonium::Resource::Record
463
+ end
464
+
465
+ UnrelatedModel.associated_with(user)
466
+ # Raises: Could not resolve the association between 'UnrelatedModel' and 'User'
467
+ #
468
+ # Define:
469
+ # 1. the associations between the models
470
+ # 2. a named scope on UnrelatedModel e.g.
471
+ #
472
+ # scope :associated_with_user, ->(user) { do_something_here }
473
+ ```
474
+
475
+ ---
476
+
477
+ ## Generator Integration
478
+
479
+ The Resource Record module integrates seamlessly with Plutonium generators:
480
+
481
+ ### Model Generation
482
+
483
+ ```bash
484
+ # Generate a model with monetary fields
485
+ rails generate pu:res:model Product name:string price_cents:integer
486
+
487
+ # Generated model includes has_cents automatically
488
+ class Product < MyApp::ResourceRecord
489
+ has_cents :price_cents
490
+ validates :name, presence: true
491
+ end
492
+ ```
493
+
494
+ ### Automatic Field Detection
495
+
496
+ Generators automatically detect and configure:
497
+ - `*_cents` fields get `has_cents` declarations
498
+ - Reference fields get `belongs_to` associations
499
+ - Required fields get presence validations
500
+
501
+ ---
502
+
503
+ ## Best Practices
504
+
505
+ ### Monetary Fields
506
+
507
+ ```ruby
508
+ # ✅ Good: Use descriptive names
509
+ has_cents :price_cents
510
+ has_cents :shipping_cost_cents, name: :shipping_cost
511
+
512
+ # ❌ Avoid: Generic names
513
+ has_cents :amount_cents # What kind of amount?
514
+ ```
515
+
516
+ ### Custom Path Parameters
517
+
518
+ ```ruby
519
+ # ✅ Good: Use stable, unique fields
520
+ class User < ApplicationRecord
521
+ private
522
+
523
+ def path_parameter(param_name)
524
+ path_parameter :username # Stable and unique
525
+ end
526
+ end
527
+
528
+ # ❌ Avoid: Changeable fields
529
+ class User < ApplicationRecord
530
+ private
531
+
532
+ def dynamic_path_parameter(param_name)
533
+ dynamic_path_parameter :name # Can change, breaks bookmarks
534
+ end
535
+ end
536
+ ```
537
+
538
+ ### Entity Scoping
539
+
540
+ ```ruby
541
+ # ✅ Good: Define custom scopes for complex queries
542
+ class Order < ApplicationRecord
543
+ scope :associated_with_customer, ->(customer) do
544
+ joins(:customer).where(customers: { id: customer.id })
545
+ end
546
+ end
547
+
548
+ # ✅ Good: Use direct associations when possible
549
+ class OrderItem < ApplicationRecord
550
+ belongs_to :order
551
+ # associated_with will automatically use the direct association
552
+ end
553
+ ```
554
+
555
+ ### Field Introspection
556
+
557
+ ```ruby
558
+ # ✅ Good: Use field introspection in dynamic code
559
+ def build_form_fields
560
+ resource_class.resource_field_names.each do |field|
561
+ # Build form field dynamically
562
+ end
563
+ end
564
+
565
+ # ✅ Good: Cache results in production
566
+ def expensive_field_analysis
567
+ Rails.cache.fetch("#{model_name}_field_analysis", expires_in: 1.hour) do
568
+ analyze_fields(resource_field_names)
569
+ end
570
+ end
571
+ ```
572
+
573
+ ---
574
+
575
+ ## Performance Considerations
576
+
577
+ ### Field Introspection Caching
578
+
579
+ Field introspection methods are automatically cached in non-local environments:
580
+
581
+ ```ruby
582
+ # Cached in production/staging
583
+ User.resource_field_names
584
+ User.has_many_association_field_names
585
+
586
+ # Always fresh in development
587
+ Rails.env.local? # => true, no caching
588
+ ```
589
+
590
+ ### Association Query Optimization
591
+
592
+ ```ruby
593
+ # ✅ Efficient: Direct association
594
+ Comment.associated_with(post) # Uses WHERE clause
595
+
596
+ # ⚠️ Less efficient: Reverse association
597
+ Post.associated_with(comment) # Uses JOIN + WHERE
598
+
599
+ # ✅ Optimal: Direct association
600
+ belongs_to :post
601
+
602
+ # ✅ Alternative: Custom scope (when direct association is not possible)
603
+ scope :associated_with_comment, ->(comment) do
604
+ where(id: comment.post_id) # Direct ID lookup
605
+ end
606
+ ```
607
+
608
+ ### SGID Performance
609
+
610
+ ```ruby
611
+ # ✅ Efficient: Batch operations
612
+ user.post_sgids = sgid_array # Single assignment
613
+
614
+ # ❌ Inefficient: Individual operations
615
+ sgid_array.each { |sgid| user.add_post_sgid(sgid) } # Multiple queries
616
+ ```
617
+
618
+ The Resource Record module provides a comprehensive foundation for building robust, feature-rich ActiveRecord models within the Plutonium framework, handling everything from monetary values to complex association queries with performance and security in mind.