Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .studio/skills-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"sha256": "41f41a9daf9573ed28eae2c87c23f2624489a52a3809b9792ac1701921390bca",
"sha256": "ade673a75c943a29456a7588b40329d260c496c6dc0c14a0d57f767b1d7fa661",
"skills": [
"jmix-add-dialog-detail-flow",
"jmix-add-entity-event-listener",
Expand All @@ -15,9 +15,11 @@
"jmix-create-liquibase-changelog",
"jmix-create-list-view",
"jmix-create-resource-role",
"jmix-create-row-level-role",
"jmix-create-service",
"jmix-create-test",
"jmix-ide-static-analysis",
"jmix-role-based-access",
"jmix-verify-api-symbol",
"jmix-verify-bootrun"
],
Expand Down
8 changes: 6 additions & 2 deletions content/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ READ the most specific skill for each artifact:
- Detail dialog from a button/action, OR master-row selection → filtered child grid: `jmix-add-dialog-detail-flow`
- Entity lifecycle/event business logic: `jmix-add-entity-event-listener`
- Database schema: `jmix-create-liquibase-changelog`
- Resource roles: `jmix-create-resource-role`
- Role-based access — model, security scope, `ui.loginToUi` login invariant (READ FIRST before any role): `jmix-role-based-access`
- Resource role — WHAT a user can do (entity/attribute/view/menu policies): `jmix-create-resource-role`
- Row-level role — WHICH rows a user sees (JPQL/predicate policies): `jmix-create-row-level-role`
- User-visible text / entity-enum captions: `jmix-add-i18n-keys`
- Tests: `jmix-create-test`
- Fetch plans / unfetched-reference / N+1 tuning: `jmix-configure-fetch-plan`
Expand All @@ -106,7 +108,9 @@ For each new persistent entity, run through: `jmix-create-entity` +
`jmix-add-i18n-keys`. For a user-facing entity, also add a list and/or detail
view (`jmix-create-list-view`, `jmix-create-detail-view`) and a view policy in
every role that can open them — **including dialog-only detail views opened
from a composition table**.
from a composition table**. Any user who logs into the UI also needs
`ui-minimal` / `ui.loginToUi` (the most commonly missed defect) — see
`jmix-role-based-access`.

Service- or listener-level defaulting does NOT relieve the entity from
defaulting required fields on initial persist — defaults must work through
Expand Down
6 changes: 3 additions & 3 deletions content/skills/jmix-create-detail-view/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ CONSTRUCTION from the WRONG/RIGHT examples below:

1. **An enum attribute is NEVER `entityComboBox`.** `entityComboBox`
is for ENTITY references; binding it to an enum (with or without a
made-up `enumClass` attribute) throws `IllegalStateException: Range
is enumeration` at render. There is no `enumClass` attribute on
`entityComboBox`. For a Jmix enum property use a plain `<comboBox>`
made-up `enumClass` attribute) fails at render — `entityComboBox`
requires an entity Range, not an enumeration. There is no `enumClass`
attribute on `entityComboBox`. For a Jmix enum property use a plain `<comboBox>`
or `<select>` — Jmix auto-populates it from the enum:

```xml
Expand Down
18 changes: 18 additions & 0 deletions content/skills/jmix-create-entity/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,23 @@ private List<ChildLine> lines; // leave uninitialized — Jmix returns a NotIns
private Parent parent;
```

## Plain association (`@ManyToOne`, not a composition)

The most common relationship in a CRUD model — e.g. `Order.customer` — is a plain
reference, NOT a composition. JPA defaults `@ManyToOne` to EAGER, which violates
the LAZY rule silently; declare `fetch = FetchType.LAZY` explicitly:

```java
@JoinColumn(name = "CUSTOMER_ID")
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
```

No `@Composition`, no `cascade`; add `optional = false` only when the reference is
mandatory. The foreign key is created in the Liquibase changelog, not by this
annotation (see `jmix-create-liquibase-changelog`). To show this reference in a
list column, the view's fetch plan must fetch it (see `jmix-create-list-view`).

## Auditing and Soft Delete

