plutonium 0.50.0 → 0.51.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/.claude/skills/plutonium/SKILL.md +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +230 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +56 -49
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +117 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +11 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plutonium-entity-scoping
|
|
3
|
-
description: Use BEFORE writing relation_scope, associated_with, scoping a model to a tenant, or any multi-tenancy work. Also when configuring entity strategies on a portal. The single source of truth for Plutonium entity scoping.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Plutonium Entity Scoping
|
|
7
|
-
|
|
8
|
-
The single source of truth for how Plutonium scopes records to a tenant/entity in multi-tenant apps. Entity scoping spans models, policies, portals, and invites — this skill consolidates the canonical rules so you don't have to stitch them together from four other skills.
|
|
9
|
-
|
|
10
|
-
## 🚨 Critical (read first)
|
|
11
|
-
|
|
12
|
-
- **Never bypass `default_relation_scope`.** Overriding `relation_scope` with `where(organization: ...)` or manual joins to the entity skips Plutonium's scoping and triggers `verify_default_relation_scope_applied!`. Always call `default_relation_scope(relation)` explicitly (not `super`).
|
|
13
|
-
- **Always declare an association path from the model to the entity.** If `associated_with` can't find a path — direct `belongs_to`, `has_one :through`, or a custom `associated_with_<entity>` scope — Plutonium raises. Fix the **model**, not the policy.
|
|
14
|
-
- **Use a generator to scaffold scoped resources.** `pu:saas:setup`, `pu:pkg:portal --scope=Entity`, and `pu:res:scaffold` do the right thing. Hand-wiring scoping is how leaks happen.
|
|
15
|
-
- **Parent scoping beats entity scoping.** When a parent is present (nested resource), `default_relation_scope` scopes via the parent, not via `entity_scope`. Don't double-scope.
|
|
16
|
-
- **Related skills:** `plutonium-model` (associations, `associated_with`), `plutonium-policy` (`relation_scope` overrides), `plutonium-portal` (entity strategies), `plutonium-invites` (membership-backed scoping).
|
|
17
|
-
|
|
18
|
-
## Quick checklist
|
|
19
|
-
|
|
20
|
-
Scoping a new model to a tenant:
|
|
21
|
-
|
|
22
|
-
1. Pick the shape: direct child, join table, or grandchild (see [Three model shapes](#three-model-shapes)).
|
|
23
|
-
2. Declare the association path on the model (`belongs_to`, `has_one :through`, or a custom `associated_with_<entity>` scope).
|
|
24
|
-
3. Verify `Model.associated_with(entity)` returns the right records in `rails runner`.
|
|
25
|
-
4. Confirm the portal is scoped: `scope_to_entity Entity, strategy: :path` (or custom) in the portal engine.
|
|
26
|
-
5. Leave `relation_scope` alone in the policy unless you need **extra** filters on top of the default.
|
|
27
|
-
6. If you do override `relation_scope`, wrap with `default_relation_scope(relation).where(...)`.
|
|
28
|
-
7. Add compound uniqueness scoped to the entity on the model (`validates :code, uniqueness: {scope: :organization_id}`).
|
|
29
|
-
8. Test: create a record in org A, confirm it does NOT appear when scoped to org B.
|
|
30
|
-
|
|
31
|
-
## How entity scoping works
|
|
32
|
-
|
|
33
|
-
Plutonium's entity scoping is built on three cooperating pieces:
|
|
34
|
-
|
|
35
|
-
- **Portal**: declares which entity class it scopes to (`scope_to_entity Organization, strategy: :path`) and how to resolve the current entity from the request.
|
|
36
|
-
- **Policy**: `default_relation_scope(relation)` calls `relation.associated_with(entity_scope)`, applying the scope to every collection query.
|
|
37
|
-
- **Model**: `associated_with(entity)` resolves the scope via a custom scope, a direct association, or auto-detected `has_one :through` chain.
|
|
38
|
-
|
|
39
|
-
The `default_relation_scope` is enforced — if you override `relation_scope` without calling it, `verify_default_relation_scope_applied!` raises at runtime.
|
|
40
|
-
|
|
41
|
-
## `associated_with` resolution
|
|
42
|
-
|
|
43
|
-
`Model.associated_with(entity)` resolves in this order:
|
|
44
|
-
|
|
45
|
-
1. **Custom named scope** `associated_with_<model_name>` (e.g. `associated_with_organization`) — highest priority, full control over the SQL.
|
|
46
|
-
2. **Direct `belongs_to` to the entity class** — `WHERE <entity>_id = ?`, most efficient.
|
|
47
|
-
3. **`has_one` / `has_one :through` to the entity class** — JOIN + WHERE, auto-detected via `reflect_on_all_associations`.
|
|
48
|
-
4. **Reverse `has_many` from the entity** — JOIN required, logs a warning (less efficient).
|
|
49
|
-
|
|
50
|
-
If none apply, raises:
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
Could not resolve the association between 'Model' and 'Entity'
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
with guidance to either add an association or define the custom scope.
|
|
57
|
-
|
|
58
|
-
## `default_relation_scope` and safe `relation_scope` overrides
|
|
59
|
-
|
|
60
|
-
`default_relation_scope(relation)` does two things:
|
|
61
|
-
|
|
62
|
-
1. If a **parent** is present (nested resource), scopes the relation via the parent association.
|
|
63
|
-
2. Otherwise, applies `relation.associated_with(entity_scope)`.
|
|
64
|
-
|
|
65
|
-
### Correct overrides
|
|
66
|
-
|
|
67
|
-
```ruby
|
|
68
|
-
# ✅ Best: don't override at all — the inherited scope already calls default_relation_scope.
|
|
69
|
-
|
|
70
|
-
# ✅ Add extra filters on top of default scope
|
|
71
|
-
relation_scope do |relation|
|
|
72
|
-
default_relation_scope(relation).where(archived: false)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# ✅ Role-based extra filter
|
|
76
|
-
relation_scope do |relation|
|
|
77
|
-
relation = default_relation_scope(relation)
|
|
78
|
-
user.admin? ? relation : relation.where(author: user)
|
|
79
|
-
end
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Wrong overrides
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
# ❌ Manually filtering by the scoped entity — bypasses default_relation_scope
|
|
86
|
-
relation_scope do |relation|
|
|
87
|
-
relation.where(organization: current_scoped_entity)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# ❌ Manual joins — same problem
|
|
91
|
-
relation_scope do |relation|
|
|
92
|
-
relation.joins(:project).where(projects: {organization_id: current_scoped_entity.id})
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# ❌ Missing default_relation_scope entirely — raises at runtime
|
|
96
|
-
relation_scope do |relation|
|
|
97
|
-
relation.where(published: true)
|
|
98
|
-
end
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
**Do not rely on `super`** from inside `relation_scope do ... end`. `default_relation_scope` is the documented public contract; `super` semantics depend on how ActionPolicy's DSL registered the scope and aren't guaranteed.
|
|
102
|
-
|
|
103
|
-
### Intentionally skipping the scope
|
|
104
|
-
|
|
105
|
-
Rare, but possible:
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
relation_scope do |relation|
|
|
109
|
-
skip_default_relation_scope!
|
|
110
|
-
relation
|
|
111
|
-
end
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Before reaching for this, consider a separate portal without scoping.
|
|
115
|
-
|
|
116
|
-
## Entity strategies (portal configuration)
|
|
117
|
-
|
|
118
|
-
The portal declares how the current entity is resolved from the request.
|
|
119
|
-
|
|
120
|
-
### Path strategy
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
module AdminPortal
|
|
124
|
-
class Engine < Rails::Engine
|
|
125
|
-
include Plutonium::Portal::Engine
|
|
126
|
-
|
|
127
|
-
config.after_initialize do
|
|
128
|
-
scope_to_entity Organization, strategy: :path
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Routes become `/organizations/:organization_id/posts`. The portal extracts `params[:organization_id]` and loads the entity automatically.
|
|
135
|
-
|
|
136
|
-
### Custom strategy
|
|
137
|
-
|
|
138
|
-
```ruby
|
|
139
|
-
module AdminPortal
|
|
140
|
-
class Engine < Rails::Engine
|
|
141
|
-
include Plutonium::Portal::Engine
|
|
142
|
-
|
|
143
|
-
config.after_initialize do
|
|
144
|
-
scope_to_entity Organization, strategy: :current_organization
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
module AdminPortal
|
|
150
|
-
module Concerns
|
|
151
|
-
module Controller
|
|
152
|
-
extend ActiveSupport::Concern
|
|
153
|
-
include Plutonium::Portal::Controller
|
|
154
|
-
|
|
155
|
-
private
|
|
156
|
-
|
|
157
|
-
def current_organization
|
|
158
|
-
@current_organization ||= Organization.find_by!(subdomain: request.subdomain)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
The strategy symbol must match a method name on the controller.
|
|
166
|
-
|
|
167
|
-
### Accessing the scoped entity
|
|
168
|
-
|
|
169
|
-
```ruby
|
|
170
|
-
current_scoped_entity # => current Organization
|
|
171
|
-
scoped_to_entity? # => true/false
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Inside a policy, the same entity is available as `entity_scope`.
|
|
175
|
-
|
|
176
|
-
## Three model shapes
|
|
177
|
-
|
|
178
|
-
The `associated_with` resolver handles three common model shapes. Pick the lightest one that fits.
|
|
179
|
-
|
|
180
|
-
### Shape 1: Direct child (belongs_to the entity)
|
|
181
|
-
|
|
182
|
-
```ruby
|
|
183
|
-
class Organization < ResourceRecord
|
|
184
|
-
has_many :projects
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
class Project < ResourceRecord
|
|
188
|
-
belongs_to :organization
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Usage
|
|
192
|
-
Project.associated_with(org)
|
|
193
|
-
# => Project.where(organization: org) # simple WHERE, most efficient
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
**When to use:** the model naturally has a direct foreign key to the entity. No extra work; auto-detected.
|
|
197
|
-
|
|
198
|
-
### Shape 2: Join table (membership-style)
|
|
199
|
-
|
|
200
|
-
A join table linking users to entities, where the entity is reachable via one of the `belongs_to`:
|
|
201
|
-
|
|
202
|
-
```ruby
|
|
203
|
-
class User < ResourceRecord
|
|
204
|
-
has_many :memberships
|
|
205
|
-
has_many :organizations, through: :memberships
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
class Organization < ResourceRecord
|
|
209
|
-
has_many :memberships
|
|
210
|
-
has_many :users, through: :memberships
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
class Membership < ResourceRecord
|
|
214
|
-
belongs_to :user
|
|
215
|
-
belongs_to :organization
|
|
216
|
-
|
|
217
|
-
# ← auto-detection already finds :organization via belongs_to
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Usage
|
|
221
|
-
Membership.associated_with(org)
|
|
222
|
-
# => Membership.where(organization: org)
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
**When to use:** a pure join table. The `belongs_to :organization` is sufficient.
|
|
226
|
-
|
|
227
|
-
If instead the join table is the scope target and you want to scope `Project` → `Membership` → `Organization`, add a `has_one :through`:
|
|
228
|
-
|
|
229
|
-
```ruby
|
|
230
|
-
class ProjectMember < ResourceRecord
|
|
231
|
-
belongs_to :project
|
|
232
|
-
belongs_to :user
|
|
233
|
-
has_one :organization, through: :project # ← enables auto-scoping
|
|
234
|
-
end
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
Now `ProjectMember.associated_with(org)` resolves via the `has_one :through` automatically.
|
|
238
|
-
|
|
239
|
-
### Shape 3: Grandchild (multiple hops via `has_one :through`)
|
|
240
|
-
|
|
241
|
-
```ruby
|
|
242
|
-
class Organization < ResourceRecord
|
|
243
|
-
has_many :projects
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
class Project < ResourceRecord
|
|
247
|
-
belongs_to :organization
|
|
248
|
-
has_many :tasks
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
class Task < ResourceRecord
|
|
252
|
-
belongs_to :project
|
|
253
|
-
has_one :organization, through: :project # ← critical line
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Deeper
|
|
257
|
-
class Comment < ResourceRecord
|
|
258
|
-
belongs_to :task
|
|
259
|
-
has_one :project, through: :task
|
|
260
|
-
has_one :organization, through: :project # ← enables auto-scoping
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# Usage
|
|
264
|
-
Task.associated_with(org)
|
|
265
|
-
# => resolves via the :organization has_one :through
|
|
266
|
-
|
|
267
|
-
Comment.associated_with(org)
|
|
268
|
-
# => resolves via Comment -> Task -> Project -> Organization
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
**When to use:** the model is two+ hops away from the entity. Declaring `has_one :organization, through: ...` is the **lightest fix** — `associated_with` finds it via `reflect_on_all_associations` with no policy override needed.
|
|
272
|
-
|
|
273
|
-
### When to fall back to a custom scope
|
|
274
|
-
|
|
275
|
-
Use a custom `associated_with_<model_name>` scope when:
|
|
276
|
-
|
|
277
|
-
- The path is polymorphic.
|
|
278
|
-
- The path needs conditional logic.
|
|
279
|
-
- You want explicit SQL for performance (e.g. avoid a multi-join chain).
|
|
280
|
-
|
|
281
|
-
```ruby
|
|
282
|
-
class Comment < ResourceRecord
|
|
283
|
-
scope :associated_with_organization, ->(org) do
|
|
284
|
-
joins(task: :project).where(projects: {organization_id: org.id})
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
# Plutonium picks this up BEFORE trying association detection.
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## How the pieces fit together
|
|
292
|
-
|
|
293
|
-
1. An admin opens `/organizations/42/projects`.
|
|
294
|
-
2. Portal's `scope_to_entity Organization, strategy: :path` extracts `42`, loads the `Organization`, sets `current_scoped_entity`.
|
|
295
|
-
3. The controller calls the policy. The policy's inherited `relation_scope` calls `default_relation_scope(relation)`.
|
|
296
|
-
4. `default_relation_scope` has no parent (this is a top-level nested resource from the portal's perspective), so it calls `relation.associated_with(current_scoped_entity)`.
|
|
297
|
-
5. `Project.associated_with(org)` resolves via the direct `belongs_to :organization` → `Project.where(organization: org)`.
|
|
298
|
-
6. The controller renders only that organization's projects. Records from other orgs are invisible.
|
|
299
|
-
|
|
300
|
-
Any model that cannot be reached from the entity via these rules must declare a `has_one :through` or a custom scope. Policies must never work around this — work around it in the **model**.
|
|
301
|
-
|
|
302
|
-
## Gotchas
|
|
303
|
-
|
|
304
|
-
- **Policy tries to filter by entity directly.** Wrong — that bypasses `default_relation_scope`. Add the association path to the model instead.
|
|
305
|
-
- **`super` inside `relation_scope`.** Unreliable. Call `default_relation_scope(relation)` explicitly.
|
|
306
|
-
- **Multiple associations to the same entity class.** E.g. `Match belongs_to :home_team, :away_team` both pointing at `Team`. Plutonium raises — override `scoped_entity_association` on the controller to pick one.
|
|
307
|
-
- **`param_key` differs from association name.** Fine — Plutonium finds the association by **class**, not param key. You can still `scope_to_entity Competition::Team, param_key: :team` and have the model use `belongs_to :competition_team`.
|
|
308
|
-
- **Forgetting compound uniqueness.** A unique constraint on `:code` alone leaks uniqueness across tenants. Use `validates :code, uniqueness: {scope: :organization_id}`.
|
|
309
|
-
- **Skipping the scope "temporarily" for debugging.** Use `skip_default_relation_scope!` explicitly — never leave a `where` bypass in the code.
|
|
310
|
-
|
|
311
|
-
## Related skills
|
|
312
|
-
|
|
313
|
-
- `plutonium-model` — `associated_with` mechanics, declaring associations, `has_one :through` patterns.
|
|
314
|
-
- `plutonium-policy` — writing `relation_scope` safely, bulk authorization, attribute permissions.
|
|
315
|
-
- `plutonium-portal` — entity strategies (path, custom), `scope_to_entity`, mounting.
|
|
316
|
-
- `plutonium-invites` — how invites and memberships interact with entity scoping.
|
|
317
|
-
- `plutonium-nested-resources` — parent scoping semantics, which take precedence over entity scoping.
|