entityassist

CRTP-first reactive persistence toolkit for GuicedEE services. Provides fluent entity and query-builder DSL on top of Vert.x 5, Hibernate Reactive 7, and Mutiny with PostgreSQL support. Features type-safe queries, reactive CRUD with Uni, dot-notation path filters, pagination, aggregates, joins, bulk operations, and stateless sessions. Use when working with reactive persistence, Hibernate Reactive, building entities and repositories, writing queries, or implementing non-blocking database operations in GuicedEE applications.

5 stars

Best use case

entityassist is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

CRTP-first reactive persistence toolkit for GuicedEE services. Provides fluent entity and query-builder DSL on top of Vert.x 5, Hibernate Reactive 7, and Mutiny with PostgreSQL support. Features type-safe queries, reactive CRUD with Uni, dot-notation path filters, pagination, aggregates, joins, bulk operations, and stateless sessions. Use when working with reactive persistence, Hibernate Reactive, building entities and repositories, writing queries, or implementing non-blocking database operations in GuicedEE applications.

Teams using entityassist should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/entityassist/SKILL.md --create-dirs "https://raw.githubusercontent.com/GuicedEE/ai-rules/main/skills/.system/entityassist/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/entityassist/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How entityassist Compares

Feature / AgententityassistStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

CRTP-first reactive persistence toolkit for GuicedEE services. Provides fluent entity and query-builder DSL on top of Vert.x 5, Hibernate Reactive 7, and Mutiny with PostgreSQL support. Features type-safe queries, reactive CRUD with Uni, dot-notation path filters, pagination, aggregates, joins, bulk operations, and stateless sessions. Use when working with reactive persistence, Hibernate Reactive, building entities and repositories, writing queries, or implementing non-blocking database operations in GuicedEE applications.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# EntityAssist Reactive

CRTP-first reactive persistence toolkit for GuicedEE services with Hibernate Reactive 7 and Mutiny.

## Core Features

- **CRTP-shaped entities** — `BaseEntity<J, Q, I>` with self-referencing fluent setters
- **Fluent query builder DSL** — Composable `where()`, `or()`, `orderBy()`, `groupBy()`, `join()`
- **Reactive CRUD with Mutiny** — All operations return `Uni<T>`
- **Dot-notation path filters** — `where("roles.name", Equals, "ADMIN")`
- **Pagination** — `setFirstResults()` / `setMaxResults()`
- **Aggregate projections** — `selectMin()`, `selectMax()`, `selectSum()`, `selectAverage()`, `selectCount()`
- **Join support** — INNER, LEFT, RIGHT joins with on-clause builders
- **Common Table Expressions** — Fluent `with()` / `withRecursiveHierarchy()` CTEs (Hibernate 7)
- **Bulk operations** — Criteria delete and update with safety guards
- **Stateless sessions** — High-throughput bulk operations
- **Bean Validation** — `validateEntity()` returns constraint violations

## Quick Start

### Define a CRTP Entity

```java
@Entity
@Accessors(chain = true)
@Table(name = "entity_class")
public class EntityClass
        extends BaseEntity<EntityClass, EntityClass.EntityClassQueryBuilder, String> {

    @Id
    @Column(name = "id", nullable = false)
    @Getter @Setter
    private String id;

    @Column(name = "name")
    @Getter @Setter
    private String name;

    @Override
    public String getId() { return id; }

    @Override
    public EntityClass setId(String id) {
        this.id = id;
        return this;
    }

    public static class EntityClassQueryBuilder
            extends QueryBuilder<EntityClassQueryBuilder, EntityClass, String> {

        @Override
        public boolean isIdGenerated() {
            return false;
        }
    }
}
```

### Entity with Relationships

```java
@Entity
@Accessors(chain = true)
@Table(name = "entity_class_two")
public class EntityClassTwo
        extends BaseEntity<EntityClassTwo, EntityClassTwo.EntityClassTwoQueryBuilder, String> {

    @Id
    @Getter @Setter
    private String id;

    @Column(name = "value")
    @Getter @Setter
    private Integer value;

    @ManyToOne
    @JoinColumn(name = "entity_class_id")
    @Getter @Setter
    private EntityClass entityClass;

    @Override
    public String getId() { return id; }

    @Override
    public EntityClassTwo setId(String id) {
        this.id = id;
        return this;
    }

    public static class EntityClassTwoQueryBuilder
            extends QueryBuilder<EntityClassTwoQueryBuilder, EntityClassTwo, String> {

        @Override
        public boolean isIdGenerated() {
            return false;
        }
    }
}
```