Add audit fields with the Spring Data annotations from `org.springframework.data.annotation`: `@CreatedBy`, `@CreatedDate`, `@LastModifiedBy`, `@LastModifiedDate`. For soft delete add `@DeletedBy` and `@DeletedDate` from `io.jmix.core.annotation` — soft-deleted rows are then auto-filtered out of `DataManager`/JPQL queries.
Expand Down Expand Up @@ -201,6 +218,7 @@ Apply common Java validation and persistence mappings when the field semantics a
- Lombok annotations (`@Data`, `@Getter`, `@Setter`, etc.) on Jmix entities — they interfere with the entity enhancer and break JPA/Jmix metadata.
- `FetchType.EAGER`.
- Missing Liquibase changelog for persistent changes.
- Relying on `@Column(unique = true)` alone for a unique constraint — Liquibase builds the schema, not Hibernate DDL, so the constraint never reaches the database. Add `<addUniqueConstraint>` to the changelog (see `jmix-create-liquibase-changelog`).
- Nullable child back references in composition aggregates.
- Relying only on UI initialization for required persistence fields.
- Instantiating or replacing a collection field that Jmix populated — it may be a `NotInstantiatedList`/`NotInstantiatedSet`. Leave collection fields uninitialized; do not assign `new ArrayList`/`new HashSet`.
Expand Down
41 changes: 36 additions & 5 deletions content/skills/jmix-create-liquibase-changelog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ Use this skill for every persistent entity or schema change.
</changeSet>
```

## Unique constraints and indexes

`@Column(unique = true)` in the entity does NOT create the constraint — Liquibase
builds the schema (not Hibernate DDL), so declare it in the changelog. Either add
it inline in `<createTable>`:

```xml
<column name="EMAIL" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="UQ_CUSTOMER__EMAIL"/>
</column>
```

or as a separate change:

```xml
<addUniqueConstraint tableName="CUSTOMER" columnNames="EMAIL"
constraintName="UQ_CUSTOMER__EMAIL"/>
```

For a non-unique lookup index use `<createIndex>`. A "field X must be unique"
requirement is not satisfied by the Java annotation alone.

## Audit and soft-delete columns

If the entity carries audit (`@CreatedBy` / `@CreatedDate` / `@LastModifiedBy` /
Expand Down Expand Up @@ -107,11 +129,20 @@ Order the parent first, the child (with its FK) second:
</changeSet>
```

For a **composition** child, the delete cascade is enforced by Jmix at the
application layer (`@Composition` + `@OnDelete(DeletePolicy.CASCADE)` on the
entity), NOT by the database — Jmix uses soft delete by default, so a DB-level
`onDelete="CASCADE"` would never fire. Leave the FK without `onDelete` unless
you specifically need hard-delete DB-level enforcement.
For a **composition** child, how the delete cascade is enforced depends on the
entities' delete mode. Soft deletion is NOT global — it applies only to entities
that declare the Soft-Delete trait (`@DeletedDate` / `@DeletedBy`); entities
without it are hard-deleted (physical `DELETE`).

- **Soft-deleted entities:** deletes are logical, so cascade is handled by Jmix at
the application layer (`@Composition` + `@OnDelete(DeletePolicy.CASCADE)` on the
entity); a DB-level `onDelete="CASCADE"` never fires — omit it.
- **Hard-deleted entities** (no Soft-Delete trait): deleting the parent physically
removes the row, so the child FK MUST declare `onDelete="CASCADE"` — otherwise the
delete FK-violates at runtime. (Jmix Studio generates this cascade for hard-delete
compositions.)

Match the FK to the entities' actual delete mode; do not assume soft delete.

## Root Changelog Reachability

