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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -1,256 +0,0 @@
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.
@@ -1,390 +0,0 @@
1
- # Working with Resources
2
-
3
- ::: tip What you'll learn
4
- - How to create and manage resources in Plutonium
5
- - Understanding resource definitions and configurations
6
- - Working with fields, associations, and nested resources
7
- - Implementing resource policies and scoping
8
- - Best practices for resource organization
9
- :::
10
-
11
- ## Introduction
12
-
13
- Resources are the core building blocks of a Plutonium application. A resource represents a model in your application that can be managed through a consistent interface, complete with views, controllers, and policies.
14
-
15
- ## Creating a Resource
16
-
17
- The fastest way to create a resource is using the scaffold generator:
18
-
19
- ```bash
20
- rails generate pu:res:scaffold Blog user:belongs_to \
21
- title:string content:text 'published_at:datetime?'
22
- ```
23
-
24
- This generates several files, including:
25
-
26
- ::: code-group
27
- ```ruby [app/models/blog.rb]
28
- class Blog < ResourceRecord
29
- belongs_to :user
30
- end
31
- ```
32
-
33
- ```ruby [app/policies/blog_policy.rb]
34
- class BlogPolicy < Plutonium::Resource::Policy
35
- def create?
36
- true
37
- end
38
-
39
- def read?
40
- true
41
- end
42
-
43
- def permitted_attributes_for_create
44
- %i[user title content published_at]
45
- end
46
-
47
- def permitted_attributes_for_read
48
- %i[user title content published_at]
49
- end
50
- end
51
- ```
52
-
53
- ```ruby [app/definitions/blog_definition.rb]
54
- class BlogDefinition < Plutonium::Resource::Definition
55
- end
56
- ```
57
- :::
58
-
59
- ## Resource Definitions
60
-
61
- Resource definitions customize how a resource behaves in your application. They define:
62
- - Fields and their types
63
- - Available actions
64
- - Search and filtering capabilities
65
- - Sorting options
66
- - Display configurations
67
-
68
- ### Basic Field Configuration
69
-
70
- ::: code-group
71
- ```ruby [Simple Fields]
72
- class BlogDefinition < Plutonium::Resource::Definition
73
- # Basic field definitions
74
- field :content, as: :text
75
-
76
- # Field with custom display options
77
- field :published_at,
78
- as: :datetime,
79
- hint: "When this post should be published"
80
- end
81
- ```
82
-
83
- ```ruby [Custom Displays]
84
- class BlogDefinition < Plutonium::Resource::Definition
85
- # Customize how fields are displayed
86
- display :title, wrapper: {class: "col-span-full"}
87
- display :content, wrapper: {class: "col-span-full"} do |f|
88
- f.text_tag class: "format dark:format-invert"
89
- end
90
-
91
- # Custom column display in tables
92
- column :published_at, align: :end
93
- end
94
- ```
95
- :::
96
-
97
- <!--
98
- ### Working with Associations
99
-
100
- Plutonium makes it easy to work with Rails associations:
101
-
102
- ```ruby
103
- class BlogDefinition < Plutonium::Resource::Definition
104
- # Define belongs_to association
105
- field :user, as: :belongs_to
106
-
107
- # Has-many association with inline creation
108
- field :comments do |f|
109
- f.has_many_tag nested: true
110
- end
111
-
112
- # Configure nested attributes
113
- define_nested_input :comments,
114
- inputs: %i[content user],
115
- limit: 3 do |input|
116
- input.define_field_input :content,
117
- type: :text,
118
- hint: "Keep it constructive"
119
- end
120
- end
121
- ```
122
- -->
123
-
124
- ### Adding Custom Actions
125
-
126
- Beyond CRUD, you can add custom actions to your resources:
127
-
128
- ```ruby
129
- # app/interactions/blogs/publish.rb
130
- module Blogs
131
- class Publish < Plutonium::Resource::Interaction
132
- # Define what this interaction accepts
133
- attribute :resource, class: Blog
134
- attribute :publish_date, :date, default: -> { Time.current }
135
-
136
- presents label: "Publish Blog",
137
- icon: Phlex::TablerIcons::Send,
138
- description: "Make this blog post public"
139
-
140
- private
141
-
142
- def execute
143
- if resource.update(
144
- published_at: publish_date
145
- )
146
- succeed(resource)
147
- .with_message("Blog post published successfully")
148
- .with_redirect_response(resource)
149
- else
150
- failed(resource.errors)
151
- end
152
- end
153
- end
154
- end
155
-
156
- # app/definitions/blog_definition.rb
157
- class BlogDefinition < Plutonium::Resource::Definition
158
- # Register the custom action
159
- action :publish,
160
- interaction: Blogs::Publish,
161
- category: :primary
162
- end
163
- ```
164
-
165
- ### Search and Filtering
166
-
167
- Add search and filtering capabilities to your resources:
168
-
169
- ```ruby
170
- class BlogDefinition < Plutonium::Resource::Definition
171
- # Enable full-text search
172
- search do |scope, query|
173
- scope.where("title ILIKE ? OR content ILIKE ?",
174
- "%#{query}%", "%#{query}%")
175
- end
176
-
177
- # Add filters
178
- filter :published_at,
179
- with: DateFilter,
180
- predicate: :gteq
181
-
182
- # Add scopes
183
- scope :published do |scope|
184
- scope.where.not(published_at: nil)
185
- end
186
- scope :draft do |scope|
187
- scope.where(published_at: nil)
188
- end
189
-
190
- # Configure sorting
191
- sort :title
192
- sort :published_at
193
-
194
- # Configure default sorting (newest first)
195
- default_sort :published_at, :desc
196
- end
197
- ```
198
-
199
- ## Resource Policies
200
-
201
- Policies control access to your resources:
202
-
203
- ```ruby
204
- class BlogPolicy < Plutonium::Resource::Policy
205
- def permitted_attributes_for_create
206
- %i[title content state published_at user_id]
207
- end
208
-
209
- def permitted_associations
210
- %i[comments]
211
- end
212
-
213
- def create?
214
- # Allow logged in users to create blogs
215
- user.present?
216
- end
217
-
218
- def update?
219
- # Users can only edit their own blogs
220
- record.user_id == user.id
221
- end
222
-
223
- def publish?
224
- # Only editors can publish
225
- user.editor? && record.draft?
226
- end
227
-
228
- # Scope visible records
229
- relation_scope do |relation|
230
- relation = super(relation)
231
- next relation unless user.admin?
232
-
233
- relation.with_deleted
234
- end
235
- end
236
- ```
237
-
238
- ## Best Practices
239
-
240
- ::: tip Resource Organization
241
- 1. Keep resource definitions focused and cohesive
242
- 2. Use packages to organize related resources
243
- 3. Leverage policy scopes for authorization
244
- 4. Extract complex logic into interactions
245
- 5. Use presenters for view-specific logic
246
- :::
247
-
248
- ::: warning Common Pitfalls
249
- - Avoid putting business logic in definitions
250
- - Don't bypass policy checks
251
- - Remember to scope resources appropriately
252
- - Test your interactions and policies
253
- :::
254
-
255
- # Deep Dive: Building a Resource
256
-
257
- In Plutonium, a **Resource** is the central concept for managing your application's data. It's more than just a model—it's a complete package that includes the model, controller, policy, views, and all the configuration that ties them together.
258
-
259
- This guide will walk you through building a complete `Post` resource from scratch, demonstrating how Plutonium's different modules work together to create a powerful and consistent user experience.
260
-
261
- ## 1. Generating the Resource
262
-
263
- We'll start with the scaffold generator, which creates all the necessary files for our `Post` resource.
264
-
265
- ```bash
266
- rails generate pu:res:scaffold Post user:belongs_to title:string content:text published_at:datetime
267
- ```
268
-
269
- This command generates:
270
- - A `Post` model with the specified attributes and a `belongs_to :user` association.
271
- - A `PostsController`.
272
- - A `PostPolicy` with basic permissions.
273
- - A `PostDefinition` file, which will be the focus of this guide.
274
-
275
- ## 2. Configuring Display & Forms (The Definition File)
276
-
277
- The **Definition** file (`app/definitions/post_definition.rb`) is where you declaratively configure how your resource is displayed and edited. Let's start by defining the fields for our table, detail page, and form.
278
-
279
- ::: code-group
280
- ```ruby [app/definitions/post_definition.rb]
281
- class PostDefinition < Plutonium::Resource::Definition
282
- # Configure the table (index view)
283
- column :user, label: "Author"
284
- column :published_at, as: :datetime
285
-
286
- # Configure the detail page (show view)
287
- display :user, label: "Author"
288
- display :published_at, as: :datetime
289
- display :content, as: :rich_text
290
-
291
- # Configure the form (new/edit views)
292
- input :user, as: :select, label: "Author" # Explicitly use a select input
293
- input :content, as: :rich_text
294
- end
295
- ```
296
- ```ruby [app/policies/post_policy.rb]
297
- # In the policy, we must permit these attributes to be read and written.
298
- class PostPolicy < Plutonium::Resource::Policy
299
- # ...
300
-
301
- def permitted_attributes_for_read
302
- [:title, :user, :published_at, :content]
303
- end
304
-
305
- def permitted_attributes_for_create
306
- [:title, :user_id, :content]
307
- end
308
-
309
- def permitted_attributes_for_update
310
- permitted_attributes_for_create
311
- end
312
- end
313
- ```
314
- :::
315
-
316
- Here, we've used the `display` helper to control the `index` and `show` views, and the `input` helper for the forms. We've also specified `:rich_text` to get a WYSIWYG editor for our content. Notice that we also had to permit these attributes in the policy.
317
-
318
- ## 3. Adding a Custom Action
319
-
320
- Standard CRUD is great, but most applications have custom business logic. Let's add a "Publish" action. This involves creating an **Interaction** for the logic and registering it in the definition.
321
-
322
- ::: code-group
323
- ```ruby [app/interactions/post_interactions/publish.rb]
324
- module PostInteractions
325
- class Publish < Plutonium::Resource::Interaction
326
- attribute :resource, class: "Post"
327
-
328
- private
329
-
330
- def execute
331
- resource.update(published_at: Time.current)
332
- succeed(resource).with_message("Post was successfully published.")
333
- end
334
- end
335
- end
336
- ```
337
- ```ruby [app/definitions/post_definition.rb]
338
- class PostDefinition < Plutonium::Resource::Definition
339
- # ... (display and input helpers)
340
-
341
- action :publish,
342
- interaction: "PostInteractions::Publish",
343
- category: :primary
344
- end
345
- ```
346
- ```ruby [app/policies/post_policy.rb]
347
- class PostPolicy < Plutonium::Resource::Policy
348
- # ... (attribute permissions)
349
-
350
- # An action is only visible if its policy returns true.
351
- def publish?
352
- # Only show the publish button if the post is not yet published.
353
- update? && record.published_at.nil?
354
- end
355
- end
356
- ```
357
- :::
358
-
359
- We now have a "Publish" button on our `Post` detail page that only appears when appropriate, thanks to the combination of the Interaction, Definition, and Policy.
360
-
361
- ## 4. Configuring Search, Filters, and Sorting
362
-
363
- To make our resource table more useful, let's add search, filtering, and sorting capabilities. This is all handled declaratively in the definition file.
364
-
365
- ```ruby
366
- # app/definitions/post_definition.rb
367
- class PostDefinition < Plutonium::Resource::Definition
368
- # ... (display, input, and action helpers)
369
-
370
- # Enable full-text search across title and content
371
- search do |scope, query|
372
- scope.where("title ILIKE :q OR content ILIKE :q", q: "%#{query}%")
373
- end
374
-
375
- # Add filters to the sidebar
376
- filter :published, with: ->(scope, value) { value ? scope.where.not(published_at: nil) : scope.where(published_at: nil) }, as: :boolean
377
- filter :user, as: :select, choices: User.pluck(:name, :id)
378
-
379
- # Define named scopes that appear as buttons
380
- scope :all
381
- scope :published, -> { where.not(published_at: nil) }
382
- scope :drafts, -> { where(published_at: nil) }
383
-
384
- # Configure which columns are sortable
385
- sort :title
386
- sort :published_at
387
- end
388
- ```
389
-
390
- With just a few lines of code, we now have a powerful and interactive table view for our posts, complete with a search bar, filter sidebar, scope buttons, and sortable columns. This demonstrates how the **Resource** module integrates seamlessly with the **Query** module.