Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Repo Hooks

EsRepo supports two optional hooks that run during entity lifecycle operations. Both are configured as attributes on the #[es_repo(...)] derive macro.

HookRunsSignatureUse case
post_persist_hookAfter events are persisted (inside the transaction)async fn(&self, &mut OP, &Entity, LastPersisted<Event>) -> Result<(), E>Auditing, side-effect recording, cross-entity writes
post_hydrate_hookAfter an entity is reconstructed from eventsfn(&self, &Entity) -> Result<(), E>Validation against external config, policy enforcement

post_persist_hook

Runs after events have been written to the database but before the entity is returned to the caller. The hook executes inside the same transaction, so it can perform additional database operations or reject the persist.

Configuration

// Simple syntax (error defaults to sqlx::Error):
#[es_repo(entity = "User", post_persist_hook = "on_persist")]

// Explicit syntax with default error:
#[es_repo(entity = "User", post_persist_hook(method = "on_persist"))]

// Explicit syntax with custom error type:
#[es_repo(entity = "User", post_persist_hook(method = "on_persist", error = "MyPersistError"))]

Hook method

The method must be defined on the repo struct with this signature:

impl Users {
    async fn on_persist<OP: es_entity::AtomicOperation>(
        &self,
        op: &mut OP,
        entity: &User,
        new_events: es_entity::events::LastPersisted<'_, UserEvent>,
    ) -> Result<(), MyPersistError> {
        // Inspect newly persisted events, write audit records, etc.
        for event in new_events {
            // ...
        }
        Ok(())
    }
}

Error propagation

When the hook returns an error it is wrapped in the PostPersistHookError variant of CreateError or ModifyError:

match users.create(new_user).await {
    Err(e) => {
        // e is UserCreateError::PostPersistHookError(MyPersistError)
        println!("persist hook failed: {e}");
    }
    Ok(user) => { /* success */ }
}

post_hydrate_hook

Runs synchronously every time an entity is reconstructed from its event stream — on create, create_all, find_by_*, list_by_*, list_for_*, and find_all. It does not run on update or delete since those operate on an already-hydrated entity. This makes it suitable for invariant checks that depend on external state (e.g. configuration or governance rules) rather than the entity’s own events.

Configuration

#[es_repo(
    entity = "User",
    post_hydrate_hook(method = "validate_user", error = "UserValidationError")
)]

Both method and error are required.

Hook method

The method is synchronous and receives a shared reference to the entity:

impl Users {
    fn validate_user(&self, entity: &User) -> Result<(), UserValidationError> {
        if entity.name == "BANNED" {
            return Err(UserValidationError("banned name".into()));
        }
        Ok(())
    }
}

Error propagation

The error appears as a PostHydrateError variant on the relevant error type:

OperationError type
create, create_allUserCreateError::PostHydrateError(...)
find_by_*UserFindError::PostHydrateError(...)
list_by_*, list_for_*, find_allUserQueryError::PostHydrateError(...)
match users.find_by_id(id).await {
    Err(e) if e.was_post_hydrate_error() => {
        println!("entity failed validation: {e}");
    }
    Err(e) => return Err(e.into()),
    Ok(user) => { /* valid */ }
}

Combining both hooks

Both hooks can be used on the same repo. During create, both hooks run in this order:

  1. Events are persisted to the database
  2. post_persist_hook runs (async, inside transaction)
  3. Entity is hydrated from events
  4. post_hydrate_hook runs (sync)
  5. Entity is returned to the caller

During update, only post_persist_hook runs — no hydration occurs because the entity is already in memory. Similarly, find_by_* and list_* operations only run post_hydrate_hook since they don’t persist events.

#[derive(EsRepo)]
#[es_repo(
    entity = "User",
    post_persist_hook(method = "audit_persist", error = "AuditError"),
    post_hydrate_hook(method = "validate_user", error = "ValidationError"),
)]
pub struct Users {
    pool: sqlx::PgPool,
}