pub struct SecureConn { /* private fields */ }Expand description
Secure database connection wrapper.
This is the primary interface for module developers to access the database.
All operations require a SecurityCtx parameter for per-request access control.
§Usage
Module services receive a &SecureConn and provide SecurityCtx per-request:
pub struct MyService<'a> {
db: &'a SecureConn,
}
impl<'a> MyService<'a> {
pub async fn get_user(&self, scope: &AccessScope, id: Uuid) -> Result<Option<User>> {
self.db.find_by_id::<user::Entity>(ctx, id)?
.one(self.db)
.await
}
}§Security Guarantees
- All queries require
SecurityCtxfrom the request - Queries are scoped by tenant/resource from the context
- Empty scopes result in deny-all (no data returned)
- Type system prevents unscoped queries from compiling
- Modules cannot access raw database connections
Implementations§
Source§impl SecureConn
impl SecureConn
Sourcepub fn db_engine(&self) -> &'static str
pub fn db_engine(&self) -> &'static str
Return database engine identifier for tracing / logging.
Sourcepub fn find<E>(&self, scope: &AccessScope) -> SecureSelect<E, Scoped>
pub fn find<E>(&self, scope: &AccessScope) -> SecureSelect<E, Scoped>
Create a scoped select query for the given entity.
Returns a SecureSelect<E, Scoped> that automatically applies
tenant/resource filtering based on the provided security context.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let users = db.find::<user::Entity>(&ctx)?
.filter(user::Column::Status.eq("active"))
.order_by_asc(user::Column::Email)
.all(db)
.await?;§Errors
Sourcepub fn find_by_id<E>(
&self,
scope: &AccessScope,
id: Uuid,
) -> Result<SecureSelect<E, Scoped>, ScopeError>
pub fn find_by_id<E>( &self, scope: &AccessScope, id: Uuid, ) -> Result<SecureSelect<E, Scoped>, ScopeError>
Create a scoped select query filtered by a specific resource ID.
This is a convenience method that combines find() with .and_id().
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let user = db.find_by_id::<user::Entity>(&ctx, user_id)?
.one(db)
.await?;§Errors
Returns ScopeError if the entity doesn’t have a resource column or scoping fails.
Sourcepub fn update_many<E>(&self, scope: &AccessScope) -> SecureUpdateMany<E, Scoped>
pub fn update_many<E>(&self, scope: &AccessScope) -> SecureUpdateMany<E, Scoped>
Create a scoped update query for the given entity.
Returns a SecureUpdateMany<E, Scoped> that automatically applies
tenant/resource filtering. Use .col_expr() or other SeaORM methods
to specify what to update.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let result = db.update_many::<user::Entity>(&ctx)?
.col_expr(user::Column::Status, Expr::value("active"))
.col_expr(user::Column::UpdatedAt, Expr::value(Utc::now()))
.exec(db)
.await?;
println!("Updated {} rows", result.rows_affected);Sourcepub fn delete_many<E>(&self, scope: &AccessScope) -> SecureDeleteMany<E, Scoped>
pub fn delete_many<E>(&self, scope: &AccessScope) -> SecureDeleteMany<E, Scoped>
Create a scoped delete query for the given entity.
Returns a SecureDeleteMany<E, Scoped> that automatically applies
tenant/resource filtering.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let result = db.delete_many::<user::Entity>(&ctx)?
.exec(db)
.await?;
println!("Deleted {} rows", result.rows_affected);Sourcepub fn insert_one<E>(
&self,
scope: &AccessScope,
am: E::ActiveModel,
) -> Result<SecureInsertOne<E::ActiveModel, Scoped>, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
pub fn insert_one<E>(
&self,
scope: &AccessScope,
am: E::ActiveModel,
) -> Result<SecureInsertOne<E::ActiveModel, Scoped>, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
Create a scoped insert builder with on_conflict() support.
Unlike the simpler insert() method, this returns a builder that allows
setting on_conflict() for upsert semantics while still enforcing
tenant validation through the secure typestate pattern.
§Example
use sea_orm::sea_query::OnConflict;
let scope = AccessScope::tenants_only(vec![tenant_id]);
let am = settings::ActiveModel {
tenant_id: Set(tenant_id),
user_id: Set(user_id),
theme: Set(Some("dark".to_string())),
..Default::default()
};
db.insert_one(&scope, am)?
.on_conflict(
OnConflict::columns([Column::TenantId, Column::UserId])
.update_columns([Column::Theme])
.to_owned()
)
.exec(db)
.await?;§Errors
ScopeError::Invalidiftenant_idis not set for tenant-scoped entitiesScopeError::TenantNotInScopeiftenant_idis not in the provided scope
Sourcepub async fn insert<E>(
&self,
scope: &AccessScope,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel>,
pub async fn insert<E>(
&self,
scope: &AccessScope,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel>,
Insert a new entity with automatic tenant validation.
This is a convenience wrapper around secure_insert() that uses
the provided security context.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let am = user::ActiveModel {
id: Set(Uuid::new_v4()),
tenant_id: Set(tenant_id),
owner_id: Set(ctx.subject_id),
email: Set("user@example.com".to_string()),
..Default::default()
};
let user = db.insert::<user::Entity>(&ctx, am).await?;§Errors
ScopeError::Invalidif entity requires tenant but scope has noneScopeError::Dbif database insert fails
Sourcepub async fn update_with_ctx<E>(
&self,
scope: &AccessScope,
id: Uuid,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel> + ModelTrait<Entity = E>,
pub async fn update_with_ctx<E>(
&self,
scope: &AccessScope,
id: Uuid,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel> + ModelTrait<Entity = E>,
Update a single entity with security scope validation.
This method ensures the entity being updated is within the security scope before performing the update. It validates that the record is accessible based on tenant/resource constraints.
§Security
- Validates the entity exists and is accessible in the security scope
- Returns
ScopeError::Deniedif the entity is not in scope - Ensures updates cannot affect entities outside the security boundary
§Example
let ctx = SecurityCtx::for_tenant(tenant_id, user_id);
// Load and modify
let user_model = db.find_by_id::<user::Entity>(&ctx, id)?
.one(db)
.await?
.ok_or(NotFound)?;
let mut user: user::ActiveModel = user_model.into();
user.email = Set("newemail@example.com".to_string());
user.updated_at = Set(Utc::now());
// Update with scope validation (pass ID separately)
let updated = db.update_with_ctx::<user::Entity>(&ctx, id, user).await?;§Errors
ScopeError::Deniedif the entity is not accessible in the current scopeScopeError::Dbif the database operation fails
Sourcepub async fn delete_by_id<E>(
&self,
scope: &AccessScope,
id: Uuid,
) -> Result<bool, ScopeError>
pub async fn delete_by_id<E>( &self, scope: &AccessScope, id: Uuid, ) -> Result<bool, ScopeError>
Delete a single entity by ID (scoped).
This validates the entity exists in scope before deleting.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
db.delete_by_id::<user::Entity>(&ctx, user_id).await?;§Returns
Ok(true)if entity was deletedOk(false)if entity not found in scope
§Errors
Returns ScopeError::Invalid if the entity does not have a resource_col defined.
Sourcepub async fn transaction<F>(self, f: F) -> (Self, Result<()>)
pub async fn transaction<F>(self, f: F) -> (Self, Result<()>)
Execute a closure inside a database transaction.
This method starts a SeaORM transaction, provides the transaction handle
to the closure as &SecureTx, and handles commit/rollback.
§Return Type
Returns anyhow::Result<Result<T, E>> where:
- Outer
Err: Database/infrastructure error (transaction rolls back) - Inner
Ok(T): Success (transaction commits) - Inner
Err(E): Domain/validation error (transaction still commits)
This design ensures domain validation errors don’t cause rollback.
§Architecture Note
Transaction boundaries should be managed by application/domain services, not by REST handlers. REST handlers should call service methods that internally decide when to open transactions.
§Example
use modkit_db::secure::SecureConn;
// In a domain service:
pub async fn create_user(
db: &SecureConn,
repo: &UsersRepo,
user: User,
) -> Result<User, DomainError> {
let result = db.transaction(|conn| async move {
// Check email uniqueness
if repo.email_exists(conn, &user.email).await? {
return Ok(Err(DomainError::EmailExists));
}
// Create user
let created = repo.create(conn, user).await?;
Ok(Ok(created))
}).await?;
result
}§Security
This method consumes self and returns it after the transaction completes.
This prevents accidental use of the outer connection inside the transaction,
making transaction bypass impossible by construction.
Only &SecureTx is available inside the closure, ensuring all operations
execute within the transaction scope.
§Returns
Returns a tuple (Self, Result<()>) where:
Selfis the connection (always returned, even on error)Result<()>indicates transaction success or failure
§Errors
The Result component is Err(anyhow::Error) if:
- The transaction cannot be started
- A database operation fails (transaction is rolled back)
- The commit fails
Sourcepub async fn transaction_with<T, F>(self, f: F) -> (Self, Result<T>)
pub async fn transaction_with<T, F>(self, f: F) -> (Self, Result<T>)
Execute a transaction and return both the connection and a result value.
This method consumes self and returns both the connection and the result
from the transaction closure. Use this when you need to return data from
within the transaction.
§Security
Like transaction, this method prevents transaction bypass
by consuming self, making it impossible to access the outer connection
inside the transaction closure.
§Example
let (conn, result) = conn.transaction_with(|tx| {
Box::pin(async move {
let user = repo.create(tx, &scope, new_user).await?;
Ok(user)
})
}).await;
let user = result?;§Returns
Returns a tuple (Self, Result<T>) where:
Selfis the connection (always returned)Result<T>contains the transaction result or error
§Errors
The Result component is Err(anyhow::Error) if:
- The transaction cannot be started
- A database operation fails (transaction is rolled back)
- The commit fails
Sourcepub async fn transaction_with_config<T, F>(
self,
cfg: TxConfig,
f: F,
) -> (Self, Result<T>)
pub async fn transaction_with_config<T, F>( self, cfg: TxConfig, f: F, ) -> (Self, Result<T>)
Execute a closure inside a database transaction with custom configuration.
This method is similar to transaction, but allows
specifying the isolation level and access mode.
§Configuration
Use TxConfig to specify transaction settings without importing SeaORM types:
use modkit_db::secure::{TxConfig, TxIsolationLevel, TxAccessMode};
let cfg = TxConfig {
isolation: Some(TxIsolationLevel::Serializable),
access_mode: Some(TxAccessMode::ReadWrite),
};§Example
use modkit_db::secure::{SecureConn, TxConfig, TxIsolationLevel};
// In a domain service requiring serializable isolation:
pub async fn reconcile_accounts(
db: &SecureConn,
repo: &AccountsRepo,
) -> anyhow::Result<Result<ReconciliationResult, DomainError>> {
let cfg = TxConfig::serializable();
db.transaction_with_config(cfg, |conn| async move {
let accounts = repo.find_all_pending(conn).await?;
for account in accounts {
repo.reconcile(conn, &account).await?;
}
Ok(Ok(ReconciliationResult { processed: accounts.len() }))
}).await
}§Backend Notes
PostgreSQL: Full support for all isolation levels and access modes.- MySQL/InnoDB: Full support for all isolation levels and access modes.
SQLite: Only supportsSerializableisolation. Other levels are mapped toSerializable. Read-only mode is a hint only.
§Security
This method consumes self and returns both the connection and result,
preventing transaction bypass by making the outer connection unavailable
inside the closure.
§Returns
Returns a tuple (Self, Result<T>) where:
Selfis the connection (always returned)Result<T>contains the transaction result or error
§Errors
The Result component is Err(anyhow::Error) if:
- The transaction cannot be started with the specified configuration
- A database operation fails (transaction is rolled back)
- The commit fails
Sourcepub async fn in_transaction<T, E, F>(
self,
f: F,
) -> (Self, Result<T, TxError<E>>)
pub async fn in_transaction<T, E, F>( self, f: F, ) -> (Self, Result<T, TxError<E>>)
Execute a closure inside a typed domain transaction.
This method returns TxError<E> which distinguishes domain errors from
infrastructure errors, allowing callers to handle them appropriately.
§Error Handling
- Domain errors returned from the closure are wrapped in
TxError::Domain(e) - Database infrastructure errors are wrapped in
TxError::Infra(InfraError)
Use TxError::into_domain to convert the result into your domain error type.
§Example
use modkit_db::secure::SecureConn;
async fn create_user(db: &SecureConn, repo: &UsersRepo, user: User) -> Result<User, DomainError> {
db.in_transaction(move |tx| Box::pin(async move {
if repo.exists(tx, user.id).await? {
return Err(DomainError::already_exists(user.id));
}
repo.create(tx, user).await
}))
.await
.map_err(|e| e.into_domain(DomainError::database_infra))
}§Security
This method consumes self and returns both the connection and result,
preventing transaction bypass by making the outer connection unavailable
inside the closure.
§Returns
Returns a tuple (Self, Result<T, TxError<E>>) where:
Selfis the connection (always returned)Result<T, TxError<E>>contains the transaction result or error
§Errors
The Result component is Err(TxError<E>) if:
- The callback returns a domain error (
TxError::Domain(E)). - The transaction fails due to a database/infrastructure error (
TxError::Infra(InfraError)).
Sourcepub async fn in_transaction_mapped<T, E, F, M>(
self,
map_infra: M,
f: F,
) -> (Self, Result<T, E>)
pub async fn in_transaction_mapped<T, E, F, M>( self, map_infra: M, f: F, ) -> (Self, Result<T, E>)
Execute a typed domain transaction with automatic infrastructure error mapping.
This is a convenience wrapper around in_transaction that
automatically converts TxError into the domain error type using the provided
mapping function for infrastructure errors.
§Example
use modkit_db::secure::SecureConn;
async fn create_user(db: &SecureConn, repo: &UsersRepo, user: User) -> Result<User, DomainError> {
db.in_transaction_mapped(DomainError::database_infra, move |tx| Box::pin(async move {
if repo.exists(tx, user.id).await? {
return Err(DomainError::already_exists(user.id));
}
repo.create(tx, user).await
})).await
}§Security
This method consumes self and returns both the connection and result,
preventing transaction bypass.
§Returns
Returns a tuple (Self, Result<T, E>) where:
Selfis the connection (always returned)Result<T, E>contains the transaction result or mapped error
§Errors
The Result component is Err(E) if:
- The callback returns a domain error (
E). - The transaction fails due to a database/infrastructure error, mapped via
map_infra.
Auto Trait Implementations§
impl Freeze for SecureConn
impl RefUnwindSafe for SecureConn
impl Send for SecureConn
impl Sync for SecureConn
impl Unpin for SecureConn
impl UnsafeUnpin for SecureConn
impl UnwindSafe for SecureConn
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> Paint for Twhere
T: ?Sized,
impl<T> Paint for Twhere
T: ?Sized,
Source§fn fg(&self, value: Color) -> Painted<&T>
fn fg(&self, value: Color) -> Painted<&T>
Returns a styled value derived from self with the foreground set to
value.
This method should be used rarely. Instead, prefer to use color-specific
builder methods like red() and
green(), which have the same functionality but are
pithier.
§Example
Set foreground color to white using fg():
use yansi::{Paint, Color};
painted.fg(Color::White);Set foreground color to white using white().
use yansi::Paint;
painted.white();Source§fn bright_black(&self) -> Painted<&T>
fn bright_black(&self) -> Painted<&T>
Source§fn bright_red(&self) -> Painted<&T>
fn bright_red(&self) -> Painted<&T>
Source§fn bright_green(&self) -> Painted<&T>
fn bright_green(&self) -> Painted<&T>
Source§fn bright_yellow(&self) -> Painted<&T>
fn bright_yellow(&self) -> Painted<&T>
Source§fn bright_blue(&self) -> Painted<&T>
fn bright_blue(&self) -> Painted<&T>
Source§fn bright_magenta(&self) -> Painted<&T>
fn bright_magenta(&self) -> Painted<&T>
Source§fn bright_cyan(&self) -> Painted<&T>
fn bright_cyan(&self) -> Painted<&T>
Source§fn bright_white(&self) -> Painted<&T>
fn bright_white(&self) -> Painted<&T>
Source§fn bg(&self, value: Color) -> Painted<&T>
fn bg(&self, value: Color) -> Painted<&T>
Returns a styled value derived from self with the background set to
value.
This method should be used rarely. Instead, prefer to use color-specific
builder methods like on_red() and
on_green(), which have the same functionality but
are pithier.
§Example
Set background color to red using fg():
use yansi::{Paint, Color};
painted.bg(Color::Red);Set background color to red using on_red().
use yansi::Paint;
painted.on_red();Source§fn on_primary(&self) -> Painted<&T>
fn on_primary(&self) -> Painted<&T>
Source§fn on_magenta(&self) -> Painted<&T>
fn on_magenta(&self) -> Painted<&T>
Source§fn on_bright_black(&self) -> Painted<&T>
fn on_bright_black(&self) -> Painted<&T>
Source§fn on_bright_red(&self) -> Painted<&T>
fn on_bright_red(&self) -> Painted<&T>
Source§fn on_bright_green(&self) -> Painted<&T>
fn on_bright_green(&self) -> Painted<&T>
Source§fn on_bright_yellow(&self) -> Painted<&T>
fn on_bright_yellow(&self) -> Painted<&T>
Source§fn on_bright_blue(&self) -> Painted<&T>
fn on_bright_blue(&self) -> Painted<&T>
Source§fn on_bright_magenta(&self) -> Painted<&T>
fn on_bright_magenta(&self) -> Painted<&T>
Source§fn on_bright_cyan(&self) -> Painted<&T>
fn on_bright_cyan(&self) -> Painted<&T>
Source§fn on_bright_white(&self) -> Painted<&T>
fn on_bright_white(&self) -> Painted<&T>
Source§fn attr(&self, value: Attribute) -> Painted<&T>
fn attr(&self, value: Attribute) -> Painted<&T>
Enables the styling Attribute value.
This method should be used rarely. Instead, prefer to use
attribute-specific builder methods like bold() and
underline(), which have the same functionality
but are pithier.
§Example
Make text bold using attr():
use yansi::{Paint, Attribute};
painted.attr(Attribute::Bold);Make text bold using using bold().
use yansi::Paint;
painted.bold();Source§fn rapid_blink(&self) -> Painted<&T>
fn rapid_blink(&self) -> Painted<&T>
Source§fn quirk(&self, value: Quirk) -> Painted<&T>
fn quirk(&self, value: Quirk) -> Painted<&T>
Enables the yansi Quirk value.
This method should be used rarely. Instead, prefer to use quirk-specific
builder methods like mask() and
wrap(), which have the same functionality but are
pithier.
§Example
Enable wrapping using .quirk():
use yansi::{Paint, Quirk};
painted.quirk(Quirk::Wrap);Enable wrapping using wrap().
use yansi::Paint;
painted.wrap();Source§fn clear(&self) -> Painted<&T>
👎Deprecated since 1.0.1: renamed to resetting() due to conflicts with Vec::clear().
The clear() method will be removed in a future release.
fn clear(&self) -> Painted<&T>
resetting() due to conflicts with Vec::clear().
The clear() method will be removed in a future release.Source§fn whenever(&self, value: Condition) -> Painted<&T>
fn whenever(&self, value: Condition) -> Painted<&T>
Conditionally enable styling based on whether the Condition value
applies. Replaces any previous condition.
See the crate level docs for more details.
§Example
Enable styling painted only when both stdout and stderr are TTYs:
use yansi::{Paint, Condition};
painted.red().on_yellow().whenever(Condition::STDOUTERR_ARE_TTY);