Skip to content
Merged
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
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ tracing-subscriber = "0.3"
| **Lifecycle Events** | `Created`, `Updated`, `Deleted` events |
| **Real-Time Streams** | Postgres LISTEN/NOTIFY integration |
| **Transactional Outbox** | `events(outbox)` — durable at-least-once event delivery with retry/backoff |
| **Lifecycle Hook Traits** | `{Entity}Hooks` trait emitted with `before_create` / `after_update` / etc.; invocation is currently manual at your service layer (tracking auto-invocation: [#127](https://github.com/RAprogramm/entity-derive/issues/127)) |
| **Lifecycle Hooks** | `{Entity}Hooks` trait plus `{Entity}Repo<H>`, a repository that runs the hooks around every mutation |
| **CQRS Commands** | Business-oriented command pattern; `sets(...)` turns one into a domain operation writing named columns |
| **Soft Delete** | `deleted_at` timestamp support |
| **Structured Logging** | Opt-in `tracing` feature wraps every generated async method in `#[tracing::instrument]` with `entity` + `op` fields |
Expand Down Expand Up @@ -288,6 +288,29 @@ handlers must be idempotent. Composes with `streams`: NOTIFY wakes
subscribers instantly, the outbox guarantees nothing is lost. Requires
the `outbox` feature and `serde_json` in your crate.

### Lifecycle Hooks

`#[entity(hooks)]` emits the `{Entity}Hooks` trait and `{Entity}Repo<H>`,
a repository that owns a pool and a hooks implementation and runs the
hooks around every mutation:

```rust,ignore
#[derive(Entity)]
#[entity(table = "users", hooks)]
pub struct User { /* ... */ }

let repo = UserRepo::new(pool, Audit);

let user = repo.create(dto).await?; // before_create → INSERT → after_create
let found = repo.find_by_id(id).await?; // reads carry no hooks
```

A failing `before_*` aborts before anything is written; `after_delete`
and `after_restore` run only when a row was actually affected. The hook
error only has to convert into the repository error, so hooks may keep
their own error type. The bare pool keeps working exactly as before —
the wrapper is opt-in.

### Domain Operations

Columns that must change only through a named operation cannot be
Expand Down
4 changes: 2 additions & 2 deletions crates/entity-derive-impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ events = []
# Generates command structs, the handler trait, and the dispatcher.
commands = []

# Generates `{Entity}Hooks` trait (currently manual-wiring; see #127 for
# auto-invocation plans).
# Generates the `{Entity}Hooks` trait and the `{Entity}Repo<H>` wrapper
# that runs the hooks around every mutation.
hooks = []

# Generates `{Entity}TransactionRepo` adapter, the (deprecated) `with_*`
Expand Down
55 changes: 22 additions & 33 deletions crates/entity-derive-impl/src/entity/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,14 @@
//! Generates a hooks trait for entities with `#[entity(hooks)]`.
//! Hooks provide before/after callbacks for CRUD operations.
//!
//! # ⚠️ Status: trait emitted, invocation is manual (as of 0.8.2)
//! # Invocation
//!
//! This module emits the `{Entity}Hooks` trait for the user to
//! implement. **The generated repository methods do not call it
//! automatically yet.** The generated `impl {Entity}Repository for
//! sqlx::PgPool` lives in a different crate from any user-provided
//! `impl …Hooks for PgPool`, and Rust's orphan rule prevents the user
//! from wiring the two together after the fact.
//!
//! Until full auto-invocation lands, the supported pattern is:
//!
//! 1. Implement `{Entity}Hooks` on a type you own (typically a wrapper around
//! `PgPool` or a service struct in your application).
//! 2. Call the hook methods explicitly at your handler / service layer around
//! the calls into the generated CRUD methods. See
//! `examples/hooks/src/main.rs` for the wiring pattern.
//!
//! Tracking auto-invocation: [issue #127](https://github.com/RAprogramm/entity-derive/issues/127).
//! The orphan rule keeps a user crate from implementing
//! `{Entity}Hooks` for `sqlx::PgPool`, so the repository impl on the
//! pool cannot call hooks — both the trait and the type are foreign
//! there. [`wrapper`] emits the type that can: `{Entity}Repo<H>` owns
//! the pool and the hooks together and runs the hooks around every
//! mutation. The bare pool keeps working without hooks.
//!
//! # Generated Code
//!
Expand All @@ -43,7 +33,7 @@
//! }
//! ```
//!
//! # Usage (manual wiring)
//! # Usage
//!
//! ```rust,ignore
//! struct AppService {
Expand All @@ -64,15 +54,13 @@
//! }
//! }
//!
//! // At your handler layer:
//! async fn create_user(svc: &AppService, mut req: CreateUserRequest) -> Result<User, AppError> {
//! svc.before_create(&mut req).await?;
//! let user = <PgPool as UserRepository>::create(&svc.pool, req).await?;
//! svc.after_create(&user).await?;
//! Ok(user)
//! }
//! // The generated repository runs them:
//! let repo = UserRepo::new(pool, AppService);
//! let user = repo.create(req).await?; // before_create → INSERT → after_create
//! ```

mod wrapper;

use proc_macro2::TokenStream;
use quote::{format_ident, quote};

Expand All @@ -87,6 +75,8 @@ pub fn generate(entity: &EntityDef) -> TokenStream {
return TokenStream::new();
}

let repo_wrapper = wrapper::generate(entity);

let vis = &entity.vis;
let entity_name = entity.name();
let hooks_trait = format_ident!("{}Hooks", entity_name);
Expand All @@ -107,15 +97,12 @@ pub fn generate(entity: &EntityDef) -> TokenStream {
/// Implement this trait to add custom logic before/after CRUD operations.
/// All methods have default no-op implementations.
///
/// # ⚠️ Invocation is manual
/// # Invocation
///
/// The generated `Repository` impl on `sqlx::PgPool` does **not**
/// call these hooks automatically. Implement the trait on a type
/// you own (e.g. a service struct that wraps the pool) and call
/// the hook methods explicitly around your repository calls.
/// See `examples/hooks` and the module docs for the wiring pattern.
///
/// Tracking auto-invocation: <https://github.com/RAprogramm/entity-derive/issues/127>.
/// Pair an implementation with a pool through the generated
/// repository — `{Entity}Repo::new(pool, hooks)` — and the
/// hooks run around every mutation. Calling the repository
/// methods on the bare pool skips them.
///
/// # Error Handling
///
Expand All @@ -132,6 +119,8 @@ pub fn generate(entity: &EntityDef) -> TokenStream {
#delete_hooks
#command_hooks
}

#repo_wrapper
}
}

Expand Down
Loading