## Type Hierarchy

```
IRootEntity                      IQueryBuilderRoot
  └─ IDefaultEntity                └─ IDefaultQueryBuilder
      └─ IBaseEntity                   └─ IQueryBuilder
          ↑                                ↑
 RootEntity<J,Q,I>              QueryBuilderRoot<J,E,I>
   └─ DefaultEntity<J,Q,I>       └─ DefaultQueryBuilder<J,E,I>
       └─ BaseEntity<J,Q,I>          └─ QueryBuilder<J,E,I>
           ↑                              ↑
    Your Entity                    Your QueryBuilder
```

Every entity binds to its query builder via CRTP generics.

## Query Builder DSL

### Persist (Create)

```java
sessionFactory.withSession(session ->
    session.withTransaction(tx ->
        entity.builder(session)
              .persist(entity)
    )
).replaceWithVoid();
```

### Find by ID

```java
sessionFactory.withSession(session ->
    new EntityClass()
        .builder(session)
        .find("test1")
        .get()                       // Uni<EntityClass>
);
```

### Where / Or / OrderBy

```java
sessionFactory.withSession(session -> {
    var qb = new EntityClass().builder(session);
    return qb
        .where(qb.getAttribute("name"), Operand.Like, "A%")
        .or(qb.getAttribute("name"), Operand.Equals, "Bob")
        .orderBy(qb.getAttribute("name"), OrderByType.ASC)
        .setMaxResults(50)
        .getAll();                   // Uni<List<EntityClass>>
});
```

### Dot-Notation Path Filters

Traverse relationships without explicit joins:

```java
sessionFactory.withSession(session -> {
    var qb = new EntityClassTwo().builder(session);
    return qb
        .where("entityClass.name", Operand.Equals, "Parent Entity")
        .where("value", Operand.GreaterThan, 10)
        .getAll();
});
```

`where(String path, ...)` splits the path on `.` and walks `path.get(segment)` for each segment
(`WhereExpression.buildPath`), creating the implicit joins. The filter is applied entirely inside the
single criteria query.

> **Reactive gotcha — filter on a joined column instead of resolving the related entity.**
> In a reactive (Hibernate Reactive / Mutiny) codebase you must **never** resolve a related entity
> synchronously just to use it in a `where(...)`. Calling another `find(...)` returns a `Uni`, and
> casting that `Uni` to the entity type throws `ClassCastException` at runtime
> (`UniOnItemTransformToUni cannot be cast to <Entity>`); awaiting/blocking it on the event loop is
> equally forbidden. Filter on the join path instead:
> ```java
> // ❌ BAD — blocks/casts a Uni to an entity to use it in where(...)
> ClassificationDataConcept dc = (ClassificationDataConcept) service.find(em, concept, system); // Uni!
> builder.where(Classification_.concept, Operand.Equals, dc);
>
> // ✅ GOOD — fully non-blocking: filter on the joined column directly
> builder.where("concept.name", Operand.Equals, concept.classificationValue());
> ```
> When the join key has a stable natural value (here the concept's persisted `name` equals
> `EnterpriseClassificationDataConcepts.classificationValue()`), the dot-notation filter is exactly
> equivalent to the entity-equality filter and stays on the reactive pipeline. This pattern fixed a
> real `ClassCastException` in `ClassificationQueryBuilder.withConcept`.

### Pagination

```java
sessionFactory.withSession(session -> {
    var qb = new EntityClass().builder(session);
    return qb
        .where(qb.getAttribute("name"), Operand.Like, "A%")
        .orderBy(qb.getAttribute("name"), OrderByType.ASC)
        .setFirstResults(0)
        .setMaxResults(20)
        .getAll();
});
```

### Count

```java
sessionFactory.withSession(session -> {
    var qb = new EntityClass().builder(session);
    return qb
        .where(qb.getAttribute("name"), Operand.Like, "A%")
        .getCount();                 // Uni<Long>
});
```

### Aggregate Projections

```java
sessionFactory.withSession(session -> {
    var qb = new EntityClassTwo().builder(session);
    return qb
        .selectMax(qb.getAttribute("value"))
        .get(Integer.class);         // Uni<Integer>
});
```

