plutonium 0.24.1 → 0.24.3
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/app/views/plutonium/_resource_header.html.erb +1 -1
- data/docs/.vitepress/config.ts +1 -0
- data/docs/guide/deep-dive/multitenancy.md +256 -0
- data/docs/modules/controller.md +1 -1
- data/docs/modules/core.md +5 -5
- data/lib/plutonium/ui/display/resource.rb +1 -1
- data/lib/plutonium/ui/form/interaction.rb +28 -2
- data/lib/plutonium/ui/frame_navigator_panel.rb +6 -4
- data/lib/plutonium/ui/layout/rodauth_layout.rb +3 -3
- data/lib/plutonium/version.rb +1 -1
- data/plutonium-blog-post.md +45 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 26384f4aed52055be1bcc886fcb5df8c98d84297254ab95c8a5de40c6263f492
|
4
|
+
data.tar.gz: 870630d6315cc1e2964a6fea922e276f52957fcf7a0a6d381dc52d6e84a96c1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 649df44bd7ad78590c769afffacbbd604919c246f3e1db98eb0397509d7111b24bc9eec2a13bd311c59ac2d1a42d1b9cef06ce7a8e15a6ae69012066ce645a67
|
7
|
+
data.tar.gz: 1ac6bc80a0788d79c6d99211134a45afce8ed54d6dfff156766c44587314951a561733de01c7c3a8e6abc41d06c0235542a7f4085ca4041fb599459497f18e0b
|
data/docs/.vitepress/config.ts
CHANGED
@@ -56,6 +56,7 @@ export default defineConfig(withMermaid({
|
|
56
56
|
items: [
|
57
57
|
{ text: "Resources", link: "/guide/deep-dive/resources" },
|
58
58
|
{ text: "Authorization", link: "/guide/deep-dive/authorization" },
|
59
|
+
{ text: "Multitenancy", link: "/guide/deep-dive/multitenancy" },
|
59
60
|
{ text: "Modules", link: "/modules/" },
|
60
61
|
]
|
61
62
|
},
|
@@ -0,0 +1,256 @@
|
|
1
|
+
# Deep Dive: Multitenancy
|
2
|
+
|
3
|
+
Plutonium is designed to make building sophisticated, multi-tenant applications straightforward. Multitenancy allows you to serve distinct groups of users (like different companies or teams) from a single application instance, ensuring each group's data is kept private and isolated.
|
4
|
+
|
5
|
+
This is achieved through a powerful feature called **Entity Scoping**, which automates data isolation, URL generation, and authorization with minimal setup.
|
6
|
+
|
7
|
+
::: tip What You'll Learn
|
8
|
+
- The core concept of Entity Scoping
|
9
|
+
- A step-by-step guide to configuring multitenancy
|
10
|
+
- The practical benefits of automatic data isolation and routing
|
11
|
+
- Advanced patterns for complex multi-tenant scenarios
|
12
|
+
- Best practices for security, testing, and performance
|
13
|
+
:::
|
14
|
+
|
15
|
+
## How Entity Scoping Works
|
16
|
+
|
17
|
+
Entity Scoping is the heart of Plutonium's multitenancy. Instead of manually filtering data in every controller and model, you declare a **Scoping Entity** for a user-facing **Portal**. This entity is typically a model like `Organization`, `Account`, or `Workspace`.
|
18
|
+
|
19
|
+
Once configured, Plutonium handles the rest automatically.
|
20
|
+
|
21
|
+
::: code-group
|
22
|
+
```ruby [Without Plutonium Scoping]
|
23
|
+
# Manual filtering is required everywhere
|
24
|
+
class PostsController < ApplicationController
|
25
|
+
def index
|
26
|
+
# Manually find the organization and scope posts
|
27
|
+
@organization = Organization.find(params[:organization_id])
|
28
|
+
@posts = @organization.posts
|
29
|
+
end
|
30
|
+
|
31
|
+
def create
|
32
|
+
# Manually associate new records with the organization
|
33
|
+
@organization = Organization.find(params[:organization_id])
|
34
|
+
@post = @organization.posts.build(post_params)
|
35
|
+
# ...
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
```ruby [With Plutonium Scoping]
|
41
|
+
# Scoping is automatic and declarative
|
42
|
+
class PostsController < ResourceController
|
43
|
+
def index
|
44
|
+
# current_authorized_scope automatically returns posts scoped to the current organization
|
45
|
+
# When using path strategy: URL -> /organizations/123/posts
|
46
|
+
# When using subdomain strategy: URL -> /posts (on acme.yourapp.com)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create
|
50
|
+
# resource_params automatically includes the current organization association
|
51
|
+
# Scoping works regardless of the strategy used
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
:::
|
56
|
+
|
57
|
+
## Setting Up Your Multi-Tenant Portal
|
58
|
+
|
59
|
+
Configuring multitenancy involves three main steps: defining your strategy, implementing it, and ensuring your models are correctly associated.
|
60
|
+
|
61
|
+
### 1. Configure Your Portal Engine
|
62
|
+
|
63
|
+
First, you tell your portal which entity to scope to and which strategy to use. This is done in the portal's `engine.rb` file using `scope_to_entity`.
|
64
|
+
|
65
|
+
A **Scoping Strategy** tells Plutonium how to identify the current tenant for each request.
|
66
|
+
|
67
|
+
::: code-group
|
68
|
+
```ruby [Path Strategy (Most Common)]
|
69
|
+
# packages/admin_portal/lib/engine.rb
|
70
|
+
# Tenant is identified by a URL parameter, e.g., /organizations/:organization_id
|
71
|
+
scope_to_entity Organization, strategy: :path
|
72
|
+
```
|
73
|
+
|
74
|
+
```ruby [Custom Strategy (Subdomain)]
|
75
|
+
# packages/customer_portal/lib/engine.rb
|
76
|
+
# Tenant is identified by the a custom strategy that checks the subdomain, e.g., acme.yourapp.com
|
77
|
+
scope_to_entity Organization, strategy: :current_organization
|
78
|
+
```
|
79
|
+
|
80
|
+
```ruby [Custom Parameter Key]
|
81
|
+
# packages/client_portal/lib/engine.rb
|
82
|
+
# Same as :path, but with a custom parameter name.
|
83
|
+
scope_to_entity Client,
|
84
|
+
strategy: :path,
|
85
|
+
param_key: :client_slug # URL -> /clients/:client_slug
|
86
|
+
```
|
87
|
+
:::
|
88
|
+
|
89
|
+
You can also use any custom name for your strategy method (the method name becomes the strategy).
|
90
|
+
|
91
|
+
### 2. Implement the Strategy Method
|
92
|
+
|
93
|
+
If you use any strategy other than `:path`, you must implement a controller method that returns the current tenant object. The method name must exactly match the strategy name.
|
94
|
+
|
95
|
+
This logic typically lives in your portal's base controller concern.
|
96
|
+
|
97
|
+
::: code-group
|
98
|
+
```ruby [Subdomain Strategy]
|
99
|
+
# packages/customer_portal/app/controllers/customer_portal/concerns/controller.rb
|
100
|
+
private
|
101
|
+
|
102
|
+
# Method name :current_organization matches the strategy
|
103
|
+
def current_organization
|
104
|
+
@current_organization ||= Organization.find_by!(subdomain: request.subdomain)
|
105
|
+
rescue ActiveRecord::RecordNotFound
|
106
|
+
redirect_to root_path, error: "Invalid organization subdomain"
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
```ruby [Session-Based Strategy]
|
111
|
+
# packages/internal_portal/app/controllers/internal_portal/concerns/controller.rb
|
112
|
+
# In engine.rb: scope_to_entity Workspace, strategy: :current_workspace
|
113
|
+
private
|
114
|
+
|
115
|
+
def current_workspace
|
116
|
+
return @current_workspace if defined?(@current_workspace)
|
117
|
+
|
118
|
+
workspace_id = session[:workspace_id] || params[:workspace_id]
|
119
|
+
@current_workspace = current_user.workspaces.find(workspace_id)
|
120
|
+
|
121
|
+
session[:workspace_id] = @current_workspace.id # Remember for next request
|
122
|
+
@current_workspace
|
123
|
+
rescue ActiveRecord::RecordNotFound
|
124
|
+
redirect_to workspace_selection_path, error: "Please select a workspace"
|
125
|
+
end
|
126
|
+
```
|
127
|
+
:::
|
128
|
+
|
129
|
+
### 3. Connect Your Models
|
130
|
+
|
131
|
+
Plutonium needs to understand how your resources relate to the scoping entity. It automatically discovers these relationships in three ways:
|
132
|
+
|
133
|
+
::: code-group
|
134
|
+
```ruby [1. Direct Association (Preferred)]
|
135
|
+
# The model belongs directly to the scoping entity.
|
136
|
+
class Post < ApplicationRecord
|
137
|
+
belongs_to :organization # Direct link
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
```ruby [2. Indirect Association]
|
142
|
+
# The model belongs to another model that has the direct link.
|
143
|
+
# Plutonium automatically follows the chain: Comment -> Post -> Organization
|
144
|
+
class Comment < ApplicationRecord
|
145
|
+
belongs_to :post
|
146
|
+
has_one :organization, through: :post # Indirect link
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
```ruby [3. Custom Scope (For Complex Cases)]
|
151
|
+
# For complex relationships, you can define an explicit scope.
|
152
|
+
# The scope name must be `associated_with_#{scoping_entity_name}`.
|
153
|
+
class Invoice < ApplicationRecord
|
154
|
+
belongs_to :customer
|
155
|
+
|
156
|
+
scope :associated_with_organization, ->(organization) do
|
157
|
+
joins(customer: :organization_memberships)
|
158
|
+
.where(organization_memberships: { organization_id: organization.id })
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
:::
|
163
|
+
|
164
|
+
## The Benefits in Practice
|
165
|
+
|
166
|
+
With this setup complete, you gain several powerful features across your portal.
|
167
|
+
|
168
|
+
### Tenant-Aware Routing
|
169
|
+
|
170
|
+
Your application's URLs are automatically transformed to include the tenant context, and Plutonium's URL helpers adapt accordingly.
|
171
|
+
|
172
|
+
- **URL Transformation:** Routes like `/posts` and `/posts/123` become `/:organization_id/posts` and `/:organization_id/posts/123`.
|
173
|
+
- **Automatic URL Generation:** The `resource_url_for` helper automatically includes the current tenant in all generated URLs, so links and forms work without any changes.
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
# Both of these helpers are automatically aware of the current tenant.
|
177
|
+
resource_url_for(Post) # => "/organizations/456/posts"
|
178
|
+
form_with model: @post # action -> "/organizations/456/posts/123"
|
179
|
+
```
|
180
|
+
|
181
|
+
### Secure Data Scoping
|
182
|
+
|
183
|
+
All data access is automatically and securely filtered to the current tenant.
|
184
|
+
|
185
|
+
- **Query Scoping:** A query like `Post.all` is automatically converted to `current_scoped_entity.posts`. This prevents accidental data leaks.
|
186
|
+
- **Record Scoping:** When fetching a single record (e.g., for `show` or `edit`), Plutonium ensures it belongs to the current tenant. If not, it raises an `ActiveRecord::RecordNotFound` error, just as if the record didn't exist.
|
187
|
+
|
188
|
+
### Integrated Authorization
|
189
|
+
|
190
|
+
The current `entity_scope` is seamlessly passed to your authorization policies, allowing for fine-grained, tenant-aware rules.
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class PostPolicy < Plutonium::Resource::Policy
|
194
|
+
# `entity_scope` is automatically available in all policy methods.
|
195
|
+
authorize :entity_scope, allow_nil: true
|
196
|
+
|
197
|
+
def update?
|
198
|
+
# A user can only update a post if it belongs to their current tenant
|
199
|
+
# AND they are the author of the post.
|
200
|
+
record.organization == entity_scope && record.author == user
|
201
|
+
end
|
202
|
+
|
203
|
+
relation_scope do |relation|
|
204
|
+
# `super` automatically applies the base entity scoping.
|
205
|
+
relation = super(relation)
|
206
|
+
|
207
|
+
# Add more logic: Admins can see all posts within their organization,
|
208
|
+
# but others can only see published posts.
|
209
|
+
user.admin? ? relation : relation.where(published: true)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
## Security Best Practices
|
215
|
+
|
216
|
+
Securing a multi-tenant application is critical. While Plutonium provides strong defaults, you must ensure your implementation is secure.
|
217
|
+
|
218
|
+
::: warning Always Validate Tenant Access
|
219
|
+
A user might belong to multiple tenants. It's crucial to verify that the logged-in user has permission to access the tenant specified in the URL. Failure to do so could allow a user to see data from another organization they don't belong to.
|
220
|
+
:::
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
# ✅ Good: Proper tenant validation
|
224
|
+
# In your custom strategy method or a before_action:
|
225
|
+
private
|
226
|
+
|
227
|
+
def current_organization
|
228
|
+
@current_organization ||= begin
|
229
|
+
# Find the organization from the URL
|
230
|
+
organization = Organization.find(params[:organization_id])
|
231
|
+
|
232
|
+
# CRITICAL: Verify the current user is a member of that organization
|
233
|
+
unless current_user.organizations.include?(organization)
|
234
|
+
raise ActionPolicy::Unauthorized, "Access denied to organization"
|
235
|
+
end
|
236
|
+
|
237
|
+
organization
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# ❌ Dangerous: No access validation
|
242
|
+
def current_organization
|
243
|
+
# This allows ANY authenticated user to access ANY organization's data
|
244
|
+
# simply by changing the ID in the URL.
|
245
|
+
Organization.find(params[:organization_id])
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
## Advanced Patterns
|
250
|
+
|
251
|
+
Plutonium's scoping is flexible enough to handle more complex scenarios.
|
252
|
+
|
253
|
+
- **Multi-Level Tenancy:** For hierarchical tenancy (e.g., Company -> Department), you can apply the primary scope at the engine level and add secondary scoping logic inside your policies' `relation_scope`.
|
254
|
+
- **Cross-Tenant Data Access:** For resources that can be shared, define a custom `associated_with_...` scope that includes both shared records and records belonging to the current tenant.
|
255
|
+
- **Tenant Switching:** Build a controller that allows users to change their active tenant by updating a `session` key, then use a session-based scoping strategy to read it.
|
256
|
+
- **API Multitenancy:** Create a custom scoping strategy (e.g., `:api_tenant`) that authenticates and identifies the tenant based on an API key or JWT from the request headers.
|
data/docs/modules/controller.md
CHANGED
@@ -188,7 +188,7 @@ Portal Controllers provide specialized functionality for multi-tenant applicatio
|
|
188
188
|
|
189
189
|
```ruby
|
190
190
|
module AdminPortal
|
191
|
-
class PostsController <
|
191
|
+
class PostsController < ResourceController
|
192
192
|
include AdminPortal::Concerns::Controller
|
193
193
|
|
194
194
|
# Automatically inherits from ::PostsController
|
data/docs/modules/core.md
CHANGED
@@ -76,7 +76,7 @@ The Bootable concern automatically detects which package and engine a controller
|
|
76
76
|
|
77
77
|
```ruby
|
78
78
|
# In packages/admin_portal/app/controllers/users_controller.rb
|
79
|
-
class UsersController <
|
79
|
+
class UsersController < ResourceController
|
80
80
|
# Automatically detects:
|
81
81
|
# current_package => AdminPortal
|
82
82
|
# current_engine => AdminPortal::Engine
|
@@ -221,13 +221,13 @@ The Core module is typically used through other Plutonium modules:
|
|
221
221
|
|
222
222
|
```ruby
|
223
223
|
# For resource controllers
|
224
|
-
class UsersController <
|
224
|
+
class UsersController < ResourceController
|
225
225
|
include Plutonium::Resource::Controller
|
226
226
|
# Core module included automatically
|
227
227
|
end
|
228
228
|
|
229
229
|
# For portal controllers
|
230
|
-
class DashboardController <
|
230
|
+
class DashboardController < ResourceController
|
231
231
|
include Plutonium::Portal::Controller
|
232
232
|
# Core module included automatically
|
233
233
|
end
|
@@ -294,12 +294,12 @@ Let the Bootable concern handle package detection automatically:
|
|
294
294
|
|
295
295
|
```ruby
|
296
296
|
# ✅ Good - automatic detection
|
297
|
-
class AdminPortal::UsersController <
|
297
|
+
class AdminPortal::UsersController < ResourceController
|
298
298
|
# Package and engine automatically detected
|
299
299
|
end
|
300
300
|
|
301
301
|
# ❌ Avoid - manual configuration
|
302
|
-
class UsersController <
|
302
|
+
class UsersController < ResourceController
|
303
303
|
def current_package
|
304
304
|
AdminPortal # Don't override unless necessary
|
305
305
|
end
|
@@ -61,7 +61,7 @@ module Plutonium
|
|
61
61
|
identifier: title.parameterize,
|
62
62
|
title: -> { h5(class: "text-2xl font-bold tracking-tight text-gray-900 dark:text-white") { title } }
|
63
63
|
) do
|
64
|
-
FrameNavigatorPanel(title: "", src:)
|
64
|
+
FrameNavigatorPanel(title: "", src:, panel_id: "association-panel-#{title.parameterize}")
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
@@ -15,8 +15,34 @@ module Plutonium
|
|
15
15
|
private
|
16
16
|
|
17
17
|
def form_action
|
18
|
-
#
|
19
|
-
|
18
|
+
# Build the correct commit URL for the interactive action
|
19
|
+
action = helpers.current_interactive_action
|
20
|
+
return nil unless action
|
21
|
+
|
22
|
+
# Create route options for the commit action (convert GET to POST action)
|
23
|
+
commit_route_options = action.route_options.merge(
|
24
|
+
Plutonium::Action::RouteOptions.new(
|
25
|
+
method: :post,
|
26
|
+
action: commit_action_name(action.route_options.url_options[:action])
|
27
|
+
)
|
28
|
+
)
|
29
|
+
|
30
|
+
# Use existing infrastructure to build the URL
|
31
|
+
subject = action.record_action? ? helpers.resource_record! : helpers.resource_class
|
32
|
+
helpers.route_options_to_url(commit_route_options, subject)
|
33
|
+
end
|
34
|
+
|
35
|
+
def commit_action_name(action_name)
|
36
|
+
case action_name
|
37
|
+
when :interactive_record_action
|
38
|
+
:commit_interactive_record_action
|
39
|
+
when :interactive_resource_action
|
40
|
+
:commit_interactive_resource_action
|
41
|
+
when :interactive_collection_action
|
42
|
+
:commit_interactive_bulk_action
|
43
|
+
else
|
44
|
+
action_name
|
45
|
+
end
|
20
46
|
end
|
21
47
|
|
22
48
|
def initialize_attributes
|
@@ -41,20 +41,22 @@ module Plutonium
|
|
41
41
|
end
|
42
42
|
|
43
43
|
class PanelContent < Plutonium::UI::Component::Base
|
44
|
-
def initialize(src:)
|
44
|
+
def initialize(id:, src:)
|
45
|
+
@id = id
|
45
46
|
@src = src
|
46
47
|
end
|
47
48
|
|
48
49
|
def view_template
|
49
|
-
DynaFrameHost src: @src, loading: :lazy, data: {"frame-navigator-target": "frame"} do
|
50
|
+
DynaFrameHost id: @id, src: @src, loading: :lazy, data: {"frame-navigator-target": "frame"} do
|
50
51
|
SkeletonTable()
|
51
52
|
end
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
55
|
-
def initialize(title:, src:)
|
56
|
+
def initialize(title:, src:, panel_id: nil)
|
56
57
|
@title = title
|
57
58
|
@src = src
|
59
|
+
@panel_id = panel_id
|
58
60
|
end
|
59
61
|
|
60
62
|
def view_template
|
@@ -65,7 +67,7 @@ module Plutonium
|
|
65
67
|
panel.with_item PanelItem.new(label: "Back", icon: Phlex::TablerIcons::ChevronLeft, data_frame_navigator_target: "backButton")
|
66
68
|
panel.with_item PanelItem.new(label: "Refresh", icon: Phlex::TablerIcons::RefreshDot, data_frame_navigator_target: "refreshButton")
|
67
69
|
panel.with_item PanelLink.new(label: "Maximize", icon: Phlex::TablerIcons::WindowMaximize, href: @src, data_frame_navigator_target: "maximizeLink")
|
68
|
-
panel.with_content PanelContent.new(src: @src)
|
70
|
+
panel.with_content PanelContent.new(id: @panel_id, src: @src)
|
69
71
|
end
|
70
72
|
end
|
71
73
|
end
|
@@ -11,7 +11,7 @@ module Plutonium
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def main_attributes = mix(super, {
|
14
|
-
class: "flex flex-col items-center justify-center px-6 py-8 mx-auto lg:py-0"
|
14
|
+
class: "flex flex-col items-center justify-center gap-2 px-6 py-8 mx-auto lg:py-0"
|
15
15
|
})
|
16
16
|
|
17
17
|
def render_content(&)
|
@@ -25,8 +25,8 @@ module Plutonium
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def render_logo
|
28
|
-
link_to root_path, class: "flex items-center text-2xl font-semibold text-gray-900 dark:text-white" do
|
29
|
-
helpers.resource_logo_tag classname: "w-24 h-24 mr-2"
|
28
|
+
link_to root_path, class: "flex items-center text-2xl font-semibold text-gray-900 dark:text-white mb-2" do
|
29
|
+
helpers.resource_logo_tag classname: "w-24 h-24 mr-2 rounded-md"
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
data/lib/plutonium/version.rb
CHANGED
@@ -0,0 +1,45 @@
|
|
1
|
+
# So, You've Decided to Build Another SaaS...
|
2
|
+
|
3
|
+
Let me guess. You have a brilliant idea for a new web application. It's going to be the next big thing. You're fired up, ready to code. You spin up a new Rails app, and then it hits you. The boilerplate. User authentication, admin panels, data tables, filters, authorization policies... Suddenly your brilliant idea is buried under a mountain of tedious, repetitive work.
|
4
|
+
|
5
|
+
We've all been there. Rails is great, but for complex applications, the initial setup can be a drag. You end up writing the same code over and over again, just to get to the starting line.
|
6
|
+
|
7
|
+
What if you could skip all that? What if you could get straight to building the features that make your app unique?
|
8
|
+
|
9
|
+
Enter Plutonium.
|
10
|
+
|
11
|
+
## What's This Plutonium Thing, Anyway?
|
12
|
+
|
13
|
+
Plutonium is a Rapid Application Development (RAD) toolkit for Rails. That's a fancy way of saying it helps you build feature-rich, enterprise-ready applications at a ridiculous speed. It's not a replacement for Rails; it's a high-level abstraction built on top of it. You still get all the Rails goodness you know and love, but with a whole lot of awesome packed on top.
|
14
|
+
|
15
|
+
The core idea behind Plutonium is its **Resource-Oriented Architecture**. In a Plutonium app, everything is a "Resource". A user is a Resource, a product is a Resource, a blog post is a Resource. You get the idea.
|
16
|
+
|
17
|
+
But these aren't just your plain old `ActiveRecord` models. A Plutonium Resource is a supercharged, self-contained unit that knows how to do a bunch of things on its own. Each Resource is made up of four parts:
|
18
|
+
|
19
|
+
* **The Model:** This is your good old `ActiveRecord` model. Nothing new here. It still handles your database interactions, validations, and associations.
|
20
|
+
* **The Definition:** This is where things get interesting. The Definition tells Plutonium how the Resource should look and behave in the UI. You define fields, search functionality, filters, and custom actions, all in a clean, declarative way.
|
21
|
+
* **The Policy:** This is for authorization. It controls who can do what with a Resource. It's like having a bouncer for every piece of data in your app.
|
22
|
+
* **The Actions:** These are for custom operations. Think of anything that's not a simple create, read, update, or delete. For example, an action to "publish" a blog post or "deactivate" a user.
|
23
|
+
|
24
|
+
This structure keeps your code incredibly organized and easy to reason about.
|
25
|
+
|
26
|
+
## Stop Organizing, Start Building
|
27
|
+
|
28
|
+
One of the biggest headaches in a large Rails app is keeping the code organized. As the app grows, `app/models`, `app/controllers`, and `app/views` can become a real mess. Plutonium solves this with a modular packaging system.
|
29
|
+
|
30
|
+
There are two types of packages:
|
31
|
+
|
32
|
+
* **Feature Packages:** These are where your core business logic lives. They're self-contained modules focused on a specific domain, like "invoicing" or "user management". They don't have any web-facing parts, just the raw logic.
|
33
|
+
* **Portal Packages:** These are the user interfaces. A portal takes one or more feature packages and presents them to a specific type of user, like an "admin portal" or a "customer portal".
|
34
|
+
|
35
|
+
This separation is a game-changer. It forces you to think about your application in a more structured way, which pays off big time in the long run.
|
36
|
+
|
37
|
+
And if you're building a multi-tenant SaaS app, you'll love this: Plutonium has built-in support for multi-tenancy. You can scope all your data to an entity like an `Organization` or `Account` with a single line of code in your portal configuration. Plutonium handles the rest, automatically ensuring that users only see the data that belongs to them.
|
38
|
+
|
39
|
+
To top it all off, Plutonium comes with a bunch of generators that create all the boilerplate for you. Need a new feature package? `rails generate pu:pkg:package my_feature`. Need a new resource with a model, definition, policy, and all the fixings? `rails generate pu:res:scaffold post title:string content:text`. It's like having a junior developer who does all the boring work for you.
|
40
|
+
|
41
|
+
## Is Plutonium for You?
|
42
|
+
|
43
|
+
Plutonium is not for every project. If you're building a simple blog or a marketing site, it's probably overkill. But if you're building a complex business application, a multi-tenant SaaS platform, an admin system, or a CMS, Plutonium can be a massive productivity boost.
|
44
|
+
|
45
|
+
So, next time you have that brilliant idea for a new app, maybe give Plutonium a try. You might just find that you can get from idea to launch a whole lot faster.
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plutonium
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.24.
|
4
|
+
version: 0.24.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Froelich
|
@@ -495,6 +495,7 @@ files:
|
|
495
495
|
- docs/api-examples.md
|
496
496
|
- docs/guide/cursor-rules.md
|
497
497
|
- docs/guide/deep-dive/authorization.md
|
498
|
+
- docs/guide/deep-dive/multitenancy.md
|
498
499
|
- docs/guide/deep-dive/resources.md
|
499
500
|
- docs/guide/getting-started/01-installation.md
|
500
501
|
- docs/guide/index.md
|
@@ -881,6 +882,7 @@ files:
|
|
881
882
|
- lib/tasks/.keep
|
882
883
|
- package-lock.json
|
883
884
|
- package.json
|
885
|
+
- plutonium-blog-post.md
|
884
886
|
- postcss-gem-import.js
|
885
887
|
- postcss.config.js
|
886
888
|
- public/.keep
|