Expand Down
44 changes: 44 additions & 0 deletions content/skills/jmix-create-list-view/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ public class CustomerListView extends StandardListView<Customer> {
</view>
```

## Columns on reference attributes → extend the fetch plan

`<fetchPlan extends="_base"/>` includes local attributes plus the instance name
(`_local` + `_instance_name` + embedded references), but NOT arbitrary references
or collections you display in columns. For a column on a reference (or nested
`a.b`) attribute, add that reference to the fetch plan:

```xml
<fetchPlan extends="_base">
<property name="customer" fetchPlan="_instance_name"/>
</fetchPlan>
...
<columns>
<column property="name"/>
<column property="customer"/> <!-- shows Customer instance name -->
<column property="customer.email"/> <!-- nested: fetch plan must include it -->
</columns>
```

This is a data-loading concern, not a compile one — it surfaces only when the grid
opens. `jmix-configure-fetch-plan` owns the exact rules (loading references, N+1
and fetch modes, and the unfetched-attribute pitfalls of *partial* plans on local
attributes).

## Choosing list-action types

The grid action that opens a row is one of:
Expand All @@ -83,6 +107,11 @@ The grid action that opens a row is one of:
descriptor attribute.
- `list_remove` — deletes the selected entity.

`list_create` and `list_edit` open `<Entity>.detail` — that detail view MUST
already exist (see `jmix-create-detail-view`). Wiring the action without the detail
view compiles but throws `NoSuchViewException: View '<Entity>.detail' is not
defined` the moment the user clicks Create or Edit.

`list_read` REPLACES `list_edit`, not the whole CRUD bar. "The list
opens records in read mode" or "use `read` instead of `edit`" still
leaves `list_create` and `list_remove` in place — drop them only when
Expand Down Expand Up @@ -193,9 +222,24 @@ XML. You do NOT need an `@Install(target = Target.DATA_LOADER)` load delegate;
if you DO write one, it must return `List<E>` — returning the `LoadContext`
itself means the query never runs and the grid is empty at open.

## Menu item (menu.xml)

To show the list view in navigation, add an `<item>` to `menu.xml`
(under `src/main/resources/.../menu.xml`) referencing the view's `@ViewController`
id via the `view` attribute:

```xml
<item view="Customer.list"/>
```

Granting a role access to this menu item is a separate security concern — see the
Menu Policy Audit in `jmix-create-resource-role`.

## Forbidden

- Declaring actions without visible buttons or another reachable UI trigger.
- A column on a reference or nested (`a.b`) attribute not added to the fetch plan (extra per-row loads / N+1 — see `jmix-configure-fetch-plan`).
- `list_create`/`list_edit` wired without an existing `<Entity>.detail` view (`NoSuchViewException` at first click).
- Using `@Table` names in JPQL.
- `urlQueryParameters` references to component ids that are not declared in the XML.
- Java controller without matching XML descriptor.
Expand Down
74 changes: 39 additions & 35 deletions content/skills/jmix-create-resource-role/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
---
name: jmix-create-resource-role
description: Create or update Jmix resource roles with entity, attribute, view, and menu policies, including the CREATE-implies-MODIFY rule.
description: Create or update a Jmix @ResourceRole with entity, attribute, view, and menu policies (CREATE-implies-MODIFY, minimum-policy mapping). See jmix-role-based-access for the model, security scope, and the ui.loginToUi login invariant.
---

# Create Resource Role

Use this skill when adding or changing Jmix security access.
Use this skill to create/update a `@ResourceRole` — WHAT a user can do (entity,
attribute, view, and menu policies). First read `jmix-role-based-access` for the
model, security scope, and the mandatory `ui.loginToUi` login invariant. For
row-level (WHICH rows) roles, see `jmix-create-row-level-role`.

The model is ADDITIVE / no-deny: if any assigned role grants access the user has
it, and there is no deny-policy. A role interface may `extend` several role
interfaces of the SAME kind to compose their policies (a role cannot mix
`@ResourceRole` and `@RowLevelRole`).
**Do not forget the login invariant:** a user with only this domain role CANNOT
log into the UI without `ui.loginToUi` — assign the built-in `ui-minimal` role too
(or add `@SpecificPolicy(resources = "ui.loginToUi")` + a `MainView` `@ViewPolicy`).
See `jmix-role-based-access`.

## TOP RULE — CREATE implies MODIFY

Expand Down Expand Up @@ -42,13 +45,16 @@ read-only even at creation (e.g. auto-generated audit fields), exclude them from
## Requirement wording → policy actions

Map the EXACT wording of the requirement to entity-policy actions. Re-read the
requirement for the entity BEFORE writing the policy block.
requirement for the entity BEFORE writing the policy block. Grant the MINIMUM the
workflow needs — a role that only reviews/confirms a record needs `READ`+`UPDATE`,
NOT `ALL`.

| Requirement wording (about an entity) | EntityPolicyAction |
|-------------------------------------------------|----------------------------|
| "view only", "read only" | `READ` |
| "view and create", "create only" | `READ`, `CREATE` |
| "view, edit" | `READ`, `UPDATE` |
| "confirm/approve/reject", "change status only" | `READ`, `UPDATE` (not `ALL`) |
| "view, create, delete" (no update) | `READ`, `CREATE`, `DELETE` |
| "full CRUD", "manage", "all operations" | `ALL` |
| "cannot be updated", "immutable" | do NOT include `UPDATE` |
Expand Down Expand Up @@ -82,33 +88,11 @@ A `@ViewPolicy` that lists parent list+detail but omits the child detail will pa
compilation and fail at runtime when the user clicks "+" inside the parent's
composition table.

## Row-Level roles
## Row-level roles

Row-level roles are a separate first-class concept from resource roles: a resource

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it fine deletion - just refactor im ok

role grants *what* you can do, a row-level role restricts *which rows* you see. They
live in their own interface annotated with `@RowLevelRole` and never mix with
`@ResourceRole`.

- `@JpqlRowLevelPolicy(entityClass = ..., where = "...")` filters at the database
level. Use `{E}` as the entity alias and `:current_user_*` params (e.g.
`:current_user_username`).
- `@PredicateRowLevelPolicy(entityClass = ..., actions = {...})` filters in-memory;
the method returns a `RowLevelPredicate` / `RowLevelBiPredicate`. Use for logic
that JPQL cannot express and for non-read operations.

Gotcha: a JPQL policy only affects the root entity of a loaded graph. If the same
entity is also loaded as a *collection* inside another entity's graph, define BOTH
a `@JpqlRowLevelPolicy` and a `@PredicateRowLevelPolicy` for it to keep access
consistent.

```java
@RowLevelRole(name = "Own Orders Only", code = "app_OwnOrdersOnly")
public interface OwnOrdersOnlyRole {
@JpqlRowLevelPolicy(entityClass = Order.class,
where = "{E}.createdBy = :current_user_username")
void orderPolicy();
}
```
Row-level roles (WHICH rows a user can access) are a SEPARATE concept — see
`jmix-create-row-level-role`. A single interface cannot mix `@ResourceRole` and
`@RowLevelRole`.

## Mechanical self-check before finishing

Expand All @@ -125,10 +109,23 @@ your own code:
`<menu id="...">` GROUP id does NOT grant its items.
3. **Every reachable view has a `@ViewPolicy` entry.** Include composition-dialog
detail views opened from a parent grid even though they have no menu item.
4. **UI login works for every non-admin role.** A user with only domain roles
cannot log into the UI without `ui.loginToUi` (via a `ui-minimal` assignment or
a `@SpecificPolicy` on the role). Verify a seeded NON-admin user actually reaches
`MainView`, not "Login failed" — testing/verifying as `admin` (full access) masks
this class of defect entirely. See `jmix-role-based-access`.

To assert a permission in Java at runtime, inject `AccessManager` and call
`applyRegisteredConstraints(...)` on a context (e.g. `EntityOperationContext`), then
check `isPermitted()`.
`applyRegisteredConstraints(...)` on a context (e.g. `CrudEntityContext` for entity
CRUD, `EntityAttributeContext` for an attribute), then check `isPermitted()`.

To show/hide a UI action or button by the current user's role or permission, use
that `AccessManager` check (or a view-level `@ViewPolicy`). Do NOT hand-roll a role
check by reading `SecurityContextHolder`/`GrantedAuthority` strings and comparing to
a raw role code: the granted authority is not the bare code (Jmix prefixes/maps it),
so such a check silently fails — the action stays hidden even for authorized users
and no error is raised. This defect compiles and passes green tests; it surfaces
only when a real non-admin user opens the view.

## Steps

Expand Down Expand Up @@ -172,6 +169,11 @@ public interface EmployeeRole {
}
```

This role grants domain access only. A user assigned just `EmployeeRole` still
CANNOT log into the UI — assign the built-in `ui-minimal` role too (or add
`@SpecificPolicy(resources = "ui.loginToUi")` + a `MainView` `@ViewPolicy` here).
See `jmix-role-based-access`.

## Role Matrix

| Surface | Required? | Policy |
Expand Down Expand Up @@ -209,3 +211,5 @@ group itself and the project's security checks use that group id.
- View policies only for list views while create/edit dialogs use detail views.
- Menu policy for a parent group when the user needs access to concrete menu items.
- Menu policy for views that are not menu entries.
- A domain role for a UI user without a `ui-minimal` assignment or `ui.loginToUi` (cannot log in — see `jmix-role-based-access`).
- Hand-rolled role checks via `SecurityContextHolder`/`GrantedAuthority` (use `AccessManager`).
Loading