Available aggregates:
- `selectMin()`
- `selectMax()`
- `selectSum()`
- `selectSumAsDouble()`
- `selectSumAsLong()`
- `selectAverage()`
- `selectCount()`
- `selectCountDistinct()`
- `selectColumn()`

### Joins

```java
sessionFactory.withSession(session -> {
    var parent = new EntityClass().builder(session);
    var child = new EntityClassTwo().builder(session);
    return child
        .join(child.getAttribute("entityClass"), parent, JoinType.INNER)
        .where(parent.getAttribute("name"), Operand.Equals, "Parent Entity")
        .getAll();
});
```

### Common Table Expressions (CTEs)

CTEs compose in the same fluent builder pattern. A CTE is described by a normal
builder (its `where(...)` filters), registered with `with(...)`, and the outer
query is constrained to the CTE's rows via an `id IN (SELECT id FROM cte)`
predicate — so results are **real managed entities** and the entire DSL
(`where`, `orderBy`, `groupBy`, projections, `getAll`, `getCount`) keeps working.

> Requires Hibernate ORM 7 (the builder uses `HibernateCriteriaBuilder` /
> `JpaCriteriaQuery` under the hood). In this Hibernate version a CTE is
> materialised as a tuple, so EntityAssist projects the entity `@Id` into the CTE
> and filters the outer entity query by membership rather than re-rooting it.

#### Non-recursive CTE

```java
sessionFactory.withSession(session -> {
    // CTE body — just another builder with its own filters
    var activeOnly = new EntityClass().builder(session)
            .where("description", Operand.Equals, "ACTIVE");

    // Outer entity query constrained by the CTE, then filtered normally
    return new EntityClass().builder(session)
            .with("active_entities", activeOnly)   // WITH active_entities AS (SELECT id ...)
            .where("name", Operand.Like, "A%")
            .getAll();                             // Uni<List<EntityClass>>
});
```

Generated SQL:

```sql
WITH active_entities (active_entities_id) AS (
    SELECT e.id FROM entity_class e WHERE e.description = ?
)
SELECT m.* FROM entity_class m
WHERE m.id IN (SELECT active_entities_id FROM active_entities)
  AND m.name LIKE ?
```

#### Recursive CTE (adjacency-list hierarchy)

`withRecursiveHierarchy(name, anchor, parentAttribute)` walks a self-referencing
hierarchy and returns the anchor row plus every descendant. `parentAttribute` is
the self-referencing attribute holding the parent identifier (a scalar FK column;
dot-paths such as `"parent.id"` are supported for associations).

```java
@Entity
@Table(name = "category_node")
public class CategoryNode
        extends BaseEntity<CategoryNode, CategoryNode.CategoryNodeQueryBuilder, String> {
    @Id private String id;
    private String name;
    @Column(name = "parent_id") private String parentId;   // self-reference
    // getters/setters + builder ...
}
```

```java
sessionFactory.withSession(session -> {
    var anchor = new CategoryNode().builder(session)
            .where("id", Operand.Equals, "1");           // start at node 1

    return new CategoryNode().builder(session)
            .withRecursiveHierarchy("subtree", anchor, "parentId")
            .getAll();                                   // node 1 + all descendants
});
```

Generated SQL:

```sql
WITH RECURSIVE subtree (subtree_id) AS (
    SELECT e.id FROM category_node e WHERE e.id = ?          -- anchor member
    UNION ALL
    SELECT c.id FROM category_node c, subtree                -- recursive member
    WHERE c.parent_id = subtree.subtree_id
)
SELECT m.* FROM category_node m
WHERE m.id IN (SELECT subtree_id FROM subtree)
```

#### Low-level recursive CTE

For non-hierarchy recursion, `withRecursive(name, anchor, recursiveProducer, unionAll)`
exposes Hibernate's recursive member directly as
`Function<JpaCteCriteria<Object>, AbstractQuery<Object>>` (the CTE projects the
entity id; `unionAll = false` switches to `UNION DISTINCT`).

**Notes:**
- Call `with(...)` / `withRecursiveHierarchy(...)` once per CTE; multiple CTEs accumulate.
- A unique CTE name is generated when `name` is `null`/blank.
- The CTE body builder must target the same entity type as the outer builder.
- The entity must expose a single `@Id` field.

#### Recursion over a link / join table (not just self-FK adjacency)

