diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml
index a9841d7..bdaea4d 100644
--- a/.github/workflows/dependabot-automerge.yml
+++ b/.github/workflows/dependabot-automerge.yml
@@ -16,23 +16,33 @@ jobs:
automerge:
name: Auto-merge Dependabot PRs
runs-on: ubuntu-latest
- if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
+ - name: Check if Dependabot PR
+ id: check
+ run: |
+ if [ "${{ github.event.pull_request.user.login }}" = "dependabot[bot]" ]; then
+ echo "is_dependabot=true" >> $GITHUB_OUTPUT
+ else
+ echo "is_dependabot=false" >> $GITHUB_OUTPUT
+ echo "Not a Dependabot PR, skipping auto-merge"
+ fi
+
- name: Fetch Dependabot metadata
+ if: steps.check.outputs.is_dependabot == 'true'
id: metadata
uses: dependabot/fetch-metadata@v2
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for minor/patch updates
- if: steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'
+ if: steps.check.outputs.is_dependabot == 'true' && (steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch')
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Approve patch updates
- if: steps.metadata.outputs.update-type == 'version-update:semver-patch'
+ if: steps.check.outputs.is_dependabot == 'true' && steps.metadata.outputs.update-type == 'version-update:semver-patch'
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
diff --git a/.gitignore b/.gitignore
index 96ef6c0..3a24ce7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/target
Cargo.lock
+examples/**/target/
diff --git a/Cargo.toml b/Cargo.toml
index 7ae1a0f..e209c51 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,9 +4,21 @@
[workspace]
resolver = "3"
members = ["crates/*"]
+exclude = [
+ "examples/basic",
+ "examples/filtering",
+ "examples/relations",
+ "examples/events",
+ "examples/hooks",
+ "examples/commands",
+ "examples/transactions",
+ "examples/soft-delete",
+ "examples/streams",
+ "examples/full-app",
+]
[workspace.package]
-version = "0.3.0"
+version = "0.4.0"
edition = "2024"
rust-version = "1.92"
authors = ["RAprogramm "]
@@ -14,9 +26,9 @@ license = "MIT"
repository = "https://github.com/RAprogramm/entity-derive"
[workspace.dependencies]
-entity-core = { path = "crates/entity-core", version = "0.1.2" }
-entity-derive = { path = "crates/entity-derive", version = "0.3.2" }
-entity-derive-impl = { path = "crates/entity-derive-impl", version = "0.1.2" }
+entity-core = { path = "crates/entity-core", version = "0.2.0" }
+entity-derive = { path = "crates/entity-derive", version = "0.4.0" }
+entity-derive-impl = { path = "crates/entity-derive-impl", version = "0.2.0" }
syn = { version = "2", features = ["full", "extra-traits", "parsing"] }
quote = "1"
proc-macro2 = "1"
diff --git a/README.md b/README.md
index 94bd40c..cd83d0e 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,9 @@
+
+
+
---
@@ -74,7 +77,7 @@ pub struct User {
```toml
[dependencies]
-entity-derive = { version = "0.3", features = ["postgres"] }
+entity-derive = { version = "0.4", features = ["postgres", "api"] }
```
---
@@ -85,9 +88,11 @@ entity-derive = { version = "0.3", features = ["postgres"] }
|---------|-------------|
| **Zero Runtime Cost** | All code generation at compile time |
| **Type Safe** | Change a field once, everything updates |
+| **Auto HTTP Handlers** | `api(handlers)` generates CRUD endpoints + router |
+| **OpenAPI Docs** | Auto-generated Swagger/OpenAPI documentation |
| **Query Filtering** | Type-safe `#[filter]`, `#[filter(like)]`, `#[filter(range)]` |
| **Relations** | `#[belongs_to]` and `#[has_many]` |
-| **Projections** | Partial views with optimized SELECT |
+| **Transactions** | Multi-entity atomic operations |
| **Lifecycle Events** | `Created`, `Updated`, `Deleted` events |
| **Real-Time Streams** | Postgres LISTEN/NOTIFY integration |
| **Lifecycle Hooks** | `before_create`, `after_update`, etc. |
@@ -131,6 +136,14 @@ entity-derive = { version = "0.3", features = ["postgres"] }
streams, // Optional: real-time Postgres NOTIFY
hooks, // Optional: before/after lifecycle hooks
commands, // Optional: CQRS command pattern
+ transactions, // Optional: multi-entity transaction support
+ api( // Optional: generate HTTP handlers + OpenAPI
+ tag = "Users",
+ handlers, // All CRUD, or handlers(get, list, create)
+ security = "bearer", // cookie, bearer, api_key, or none
+ title = "My API",
+ api_version = "1.0.0",
+ ),
)]
```
diff --git a/crates/entity-core/Cargo.toml b/crates/entity-core/Cargo.toml
index ee3f3e3..67d8791 100644
--- a/crates/entity-core/Cargo.toml
+++ b/crates/entity-core/Cargo.toml
@@ -3,7 +3,7 @@
[package]
name = "entity-core"
-version = "0.1.3"
+version = "0.2.0"
edition = "2024"
rust-version = "1.92"
authors = ["RAprogramm "]
diff --git a/crates/entity-core/src/prelude.rs b/crates/entity-core/src/prelude.rs
index 5f9723d..45fa8c6 100644
--- a/crates/entity-core/src/prelude.rs
+++ b/crates/entity-core/src/prelude.rs
@@ -11,12 +11,11 @@
#[cfg(feature = "streams")]
pub use crate::stream::StreamError;
+#[cfg(feature = "postgres")]
+pub use crate::transaction::TransactionContext;
pub use crate::{
CommandKind, EntityCommand, EntityEvent, EventKind, Pagination, Repository, SortDirection,
async_trait,
policy::{PolicyError, PolicyOperation},
- transaction::{
- Transaction, TransactionContext, TransactionError, TransactionOps, TransactionRunner,
- Transactional
- }
+ transaction::{Transaction, TransactionError}
};
diff --git a/crates/entity-core/src/transaction.rs b/crates/entity-core/src/transaction.rs
index fd2faca..9e40738 100644
--- a/crates/entity-core/src/transaction.rs
+++ b/crates/entity-core/src/transaction.rs
@@ -4,13 +4,13 @@
//! Transaction support for entity-derive.
//!
//! This module provides type-safe transaction management with automatic
-//! commit/rollback semantics. It uses the builder pattern for composing
-//! multiple repositories into a single transaction context.
+//! commit/rollback semantics. It uses a fluent builder pattern for composing
+//! multiple entity operations into a single transaction.
//!
//! # Overview
//!
//! - [`Transaction`] — Entry point for creating transactions
-//! - [`TransactionContext`] — Holds active transaction and repository adapters
+//! - [`TransactionContext`] — Holds active transaction, provides repo access
//! - [`TransactionError`] — Error wrapper for transaction operations
//!
//! # Example
@@ -18,7 +18,7 @@
//! ```rust,ignore
//! use entity_derive::prelude::*;
//!
-//! async fn transfer(pool: &PgPool, from: Uuid, to: Uuid, amount: Decimal) -> Result<(), AppError> {
+//! async fn transfer(pool: &PgPool, from: Uuid, to: Uuid, amount: i64) -> Result<(), AppError> {
//! Transaction::new(pool)
//! .with_accounts()
//! .with_transfers()
@@ -27,6 +27,7 @@
//!
//! ctx.accounts().update(from, UpdateAccount {
//! balance: Some(from_acc.balance - amount),
+//! ..Default::default()
//! }).await?;
//!
//! ctx.transfers().create(CreateTransfer { from, to, amount }).await?;
@@ -36,23 +37,38 @@
//! }
//! ```
-use std::{error::Error as StdError, fmt, future::Future, marker::PhantomData};
+#[cfg(feature = "postgres")]
+use std::future::Future;
+use std::{error::Error as StdError, fmt};
-/// Transaction builder for composing repositories.
+/// Transaction builder for composing multi-entity operations.
///
-/// Use [`Transaction::new`] to create a builder, then chain `.with_*()` methods
-/// to add repositories, and finally call `.run()` to execute.
+/// Use [`Transaction::new`] to create a builder, chain `.with_*()` methods
+/// to declare which entities you'll use, then call `.run()` to execute.
///
/// # Type Parameters
///
-/// - `DB` — Database type (e.g., `Postgres`)
-/// - `Repos` — Tuple of repository adapters accumulated via builder
-pub struct Transaction<'p, DB, Repos = ()> {
- pool: &'p DB,
- _repos: PhantomData
+/// - `'p` — Pool lifetime
+/// - `DB` — Database pool type (e.g., `PgPool`)
+///
+/// # Example
+///
+/// ```rust,ignore
+/// Transaction::new(&pool)
+/// .with_users()
+/// .with_orders()
+/// .run(|mut ctx| async move {
+/// let user = ctx.users().find_by_id(id).await?;
+/// ctx.orders().create(order).await?;
+/// Ok(())
+/// })
+/// .await?;
+/// ```
+pub struct Transaction<'p, DB> {
+ pool: &'p DB
}
-impl<'p, DB> Transaction<'p, DB, ()> {
+impl<'p, DB> Transaction<'p, DB> {
/// Create a new transaction builder.
///
/// # Arguments
@@ -66,82 +82,76 @@ impl<'p, DB> Transaction<'p, DB, ()> {
/// ```
pub const fn new(pool: &'p DB) -> Self {
Self {
- pool,
- _repos: PhantomData
+ pool
}
}
-}
-impl<'p, DB, Repos> Transaction<'p, DB, Repos> {
/// Get reference to the underlying pool.
pub const fn pool(&self) -> &'p DB {
self.pool
}
-
- /// Transform repository tuple type.
- ///
- /// Used internally by generated `with_*` methods.
- #[doc(hidden)]
- pub const fn with_repo(self) -> Transaction<'p, DB, NewRepos> {
- Transaction {
- pool: self.pool,
- _repos: PhantomData
- }
- }
}
-/// Active transaction context with repository adapters.
+/// Active transaction context with repository access.
///
/// This struct holds the database transaction and provides access to
-/// repository adapters that operate within the transaction.
+/// entity repositories via extension traits generated by the macro.
///
/// # Automatic Rollback
///
/// If dropped without explicit commit, the transaction is automatically
/// rolled back via the underlying database transaction's Drop impl.
///
-/// # Type Parameters
+/// # Accessing Repositories
+///
+/// Each entity with `#[entity(transactions)]` generates an extension trait
+/// that adds an accessor method:
///
-/// - `'t` — Transaction lifetime
-/// - `Tx` — Transaction type (e.g., `sqlx::Transaction<'t, Postgres>`)
-/// - `Repos` — Tuple of repository adapters
-pub struct TransactionContext<'t, Tx, Repos> {
- tx: Tx,
- repos: Repos,
- _lifetime: PhantomData<&'t ()>
+/// ```rust,ignore
+/// // For entity BankAccount, use:
+/// ctx.bank_accounts().find_by_id(id).await?;
+/// ctx.bank_accounts().create(dto).await?;
+/// ctx.bank_accounts().update(id, dto).await?;
+/// ```
+#[cfg(feature = "postgres")]
+pub struct TransactionContext {
+ tx: sqlx::Transaction<'static, sqlx::Postgres>
}
-impl<'t, Tx, Repos> TransactionContext<'t, Tx, Repos> {
+#[cfg(feature = "postgres")]
+impl TransactionContext {
/// Create a new transaction context.
///
/// # Arguments
///
/// * `tx` — Active database transaction
- /// * `repos` — Repository adapters tuple
#[doc(hidden)]
- pub const fn new(tx: Tx, repos: Repos) -> Self {
+ pub fn new(tx: sqlx::Transaction<'static, sqlx::Postgres>) -> Self {
Self {
- tx,
- repos,
- _lifetime: PhantomData
+ tx
}
}
/// Get mutable reference to the underlying transaction.
///
- /// Use this for custom queries within the transaction.
- pub fn transaction(&mut self) -> &mut Tx {
+ /// Use this for custom queries within the transaction or
+ /// for repository adapters to execute queries.
+ pub fn transaction(&mut self) -> &mut sqlx::Transaction<'static, sqlx::Postgres> {
&mut self.tx
}
- /// Get reference to repository adapters.
- pub const fn repos(&self) -> &Repos {
- &self.repos
+ /// Commit the transaction.
+ ///
+ /// Consumes self and commits all changes.
+ pub async fn commit(self) -> Result<(), sqlx::Error> {
+ self.tx.commit().await
}
- /// Get mutable reference to repository adapters.
- pub fn repos_mut(&mut self) -> &mut Repos {
- &mut self.repos
+ /// Rollback the transaction.
+ ///
+ /// Consumes self and rolls back all changes.
+ pub async fn rollback(self) -> Result<(), sqlx::Error> {
+ self.tx.rollback().await
}
}
@@ -211,133 +221,85 @@ impl TransactionError {
}
}
-/// Trait for types that can begin a transaction.
-///
-/// Implemented for database pools to enable transaction creation.
-#[allow(async_fn_in_trait)]
-pub trait Transactional: Sized + Send + Sync {
- /// Transaction type.
- type Transaction<'t>: Send
- where
- Self: 't;
-
- /// Error type for transaction operations.
- type Error: StdError + Send + Sync;
-
- /// Begin a new transaction.
- async fn begin(&self) -> Result, Self::Error>;
-}
-
-/// Trait for transaction types that can be committed or rolled back.
-#[allow(async_fn_in_trait)]
-pub trait TransactionOps: Sized + Send {
- /// Error type.
- type Error: StdError + Send + Sync;
-
- /// Commit the transaction.
- async fn commit(self) -> Result<(), Self::Error>;
-
- /// Rollback the transaction.
- async fn rollback(self) -> Result<(), Self::Error>;
+#[cfg(feature = "postgres")]
+impl From> for sqlx::Error {
+ fn from(err: TransactionError) -> Self {
+ err.into_inner()
+ }
}
-/// Trait for executing operations within a transaction.
-///
-/// This trait is implemented on [`Transaction`] with specific repository
-/// combinations, enabling type-safe execution.
-#[allow(async_fn_in_trait)]
-pub trait TransactionRunner<'p, Repos>: Sized {
- /// Transaction type.
- type Tx: TransactionOps;
-
- /// Database error type.
- type DbError: StdError + Send + Sync;
-
- /// Execute a closure within the transaction.
+// PostgreSQL implementation
+#[cfg(feature = "postgres")]
+impl<'p> Transaction<'p, sqlx::PgPool> {
+ /// Execute a closure within a PostgreSQL transaction.
///
- /// Automatically commits on `Ok`, rolls back on `Err` or panic.
+ /// Automatically commits on `Ok`, rolls back on `Err` or drop.
///
/// # Type Parameters
///
/// - `F` — Closure type
/// - `Fut` — Future returned by closure
/// - `T` — Success type
- /// - `E` — Error type (must be convertible from database error)
- async fn run(self, f: F) -> Result
+ /// - `E` — Error type (must be convertible from sqlx::Error)
+ ///
+ /// # Example
+ ///
+ /// ```rust,ignore
+ /// Transaction::new(&pool)
+ /// .with_users()
+ /// .run(|mut ctx| async move {
+ /// let user = ctx.users().create(dto).await?;
+ /// Ok(user)
+ /// })
+ /// .await?;
+ /// ```
+ pub async fn run(self, f: F) -> Result
where
- F: FnOnce(TransactionContext<'_, Self::Tx, Repos>) -> Fut + Send,
+ F: FnOnce(TransactionContext) -> Fut + Send,
Fut: Future