Adding Resources
This guide covers creating new resources and connecting them to portals.
Quick Start
# Generate a resource in the main app
rails g pu:res:scaffold Product name:string 'price:decimal{10,2}' --dest=main_app
# Generate a resource in a feature package
rails g pu:res:scaffold Product name:string 'price:decimal{10,2}' --dest=inventory
# Connect to a portal
rails g pu:res:conn Product --dest=admin_portalThe Resource Generator
Basic Usage
rails g pu:res:scaffold ModelName field:type field:type --dest=DESTINATIONAlways specify --dest to avoid interactive prompts:
--dest=main_appfor resources in the main application--dest=package_namefor resources in a feature package
Field Types
Format: name:type:index_type
| Type | Example | Description |
|---|---|---|
string | title:string | Short text (required) |
'title:string?' | 'title:string?' | Short text (nullable) |
text | body:text | Long text |
integer | quantity:integer | Whole numbers |
decimal | 'price:decimal{10,2}' | Decimal with precision |
float | rating:float | Floating point |
boolean | active:boolean | True/false |
date | published_on:date | Date only |
datetime | published_at:datetime | Date and time |
time | starts_at:time | Time only |
json | metadata:json | JSON data |
Nullable Fields
Append ? to make a field nullable. Quote fields with special characters:
'name:string?' # Nullable string
'description:text?' # Nullable text
'published_at:datetime?' # Nullable datetimeDecimal Precision
Use {precision,scale} syntax for decimal fields:
'price:decimal{10,2}' # precision: 10, scale: 2
'latitude:decimal{11,8}' # precision: 11, scale: 8
'amount:decimal?{15,2}' # nullable with precisionAssociations
# Required belongs_to
user:belongs_to
company:references # Same as belongs_to
# Nullable belongs_to
'parent:belongs_to?' # Creates: null: true, optional: true
# Cross-package reference
blogging/post:belongs_toIndexes
Add index type as the third segment:
email:string:index # Regular index
email:string:uniq # Unique indexSpecial Types
password_digest # has_secure_password
auth_token:token # has_secure_token (auto unique index)
content:rich_text # has_rich_text (Action Text)
avatar:attachment # has_one_attached (Active Storage)
photos:attachments # has_many_attached
price_cents:integer # has_cents (money field)Generator Options
# Skip model generation (use existing model)
rails g pu:res:scaffold Post --no-model --dest=main_app
# Skip migration generation
rails g pu:res:scaffold Post --no-migration --dest=main_app
# Both (for existing models with Plutonium::Resource::Record)
rails g pu:res:scaffold Post --no-model --no-migration --dest=main_appGenerated Files
For Main App Resources
app/
├── models/post.rb
├── controllers/posts_controller.rb
├── definitions/post_definition.rb
└── policies/post_policy.rb
db/migrate/xxx_create_posts.rbFor Packaged Resources
packages/blogging/
├── app/
│ ├── models/blogging/post.rb
│ ├── controllers/blogging/posts_controller.rb
│ ├── definitions/blogging/post_definition.rb
│ └── policies/blogging/post_policy.rb
db/migrate/xxx_create_blogging_posts.rbModel
class Post < ResourceRecord
include Plutonium::Resource::Record
endDefinition
class PostDefinition < ResourceDefinition
# Fields auto-detected from model
endPolicy
class PostPolicy < ResourcePolicy
def permitted_attributes_for_create
%i[title content user_id]
end
def permitted_attributes_for_read
%i[title content user_id created_at updated_at]
end
endConnecting to Portals
Resources must be connected to a portal to be accessible via the web.
Using the Generator
rails g pu:res:conn Post --dest=admin_portalThis:
- Registers the resource in portal routes
- Creates a portal-specific controller
- Creates portal-specific policy and definition (if base versions don't exist)
Connecting Multiple Resources
rails g pu:res:conn Post Comment Tag --dest=admin_portalConnecting Namespaced Resources
Use the full class name for packaged resources:
rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portalWhat Gets Generated
packages/admin_portal/
├── app/
│ ├── controllers/admin_portal/posts_controller.rb
│ ├── policies/admin_portal/post_policy.rb
│ └── definitions/admin_portal/post_definition.rb
└── config/routes.rb # Updated with register_resourcePortal-Specific Customization
Portal Controller
# packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
class AdminPortal::PostsController < ::PostsController
include AdminPortal::Concerns::Controller
private
def build_resource
super.tap do |post|
post.user = current_user
end
end
endPortal Definition
# packages/admin_portal/app/definitions/admin_portal/post_definition.rb
class AdminPortal::PostDefinition < ::PostDefinition
# Add admin-only fields
field :internal_notes
# Customize existing fields
field :status, as: :select, collection: %w[draft published archived]
endPortal Policy
# packages/admin_portal/app/policies/admin_portal/post_policy.rb
class AdminPortal::PostPolicy < ::PostPolicy
include AdminPortal::ResourcePolicy
# Admins can do everything
def destroy?
true
end
def permitted_attributes_for_create
super + [:internal_notes]
end
endMultiple Portals
Connect the same resource to multiple portals:
rails g pu:res:conn Post --dest=admin_portal
rails g pu:res:conn Post --dest=author_portalEach portal can have different customizations.
From Existing Models
If you have existing Rails models you want to convert to Plutonium resources:
Option 1: Model already includes Plutonium::Resource::Record
rails g pu:res:scaffold Post --no-model --no-migration --dest=main_appThis generates only the definition, policy, and controller.
Option 2: Let the generator update the model
rails g pu:res:scaffold Post --dest=main_appRun without attributes to auto-import fields from the model's content columns.
Required Model Setup
Your model must include Plutonium::Resource::Record:
class Post < ApplicationRecord
include Plutonium::Resource::Record
endAdding Fields After Creation
1. Create Migration
rails g migration AddStatusToPosts status:string2. Update Model (if needed)
class Post < ResourceRecord
validates :status, inclusion: { in: %w[draft published] }
end3. Fields Auto-Detected
New columns automatically appear in forms. To customize:
# In definition
field :status, as: :select, collection: %w[draft published]Migration Customizations
Always review and customize generated migrations:
Inline Indexes (preferred)
create_table :posts do |t|
t.belongs_to :user, null: false, foreign_key: true
t.string :title, null: false
t.timestamps
t.index :title
t.index [:user_id, :title], unique: true
endCascade Delete
t.belongs_to :user, null: false, foreign_key: {on_delete: :cascade}Default Values
t.boolean :is_active, default: true
t.integer :status, default: 0Removing Resources
Remove from Portal
- Remove
register_resourcefrom portal routes - Delete portal-specific files in
packages/portal_name/app/
Remove Entirely
# Remove files (main app example)
rm app/models/post.rb
rm app/controllers/posts_controller.rb
rm app/definitions/post_definition.rb
rm app/policies/post_policy.rb
# Create migration to drop table
rails g migration DropPostsBest Practices
1. Always Specify --dest
Avoids interactive prompts and makes commands reproducible.
2. Quote Special Characters
Fields with ? or {} must be quoted to prevent shell expansion:
rails g pu:res:scaffold Post 'content:text?' 'price:decimal{10,2}' --dest=main_app3. Run Migrations Before Connecting
rails g pu:res:scaffold Post title:string --dest=main_app
rails db:migrate
rails g pu:res:conn Post --dest=admin_portal4. Review Generated Migrations
Add cascade deletes, composite indexes, and default values as needed.
Troubleshooting
Resource Not Found
Ensure the resource is connected to the portal with register_resource.
Fields Not Showing
Check that the migration has run and the policy includes the field in permitted_attributes_for_read.
Policy Denying Access
Check the policy's permission methods (index?, show?, create?, etc.) return true.
Connection Generator Fails
Ensure migrations have run - the generator reads model columns to build policy attributes.