`withRecursiveHierarchy(...)` only fits an **adjacency list on one entity** (a self-referencing FK:
`child.parent_id = tree.id`). When the hierarchy is stored in a **separate link table** — a
`many-to-many` / association entity such as `ParentXChild(parentId, childId)` — `withRecursiveHierarchy`
**cannot** express it (its recursive member always re-roots on the same entity). Use the low-level
`withRecursive(...)` instead: its `recursiveProducer` is **raw Hibernate criteria**, so the recursive
member may root on a *different* mapped entity (the link table) and project a column back as the outer
entity's id.

Requirements / shape:
- The **outer** builder and the **anchor** builder are rooted on the target entity `E` (the CTE
  projects `E`'s `@Id`, and the outer query is trimmed by `id IN (SELECT … FROM cte)`).
- The **link table must be a mapped EntityAssist entity** (it needs a query builder / metamodel).
- The recursive member selects the link column that yields the next `E` id (e.g.
  `select x.parentId from ParentXChild x, cte a where x.childId = a.<cteIdAlias>` + any edge filters),
  aliased to the CTE id alias and typed as `E`'s id type.

```java
// Climb a parent/child hierarchy stored in a link table (childId → parentId), seeded from an anchor.
var anchor = new SecurityToken().builder(session)
        .where("securityToken", Operand.InList, tokenStrings)
        .where("enterpriseId", Operand.Equals, enterpriseId);

return new SecurityToken().builder(session)
        .withRecursive("applicable", anchor, cteRef -> {
            var cb        = (HibernateCriteriaBuilder) builder.getCriteriaBuilder();
            var recursive = cb.createQuery(UUID.class);
            var x         = recursive.from(SecurityTokenXSecurityToken.class);  // the LINK entity
            var a         = recursive.from(cteRef);
            var parentId  = x.get("parentSecurityTokenId");
            parentId.alias("applicable_id");
            recursive.select(parentId);
            recursive.where(cb.and(
                    cb.equal(x.get("childSecurityTokenId"), a.get("applicable_id")),
                    /* edge filters: enterprise, SCD in-range, active-flag visible … */));
            return recursive;
        }, /* unionAll */ false)            // UNION DISTINCT dedupes diamond hierarchies
        .getAll();                          // every SecurityToken reachable by climbing parents
```

**Why this matters (CTE fusion).** Because the climb constrains a *real* `E` builder via
`id IN (SELECT … FROM cte)`, it composes with the rest of the DSL in the **same statement** — so a
recursive expansion and a downstream membership/security trim (e.g. an `id IN (readableIds)` /
`canRead(...)` filter) run as **one CTE-backed query** instead of: native recursive query → collect a
`Set<UUID>` → second `IN (…)` query. It is also fully portable (no vendor SQL string) and goes through
Hibernate Reactive, which **auto-flushes before the query** — removing any "pin `:now` to a logical
clock because the native query doesn't flush" visibility workarounds. Trade-off: the `recursiveProducer`
is raw criteria (more verbose than fluent `where(...)`), so the win is portability + composition/fusion,
not fewer lines.

### Bulk Delete

```java
sessionFactory.withSession(session ->
    session.withTransaction(tx -> {
        var qb = new EntityClass().builder(session);
        return qb
            .where(qb.getAttribute("name"), Operand.Equals, "obsolete")
            .delete();               // Uni<Integer> — rows affected
    })
);
```

**Safety guard:** Bulk `delete()` requires at least one filter. Use `truncate()` to remove all rows.

### Entity Delete

```java
sessionFactory.withSession(session ->
    session.withTransaction(tx ->
        entity.builder(session)
              .delete(entity)        // Uni<EntityClass>
    )
);
```

### Update (Merge)

```java
entity.setName("Updated Name");
sessionFactory.withSession(session ->
    session.withTransaction(tx ->
        entity.builder(session)
              .update()              // Uni<EntityClass>
    )
);
```

### Stateless Sessions

For high-throughput bulk operations:

```java
sessionFactory.withStatelessSession(session ->
    entity.builder(session)           // uses Mutiny.StatelessSession
          .persist(entity)
);
```

**Bulk-insert pattern (resolve-once + one stateless transaction).** When securing/post-processing many just-created rows, do NOT loop a per-row operation that re-resolves shared references and round-trips for every row. Resolve the shared values **once** on the live session, then write all rows in a single `withStatelessTransaction` so the persistence context never grows and inserts can be JDBC-batched:

```java
// ❌ BAD — per row: N × (re-resolve shared refs + find + persist) round-trips
for (var row : rows) row.createSecurity(session, ...).await()...;

// ✅ GOOD — resolve shared refs once, batch all inserts in ONE stateless tx
return resolveSharedRefs(session, system)                 // 1 pass on the live session
    .chain(refs -> sessionFactory.withStatelessTransaction(st -> {
        Uni<Long> chain = Uni.createFrom().item(0L);
        for (var row : rows)
            chain = chain.chain(n -> row.persistDerived(st, refs).map(k -> n + k));
        return chain;                                     // pure inserts, no growing context
    }));
```

For just-created rows skip any per-row existence gate entirely (the caller already knows they are new); only use a `count == 0` gate when the pass must be idempotent over a whole table (re-installs).

## Transactions with Mutiny

```java
sessionFactory.withSession(session ->
    session.withTransaction(tx ->
        new EntityClass().builder(session)
            .persist(new EntityClass().setId("b1").setName("Bob"))
            .chain(() ->
                new EntityClass().builder(session)
                    .find("b1")
                    .get()
            )
            .invoke(found -> log.info("Created and retrieved: {}", found.getName()))
    )
);
```

## Configuration

### Database Module

Create a `DatabaseModule` subclass annotated with `@EntityManager`:

```java
@EntityManager(value = "entityAssistReactive", defaultEm = true)
public class EntityAssistReactiveDBModule
        extends DatabaseModule<EntityAssistReactiveDBModule>
        implements IGuiceModule<EntityAssistReactiveDBModule> {

    @Override
    protected String getPersistenceUnitName() {
        return "entityAssistReactive";
    }

    @Override
    protected ConnectionBaseInfo getConnectionBaseInfo(
            PersistenceUnitDescriptor unit, Properties filteredProperties) {
        PostgresConnectionBaseInfo connectionInfo = new PostgresConnectionBaseInfo();
        connectionInfo.setServerName("localhost");
        connectionInfo.setPort("5432");
        connectionInfo.setDatabaseName("mydb");
        connectionInfo.setUsername(System.getenv("DB_USER"));
        connectionInfo.setPassword(System.getenv("DB_PASSWORD"));
        connectionInfo.setDefaultConnection(true);
        connectionInfo.setReactive(true);
        return connectionInfo;
    }

    @Override
    protected String getJndiMapping() {
        return "jdbc:entityAssistReactive";
    }
}
```

### JPMS Registration

```java
module my.app {
    requires com.entityassist;
    requires com.guicedee.persistence;

    opens my.app.entities to org.hibernate.orm.core, com.google.guice, com.entityassist;

    provides com.guicedee.client.services.lifecycle.IGuiceModule
        with my.app.MyDatabaseModule;
}
```

### Environment Variables

| Variable | Purpose | Default |
|---|---|---|
| `DB_HOST` | Database hostname | `localhost` |
| `DB_PORT` | Database port | `5432` |
| `DB_NAME` | Database name | — |
| `DB_USER` | Database username | — |
| `DB_PASSWORD` | Database password | — |
| `ENVIRONMENT` | Runtime environment | `dev` |

## Operands

See [references/operands.md](references/operands.md) for complete list.

Common operands:
- `Equals`, `NotEquals`
- `Like`, `NotLike`
- `LessThan`, `LessThanEqualTo`
- `GreaterThan`, `GreaterThanEqualTo`
- `Null`, `NotNull`
- `InList`, `NotInList`

## Key Classes

**Entities:**
- `RootEntity<J,Q,I>` — Root CRTP entity with `builder()`, `persist()`, `update()`
- `DefaultEntity<J,Q,I>` — Intermediate extension point
- `BaseEntity<J,Q,I>` — Primary superclass for user entities

**Query Builders:**
- `QueryBuilderRoot<J,E,I>` — Root builder with session management
- `DefaultQueryBuilder<J,E,I>` — Fluent DSL methods
- `QueryBuilder<J,E,I>` — Primary superclass for user builders

**Expressions:**
- `WhereExpression` — Single `where` predicate
- `GroupedExpression` — AND/OR predicate grouping
- `JoinExpression` — Join definition
- `CteExpression` — Common Table Expression definition (name, body builder, recursion)
- `SelectExpression` — Column selection with aggregates
- `OrderByExpression` — Column + direction
- `GroupByExpression` — Column grouping

## ActiveFlag Lifecycle Enum

Rich status model with ranged queries:

```java
public enum ActiveFlag {
    Unknown,
    Deleted,
    Active,
    Permanent
}
```

Helpers:
- `getActiveRange()` — Active to Permanent
- `getVisibleRangeAndUp()` — Active and above
- And more status range helpers

## Converters

Built-in JPA attribute converters:
- `LocalDateAttributeConverter` — `LocalDate` ↔ `java.sql.Date`
- `LocalDateTimeAttributeConverter` — `LocalDateTime` ↔ `java.sql.Timestamp`
- `LocalDateTimestampAttributeConverter` — `LocalDate` ↔ `java.sql.Timestamp`

## Testing with Testcontainers

```java
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class EntityAssistReactiveTest {

    private Mutiny.SessionFactory sessionFactory;

    @BeforeAll
    public void setup() {
        IGuiceContext.instance();
        JtaPersistService ps = (JtaPersistService) IGuiceContext.get(
            Key.get(PersistService.class, Names.named("entityAssistReactive")));
        ps.start();

        sessionFactory = IGuiceContext.get(
            Key.get(Mutiny.SessionFactory.class, Names.named("entityAssistReactive")));
    }

    @Test
    void roundTrip() {
        EntityClass entity = new EntityClass()
            .setId("test1")
            .setName("Test Entity");

        sessionFactory.withSession(session ->
            session.withTransaction(tx ->
                entity.builder(session).persist(entity)
            ).chain(() ->
                new EntityClass().builder(session)
                    .find("test1").get()
            ).invoke(found -> {
                assertNotNull(found);
                assertEquals("test1", found.getId());
            })
        ).replaceWithVoid();
    }
}
```

## Best Practices

- Never use `await().indefinitely()` in service flows; return `Uni` and keep composition with `chain(...)` / `invoke(...)`
- Always run in Vert.x context (event loop or worker)
- Prefer projections for read-heavy paths
- Use `setFirstResults()` / `setMaxResults()` for pagination
- Keep transactions short; chain `Uni` calls
- Bulk `delete()` requires filters — use `truncate()` for all rows
- Use stateless sessions for bulk inserts
- Validate entities before persistence with `validateEntity()`
- **Filter associations with a JOIN in the WHERE clause, not `session.fetch()`.** To *restrict* a result set
  by a related row, add the join/predicate to the builder (`where(...)`, dot-notation path filters, or the
  domain helpers like `withConcept(...)` / `withEnterprise(...)` / `inActiveRange()`). Reserve
  `session.fetch(...)` for *loading* a lazy association you actually need to read — never to filter on the
  heap. The join keeps the predicate in a single SQL statement, avoids materialising unneeded rows, and
  sidesteps lazy-init hazards on the reactive session.

  ```java
  // ✅ Good — predicate pushed into SQL via a join on the WHERE clause
  var qb = new Classification().builder(session);
  return qb.where(qb.getAttribute("concepts.name"), Operand.Equals, conceptName)
           .inActiveRange()
           .getCount();                         // existence check, no rows materialised

  // ❌ Bad — fetch the association then filter in Java
  return entityService.find(session, id)
      .chain(e -> session.fetch(e.getConcepts()))
      .map(concepts -> concepts.stream().anyMatch(...));
  ```
- **Prefer `getCount()` over `get()` + null-check** for existence tests — it avoids selecting and hydrating a
  full entity just to discover whether a row exists.
- **Operate on the caller's `Mutiny.Session`.** Library/builder code must accept and use the session/transaction
  it is handed; never open a nested session (e.g. a second `withSession`/`withTransaction`) inside a method that
  already received one. Nesting sessions on the same Vert.x context triggers `HR000069` (wrong-thread) and
  "Illegal pop()" errors and violates one-action-per-session. Establish the unit of work at the entry point.

## JPMS Module

```java
module com.entityassist {
    requires transitive com.guicedee.persistence;
    requires transitive jakarta.persistence;
    requires transitive org.hibernate.reactive;
    requires transitive io.smallrye.mutiny;

    exports com.entityassist.entities;
    exports com.entityassist.querybuilder;
    exports com.entityassist.enumerations;

    opens com.entityassist.entities to org.hibernate.orm.core, com.google.guice;
}
```

### Required JVM module flags (runtime / jlink)

Hibernate ORM core reflectively reaches into EntityAssist's classes when bootstrapping
the metamodel, but `org.hibernate.orm.core` does not (and must not) statically
`requires com.entityassist`. The reverse read edge therefore has to be added at launch
time. Add this flag to the application launcher (and to any `jlink`
`--add-reads`/launcher options, surefire `argLine`, and IDE run configs):

```
--add-reads
org.hibernate.orm.core=com.entityassist
```

Without it, metamodel/attribute resolution for EntityAssist entities fails at runtime
with an `IllegalAccessError` / `does not read` module error. This edge belongs to
EntityAssist (it is required by *any* application that persists EntityAssist entities),
independent of which downstream domain (e.g. ActivityMaster) is in use.

## Installation

```xml
<dependency>
  <groupId>com.entityassist</groupId>
  <artifactId>entity-assist-reactive</artifactId>
</dependency>
```

## Module Graph

```
com.entityassist
 ├── com.guicedee.persistence
 ├── com.guicedee.client
 ├── jakarta.persistence
 ├── org.hibernate.reactive
 ├── org.hibernate.orm.core
 ├── io.smallrye.mutiny
 ├── io.vertx.sql.client.pg
 └── jakarta.xml.bind
```

## References

- Module: `com.entityassist`
- Hibernate Reactive: 7.x
- Mutiny: 1.x
- Vert.x: 5.x
- Java: 25+
- License: Apache 2.0

Related Skills

skill-installer

5
from GuicedEE/ai-rules

Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).

