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.
- checksums.yaml +4 -4
- data/# Plutonium: The pre-alpha demo.md +4 -2
- data/.claude/skills/assets/SKILL.md +416 -0
- data/.claude/skills/connect-resource/SKILL.md +112 -0
- data/.claude/skills/controller/SKILL.md +302 -0
- data/.claude/skills/create-resource/SKILL.md +240 -0
- data/.claude/skills/definition/SKILL.md +218 -0
- data/.claude/skills/definition-actions/SKILL.md +386 -0
- data/.claude/skills/definition-fields/SKILL.md +474 -0
- data/.claude/skills/definition-query/SKILL.md +334 -0
- data/.claude/skills/forms/SKILL.md +439 -0
- data/.claude/skills/installation/SKILL.md +300 -0
- data/.claude/skills/interaction/SKILL.md +382 -0
- data/.claude/skills/model/SKILL.md +267 -0
- data/.claude/skills/model-features/SKILL.md +286 -0
- data/.claude/skills/nested-resources/SKILL.md +274 -0
- data/.claude/skills/package/SKILL.md +191 -0
- data/.claude/skills/policy/SKILL.md +352 -0
- data/.claude/skills/portal/SKILL.md +400 -0
- data/.claude/skills/resource/SKILL.md +281 -0
- data/.claude/skills/rodauth/SKILL.md +452 -0
- data/.claude/skills/views/SKILL.md +563 -0
- data/Appraisals +46 -4
- data/CHANGELOG.md +32 -1
- data/app/assets/plutonium.css +2 -2
- data/config/brakeman.ignore +239 -0
- data/config/initializers/action_policy.rb +1 -1
- data/docs/.vitepress/config.ts +132 -47
- data/docs/concepts/architecture.md +226 -0
- data/docs/concepts/auto-detection.md +254 -0
- data/docs/concepts/index.md +61 -0
- data/docs/concepts/packages-portals.md +304 -0
- data/docs/concepts/resources.md +224 -0
- data/docs/cookbook/blog.md +412 -0
- data/docs/cookbook/index.md +289 -0
- data/docs/cookbook/saas.md +481 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +146 -0
- data/docs/getting-started/tutorial/01-setup.md +118 -0
- data/docs/getting-started/tutorial/02-first-resource.md +180 -0
- data/docs/getting-started/tutorial/03-authentication.md +246 -0
- data/docs/getting-started/tutorial/04-authorization.md +170 -0
- data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
- data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
- data/docs/getting-started/tutorial/index.md +64 -0
- data/docs/guides/adding-resources.md +420 -0
- data/docs/guides/authentication.md +551 -0
- data/docs/guides/authorization.md +468 -0
- data/docs/guides/creating-packages.md +380 -0
- data/docs/guides/custom-actions.md +523 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/multi-tenancy.md +302 -0
- data/docs/guides/nested-resources.md +411 -0
- data/docs/guides/search-filtering.md +266 -0
- data/docs/guides/theming.md +321 -0
- data/docs/index.md +67 -26
- data/docs/public/CLAUDE.md +64 -21
- data/docs/reference/assets/index.md +496 -0
- data/docs/reference/controller/index.md +363 -0
- data/docs/reference/definition/actions.md +400 -0
- data/docs/reference/definition/fields.md +350 -0
- data/docs/reference/definition/index.md +252 -0
- data/docs/reference/definition/query.md +342 -0
- data/docs/reference/generators/index.md +469 -0
- data/docs/reference/index.md +49 -0
- data/docs/reference/interaction/index.md +445 -0
- data/docs/reference/model/features.md +248 -0
- data/docs/reference/model/index.md +219 -0
- data/docs/reference/policy/index.md +385 -0
- data/docs/reference/portal/index.md +382 -0
- data/docs/reference/views/forms.md +396 -0
- data/docs/reference/views/index.md +479 -0
- data/gemfiles/rails_7.gemfile +9 -2
- data/gemfiles/rails_7.gemfile.lock +146 -111
- data/gemfiles/rails_8.0.gemfile +20 -0
- data/gemfiles/rails_8.0.gemfile.lock +417 -0
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/gemfiles/rails_8.1.gemfile.lock +419 -0
- data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
- data/lib/generators/pu/pkg/portal/USAGE +65 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
- data/lib/generators/pu/res/conn/USAGE +71 -0
- data/lib/generators/pu/res/model/USAGE +106 -110
- data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
- data/lib/generators/pu/res/scaffold/USAGE +85 -0
- data/lib/generators/pu/rodauth/install_generator.rb +2 -6
- data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
- data/lib/generators/pu/skills/sync/USAGE +14 -0
- data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/interaction/base.rb +1 -0
- data/lib/plutonium/package/engine.rb +2 -2
- data/lib/plutonium/query/adhoc_block.rb +6 -2
- data/lib/plutonium/query/model_scope.rb +1 -1
- data/lib/plutonium/railtie.rb +4 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/query_object.rb +38 -8
- data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +19 -4
- data/package.json +1 -1
- metadata +76 -39
- data/brakeman.ignore +0 -28
- data/docs/api-examples.md +0 -49
- data/docs/guide/claude-code-guide.md +0 -74
- data/docs/guide/deep-dive/authorization.md +0 -189
- data/docs/guide/deep-dive/multitenancy.md +0 -256
- data/docs/guide/deep-dive/resources.md +0 -390
- data/docs/guide/getting-started/01-installation.md +0 -165
- data/docs/guide/index.md +0 -28
- data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
- data/docs/guide/introduction/02-core-concepts.md +0 -440
- data/docs/guide/tutorial/01-project-setup.md +0 -75
- data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
- data/docs/guide/tutorial/03-defining-resources.md +0 -90
- data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
- data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
- data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
- data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
- data/docs/markdown-examples.md +0 -85
- data/docs/modules/action.md +0 -244
- data/docs/modules/authentication.md +0 -236
- data/docs/modules/configuration.md +0 -599
- data/docs/modules/controller.md +0 -443
- data/docs/modules/core.md +0 -316
- data/docs/modules/definition.md +0 -1308
- data/docs/modules/display.md +0 -759
- data/docs/modules/form.md +0 -495
- data/docs/modules/generator.md +0 -400
- data/docs/modules/index.md +0 -167
- data/docs/modules/interaction.md +0 -642
- data/docs/modules/package.md +0 -151
- data/docs/modules/policy.md +0 -176
- data/docs/modules/portal.md +0 -710
- data/docs/modules/query.md +0 -297
- data/docs/modules/resource_record.md +0 -618
- data/docs/modules/routing.md +0 -690
- data/docs/modules/table.md +0 -301
- data/docs/modules/ui.md +0 -631
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: portal
|
|
3
|
+
description: Plutonium portals - web interfaces with authentication, entity scoping, and routes
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plutonium Portals
|
|
7
|
+
|
|
8
|
+
Portals are Rails engines that provide web interfaces for specific user types.
|
|
9
|
+
|
|
10
|
+
## Creating a Portal
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
rails g pu:pkg:portal dashboard
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Generator Options
|
|
17
|
+
|
|
18
|
+
| Option | Description |
|
|
19
|
+
|--------|-------------|
|
|
20
|
+
| `--auth=NAME` | Rodauth account to authenticate with (e.g., `--auth=user`) |
|
|
21
|
+
| `--public` | Grant public access (no authentication) |
|
|
22
|
+
| `--byo` | Bring your own authentication |
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Non-interactive examples
|
|
26
|
+
rails g pu:pkg:portal admin --auth=admin
|
|
27
|
+
rails g pu:pkg:portal api --public
|
|
28
|
+
rails g pu:pkg:portal custom --byo
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Without flags, the generator prompts interactively:
|
|
32
|
+
- **Rodauth account** - Use existing Rodauth authentication
|
|
33
|
+
- **Public access** - No authentication required
|
|
34
|
+
- **Bring your own** - Implement custom `current_user`
|
|
35
|
+
|
|
36
|
+
## Portal Engine
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# packages/dashboard_portal/lib/engine.rb
|
|
40
|
+
module DashboardPortal
|
|
41
|
+
class Engine < Rails::Engine
|
|
42
|
+
include Plutonium::Portal::Engine
|
|
43
|
+
|
|
44
|
+
config.after_initialize do
|
|
45
|
+
# Optional: multi-tenancy
|
|
46
|
+
scope_to_entity Organization, strategy: :path
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Authentication
|
|
53
|
+
|
|
54
|
+
### Rodauth Integration
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# packages/dashboard_portal/app/controllers/dashboard_portal/concerns/controller.rb
|
|
58
|
+
module DashboardPortal
|
|
59
|
+
module Concerns
|
|
60
|
+
module Controller
|
|
61
|
+
extend ActiveSupport::Concern
|
|
62
|
+
include Plutonium::Portal::Controller
|
|
63
|
+
include Plutonium::Auth::Rodauth(:user) # Use :user account
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Public Access
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
module DashboardPortal
|
|
73
|
+
module Concerns
|
|
74
|
+
module Controller
|
|
75
|
+
extend ActiveSupport::Concern
|
|
76
|
+
include Plutonium::Portal::Controller
|
|
77
|
+
include Plutonium::Auth::Public
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Custom Authentication
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
module DashboardPortal
|
|
87
|
+
module Concerns
|
|
88
|
+
module Controller
|
|
89
|
+
extend ActiveSupport::Concern
|
|
90
|
+
include Plutonium::Portal::Controller
|
|
91
|
+
include Plutonium::Auth::Public
|
|
92
|
+
|
|
93
|
+
def current_user
|
|
94
|
+
@current_user ||= User.find_by(api_key: request.headers["X-API-Key"])
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Entity Scoping (Multi-tenancy)
|
|
102
|
+
|
|
103
|
+
Automatically scope all data to a parent entity.
|
|
104
|
+
|
|
105
|
+
### Path Strategy
|
|
106
|
+
|
|
107
|
+
Entity ID in URL path:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
module AdminPortal
|
|
111
|
+
class Engine < Rails::Engine
|
|
112
|
+
include Plutonium::Portal::Engine
|
|
113
|
+
|
|
114
|
+
config.after_initialize do
|
|
115
|
+
scope_to_entity Organization, strategy: :path
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Routes become: `/organizations/:organization_id/posts`
|
|
122
|
+
|
|
123
|
+
### Custom Strategy
|
|
124
|
+
|
|
125
|
+
Implement your own lookup method:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
module AdminPortal
|
|
129
|
+
class Engine < Rails::Engine
|
|
130
|
+
include Plutonium::Portal::Engine
|
|
131
|
+
|
|
132
|
+
config.after_initialize do
|
|
133
|
+
scope_to_entity Organization, strategy: :current_organization
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# In controller concern
|
|
139
|
+
module AdminPortal
|
|
140
|
+
module Concerns
|
|
141
|
+
module Controller
|
|
142
|
+
extend ActiveSupport::Concern
|
|
143
|
+
include Plutonium::Portal::Controller
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Method name must match strategy
|
|
148
|
+
def current_organization
|
|
149
|
+
@current_organization ||= Organization.find_by!(subdomain: request.subdomain)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Accessing the Scoped Entity
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
current_scoped_entity # The current Organization/Account/etc.
|
|
160
|
+
scoped_to_entity? # true if scoping is active
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Model Requirements
|
|
164
|
+
|
|
165
|
+
Models must have an association path to the scoped entity:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# Direct association (preferred)
|
|
169
|
+
class Post < ResourceRecord
|
|
170
|
+
belongs_to :organization
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Through association
|
|
174
|
+
class Comment < ResourceRecord
|
|
175
|
+
belongs_to :post
|
|
176
|
+
has_one :organization, through: :post
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Complex (define custom scope)
|
|
180
|
+
class AuditLog < ResourceRecord
|
|
181
|
+
scope :associated_with_organization, ->(org) {
|
|
182
|
+
joins(:user).where(users: { organization_id: org.id })
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Routes
|
|
188
|
+
|
|
189
|
+
### Portal Routes
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# packages/dashboard_portal/config/routes.rb
|
|
193
|
+
DashboardPortal::Engine.routes.draw do
|
|
194
|
+
root to: "dashboard#index"
|
|
195
|
+
|
|
196
|
+
# Register resources
|
|
197
|
+
register_resource ::Post
|
|
198
|
+
register_resource Blogging::Comment
|
|
199
|
+
|
|
200
|
+
# Custom routes
|
|
201
|
+
get "settings", to: "settings#index"
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Custom Routes on Resources
|
|
206
|
+
|
|
207
|
+
Add member or collection routes with a block:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
register_resource ::Post do
|
|
211
|
+
member do
|
|
212
|
+
get :preview
|
|
213
|
+
get :analytics
|
|
214
|
+
post :publish
|
|
215
|
+
end
|
|
216
|
+
collection do
|
|
217
|
+
get :archived
|
|
218
|
+
post :bulk_publish
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This generates:
|
|
224
|
+
- `GET /posts/:id/preview`
|
|
225
|
+
- `GET /posts/:id/analytics`
|
|
226
|
+
- `POST /posts/:id/publish`
|
|
227
|
+
- `GET /posts/archived`
|
|
228
|
+
- `POST /posts/bulk_publish`
|
|
229
|
+
|
|
230
|
+
### Mounting in Main App
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# config/routes.rb
|
|
234
|
+
Rails.application.routes.draw do
|
|
235
|
+
# With authentication constraint
|
|
236
|
+
constraints Rodauth::Rails.authenticate(:user) do
|
|
237
|
+
mount DashboardPortal::Engine, at: "/dashboard"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Or without
|
|
241
|
+
mount PublicPortal::Engine, at: "/public"
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Controller Hierarchy
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
::PlutoniumController (app-wide base)
|
|
249
|
+
↓
|
|
250
|
+
::ResourceController (resource handling)
|
|
251
|
+
↓
|
|
252
|
+
DashboardPortal::ResourceController (portal base)
|
|
253
|
+
↓
|
|
254
|
+
DashboardPortal::PostsController (resource-specific)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Portal Controllers
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# packages/dashboard_portal/app/controllers/dashboard_portal/resource_controller.rb
|
|
261
|
+
module DashboardPortal
|
|
262
|
+
class ResourceController < ::ResourceController
|
|
263
|
+
include DashboardPortal::Concerns::Controller
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Dynamic Controllers
|
|
269
|
+
|
|
270
|
+
Controllers are auto-created if not defined. When accessing `DashboardPortal::PostsController`:
|
|
271
|
+
|
|
272
|
+
1. If file exists, use it
|
|
273
|
+
2. Otherwise, dynamically create inheriting from `::PostsController`
|
|
274
|
+
3. Include `DashboardPortal::Concerns::Controller`
|
|
275
|
+
|
|
276
|
+
## Portal-Specific Overrides
|
|
277
|
+
|
|
278
|
+
### Override Definition
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
# packages/dashboard_portal/app/definitions/dashboard_portal/post_definition.rb
|
|
282
|
+
class DashboardPortal::PostDefinition < ::PostDefinition
|
|
283
|
+
# Hide certain actions from this portal
|
|
284
|
+
# Add portal-specific scopes
|
|
285
|
+
scope :my_posts, -> { where(user: current_user) }
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Override Policy
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
# packages/dashboard_portal/app/policies/dashboard_portal/post_policy.rb
|
|
293
|
+
class DashboardPortal::PostPolicy < ::PostPolicy
|
|
294
|
+
include DashboardPortal::ResourcePolicy
|
|
295
|
+
|
|
296
|
+
def destroy?
|
|
297
|
+
false # No deletion in user portal
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def permitted_attributes_for_create
|
|
301
|
+
%i[title content] # Fewer fields than admin
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Override Controller
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# packages/dashboard_portal/app/controllers/dashboard_portal/posts_controller.rb
|
|
310
|
+
module DashboardPortal
|
|
311
|
+
class PostsController < ResourceController
|
|
312
|
+
private
|
|
313
|
+
|
|
314
|
+
def preferred_action_after_submit
|
|
315
|
+
"index"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Layout and Views
|
|
322
|
+
|
|
323
|
+
### Portal Layout
|
|
324
|
+
|
|
325
|
+
```erb
|
|
326
|
+
<!-- packages/dashboard_portal/app/views/layouts/dashboard_portal.html.erb -->
|
|
327
|
+
<!DOCTYPE html>
|
|
328
|
+
<html>
|
|
329
|
+
<head>
|
|
330
|
+
<title>Dashboard</title>
|
|
331
|
+
<%= csrf_meta_tags %>
|
|
332
|
+
<%= stylesheet_link_tag "application" %>
|
|
333
|
+
</head>
|
|
334
|
+
<body>
|
|
335
|
+
<nav><!-- Portal navigation --></nav>
|
|
336
|
+
<main><%= yield %></main>
|
|
337
|
+
</body>
|
|
338
|
+
</html>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Dashboard Controller
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
# packages/dashboard_portal/app/controllers/dashboard_portal/dashboard_controller.rb
|
|
345
|
+
module DashboardPortal
|
|
346
|
+
class DashboardController < PlutoniumController
|
|
347
|
+
def index
|
|
348
|
+
# Dashboard home page
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Multiple Portals Example
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# Admin portal - full access
|
|
358
|
+
module AdminPortal
|
|
359
|
+
class Engine < Rails::Engine
|
|
360
|
+
include Plutonium::Portal::Engine
|
|
361
|
+
|
|
362
|
+
config.after_initialize do
|
|
363
|
+
scope_to_entity Organization, strategy: :path
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# User dashboard - limited access
|
|
369
|
+
module DashboardPortal
|
|
370
|
+
class Engine < Rails::Engine
|
|
371
|
+
include Plutonium::Portal::Engine
|
|
372
|
+
|
|
373
|
+
config.after_initialize do
|
|
374
|
+
scope_to_entity Organization, strategy: :path
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Public portal - read-only, no auth
|
|
380
|
+
module PublicPortal
|
|
381
|
+
class Engine < Rails::Engine
|
|
382
|
+
include Plutonium::Portal::Engine
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Each portal can:
|
|
388
|
+
- Have different authentication
|
|
389
|
+
- Show different fields
|
|
390
|
+
- Allow different actions
|
|
391
|
+
- Use different layouts
|
|
392
|
+
|
|
393
|
+
## Related Skills
|
|
394
|
+
|
|
395
|
+
- `package` - Package overview (features vs portals)
|
|
396
|
+
- `rodauth` - Authentication setup and configuration
|
|
397
|
+
- `connect-resource` - Connecting resources to portals
|
|
398
|
+
- `policy` - Portal-specific policies
|
|
399
|
+
- `definition` - Portal-specific definitions
|
|
400
|
+
- `controller` - Portal-specific controllers
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resource
|
|
3
|
+
description: Overview of Plutonium resources - what they are and how the pieces fit together
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plutonium Resources
|
|
7
|
+
|
|
8
|
+
A **resource** in Plutonium is the combination of four layers that work together to provide full CRUD functionality with minimal code.
|
|
9
|
+
|
|
10
|
+
## The Four Layers
|
|
11
|
+
|
|
12
|
+
| Layer | File | Purpose |
|
|
13
|
+
|-------|------|---------|
|
|
14
|
+
| **Model** | `app/models/post.rb` | Data, validations, associations, business rules |
|
|
15
|
+
| **Definition** | `app/definitions/post_definition.rb` | UI configuration - how fields render, actions, filters |
|
|
16
|
+
| **Policy** | `app/policies/post_policy.rb` | Authorization - who can do what |
|
|
17
|
+
| **Controller** | `app/controllers/posts_controller.rb` | Request handling - usually empty, inherits CRUD |
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
21
|
+
│ Resource │
|
|
22
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
23
|
+
│ Model │ Definition │ Policy │ Controller │
|
|
24
|
+
│ (WHAT it is) │ (HOW it looks) │ (WHO can act) │ (HOW it │
|
|
25
|
+
│ │ │ │ responds) │
|
|
26
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
27
|
+
│ - attributes │ - field types │ - permissions │ - CRUD │
|
|
28
|
+
│ - associations │ - inputs/forms │ - scoping │ - redirects│
|
|
29
|
+
│ - validations │ - displays │ - attributes │ - params │
|
|
30
|
+
│ - scopes │ - actions │ │ │
|
|
31
|
+
│ - callbacks │ - filters │ │ │
|
|
32
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Creating Resources
|
|
36
|
+
|
|
37
|
+
### New Resources (from scratch)
|
|
38
|
+
|
|
39
|
+
Use the scaffold generator to create all four layers at once:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
rails g pu:res:scaffold Post title:string content:text:required published:boolean
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This generates:
|
|
46
|
+
- `app/models/post.rb` - Model with validations
|
|
47
|
+
- `app/definitions/post_definition.rb` - Definition (empty, uses auto-detection)
|
|
48
|
+
- `app/policies/post_policy.rb` - Policy with sensible defaults
|
|
49
|
+
- `app/controllers/posts_controller.rb` - Controller (empty, inherits CRUD)
|
|
50
|
+
- Migration file
|
|
51
|
+
|
|
52
|
+
See `create-resource` skill for full generator options.
|
|
53
|
+
|
|
54
|
+
### From Existing Models
|
|
55
|
+
|
|
56
|
+
For existing Rails projects, you can convert models to Plutonium resources:
|
|
57
|
+
|
|
58
|
+
1. **Include the module** in your model:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
class Post < ApplicationRecord
|
|
62
|
+
include Plutonium::Resource::Record
|
|
63
|
+
# Your existing code...
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or inherit from a base class that includes it:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
class Post < ResourceRecord
|
|
71
|
+
# Your existing code...
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
2. **Generate the supporting files** (definition, policy, controller):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
rails g pu:res:scaffold Post --no-migration
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This creates definition, policy, and controller without touching your existing model.
|
|
82
|
+
|
|
83
|
+
3. **Connect to a portal**:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
rails g pu:res:conn Post --dest=admin_portal
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Connecting to Portals
|
|
90
|
+
|
|
91
|
+
Resources must be connected to a portal to be accessible:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
rails g pu:res:conn Post --portal AdminPortal
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This:
|
|
98
|
+
- Registers the resource in the portal
|
|
99
|
+
- Creates a route
|
|
100
|
+
- Optionally creates portal-specific definition override
|
|
101
|
+
|
|
102
|
+
See `connect-resource` skill for details.
|
|
103
|
+
|
|
104
|
+
## Layer Responsibilities
|
|
105
|
+
|
|
106
|
+
### Model (Data Layer)
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class Post < ResourceRecord
|
|
110
|
+
belongs_to :author, class_name: "User"
|
|
111
|
+
has_many :comments
|
|
112
|
+
|
|
113
|
+
validates :title, presence: true
|
|
114
|
+
|
|
115
|
+
scope :published, -> { where(published: true) }
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The model handles:
|
|
120
|
+
- Database schema and associations
|
|
121
|
+
- Data validation
|
|
122
|
+
- Business logic scopes
|
|
123
|
+
- Callbacks
|
|
124
|
+
|
|
125
|
+
**Skills:** `model`, `model-features`
|
|
126
|
+
|
|
127
|
+
### Definition (UI Layer)
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
class PostDefinition < ResourceDefinition
|
|
131
|
+
# Override auto-detected field types
|
|
132
|
+
input :content, as: :rich_text
|
|
133
|
+
|
|
134
|
+
# Add filters and scopes
|
|
135
|
+
filter :published, with: Plutonium::Query::Filters::Boolean
|
|
136
|
+
scope :published
|
|
137
|
+
|
|
138
|
+
# Add actions
|
|
139
|
+
action :publish, interaction: PublishPostInteraction
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The definition handles:
|
|
144
|
+
- Field type overrides (auto-detection handles most cases)
|
|
145
|
+
- Form input customization
|
|
146
|
+
- Display formatting
|
|
147
|
+
- Search, filters, scopes, sorting
|
|
148
|
+
- Actions (interactive operations)
|
|
149
|
+
|
|
150
|
+
**Skills:** `definition`, `definition-fields`, `definition-actions`, `definition-query`
|
|
151
|
+
|
|
152
|
+
### Policy (Authorization Layer)
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class PostPolicy < ResourcePolicy
|
|
156
|
+
# Who can perform actions
|
|
157
|
+
def create?
|
|
158
|
+
user.present?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def read?
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def publish?
|
|
166
|
+
user.admin? || record.author == user
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# What records are visible
|
|
170
|
+
relation_scope do |relation|
|
|
171
|
+
return relation if user.admin?
|
|
172
|
+
relation.where(author: user)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# What attributes are readable/writable
|
|
176
|
+
def permitted_attributes_for_read
|
|
177
|
+
%i[title content published author created_at]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def permitted_attributes_for_create
|
|
181
|
+
%i[title content]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The policy handles:
|
|
187
|
+
- Action authorization (create?, update?, destroy?, custom actions)
|
|
188
|
+
- Resource scoping (what records user can see)
|
|
189
|
+
- Attribute permissions (read/write access per field)
|
|
190
|
+
|
|
191
|
+
**Skill:** `policy`
|
|
192
|
+
|
|
193
|
+
### Controller (Request Layer)
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class PostsController < ::ResourceController
|
|
197
|
+
# Empty - all CRUD actions inherited automatically
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Controllers are usually empty because they inherit full CRUD functionality. Customize only when needed:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
class PostsController < ::ResourceController
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
def preferred_action_after_submit
|
|
208
|
+
"index" # Redirect to list instead of show
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The controller handles:
|
|
214
|
+
- Request/response cycle
|
|
215
|
+
- Redirect logic
|
|
216
|
+
- Custom parameter processing
|
|
217
|
+
- Non-standard authorization flows
|
|
218
|
+
|
|
219
|
+
**Skill:** `controller`
|
|
220
|
+
|
|
221
|
+
## Auto-Detection
|
|
222
|
+
|
|
223
|
+
Plutonium automatically detects from your model:
|
|
224
|
+
- All database columns with appropriate field types
|
|
225
|
+
- Associations (belongs_to, has_one, has_many)
|
|
226
|
+
- Attachments (Active Storage)
|
|
227
|
+
- Enums
|
|
228
|
+
|
|
229
|
+
**You only need to declare when overriding defaults.**
|
|
230
|
+
|
|
231
|
+
## Portal-Specific Customization
|
|
232
|
+
|
|
233
|
+
Each portal can have its own definition that overrides the base:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
# Base definition
|
|
237
|
+
class PostDefinition < ResourceDefinition
|
|
238
|
+
scope :published
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Admin portal sees more
|
|
242
|
+
class AdminPortal::PostDefinition < ::PostDefinition
|
|
243
|
+
scope :draft
|
|
244
|
+
scope :pending_review
|
|
245
|
+
action :feature, interaction: FeaturePostInteraction
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Public portal is restricted
|
|
249
|
+
class PublicPortal::PostDefinition < ::PostDefinition
|
|
250
|
+
# Only published scope, no actions
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Workflow Summary
|
|
255
|
+
|
|
256
|
+
1. **Generate** - `rails g pu:res:scaffold Model attributes...`
|
|
257
|
+
2. **Connect** - `rails g pu:res:conn Model --portal PortalName`
|
|
258
|
+
3. **Customize** - Edit definition/policy as needed (model rarely needs changes)
|
|
259
|
+
4. **Override per portal** - Create portal-specific definitions when needed
|
|
260
|
+
|
|
261
|
+
## Related Skills
|
|
262
|
+
|
|
263
|
+
- `model` - Model structure and organization
|
|
264
|
+
- `model-features` - has_cents, associations, scopes, routes
|
|
265
|
+
- `definition` - Definition overview and structure
|
|
266
|
+
- `definition-fields` - Fields, inputs, displays, columns
|
|
267
|
+
- `definition-actions` - Actions and interactions
|
|
268
|
+
- `interaction` - Writing interaction classes
|
|
269
|
+
- `definition-query` - Search, filters, scopes, sorting
|
|
270
|
+
- `policy` - Authorization and permissions
|
|
271
|
+
- `controller` - Controller customization
|
|
272
|
+
- `views` - Custom pages, displays, tables using Phlex
|
|
273
|
+
- `forms` - Custom form templates and field builders
|
|
274
|
+
- `assets` - TailwindCSS and component theming
|
|
275
|
+
- `package` - Feature and portal packages
|
|
276
|
+
- `portal` - Portal configuration and entity scoping
|
|
277
|
+
- `nested-resources` - Parent/child routes and scoping
|
|
278
|
+
- `installation` - Setting up Plutonium
|
|
279
|
+
- `rodauth` - Authentication setup
|
|
280
|
+
- `create-resource` - Scaffold generator details
|
|
281
|
+
- `connect-resource` - Portal connection details
|