skill-creator

5
from GuicedEE/ai-rules

Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.

jwebmp-webawesome

5
from GuicedEE/ai-rules

WebAwesome icon integration for JWebMP — modern, open-source icon library. Provides 1,500+ icons with solid/regular styles, sizing, rotation, animation, and CSS utilities. Drop-in FontAwesome alternative with fresh designs. Use when working with WebAwesome icons, modern icon designs, or as FontAwesome alternative in JWebMP applications.

jwebmp-webawesome-pro

5
from GuicedEE/ai-rules

WebAwesome Pro integration for JWebMP with premium icons and features. Extends jwebmp-webawesome with additional styles, premium icons, and advanced features. Use when working with WebAwesome Pro icons or premium WebAwesome features in JWebMP applications.

jwebmp-weather-icons

5
from GuicedEE/ai-rules

Weather Icons font library for JWebMP providing weather icon fonts. Use when displaying weather-related icons.

jwebmp-waypoints

5
from GuicedEE/ai-rules

Waypoints jQuery plugin for JWebMP triggering functions when elements enter the viewport. Use when implementing scroll-based interactions and animations.

jwebmp-waves-effect

5
from GuicedEE/ai-rules

Waves material design ripple effect for JWebMP creating Material Design click ripples on elements. Use when adding Material Design interaction effects.

jwebmp-vertx

5
from GuicedEE/ai-rules

Portable connector between JWebMP and Vert.x 5 powered by GuicedEE. Provides automatic page routing, AJAX event pipeline, data component servlet, CSS endpoint, site-loader script, WebSocket broadcasting via event bus, user-agent detection, and call-scope integration. Use when working with JWebMP Vert.x integration, HTTP routing, AJAX handling, WebSocket communication, or building reactive web applications with JWebMP.

jwebmp-tsclient

5
from GuicedEE/ai-rules

TypeScript client generation for JWebMP plugins. Provides annotations and utilities for generating TypeScript interfaces, components, services, and modules from Java code. Supports @TsDependency, @TsDevDependency, @NgComponent, @NgDataService, @NgRestClient annotations. Use when creating JWebMP plugins that generate TypeScript code, defining npm dependencies, building Angular-integrated components, or generating typed Angular REST client services.

jwebmp-toastr

5
from GuicedEE/ai-rules

Toastr jQuery notification plugin integration for JWebMP displaying non-blocking toast notifications. Use when showing transient user notifications and alerts.

jwebmp-themify-icons

5
from GuicedEE/ai-rules

Themify Icons font library for JWebMP providing a comprehensive icon font collection. Use when adding Themify icons to projects.

jwebmp-skycons

5
from GuicedEE/ai-rules

Skycons animated weather icons for JWebMP creating beautiful animated SVG weather visualizations. Use when rendering weather data with animated icons.