more multi-tenancy
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{config::AppConfig, state::AppState, utils::tracing::init_tracing};
|
||||
|
||||
/// Initialize tracing, load configuration, and build the shared `AppState`.
|
||||
/// Optionally override the connection pool size for lightweight components.
|
||||
pub async fn init_component(name: &str, pool_override: Option<u32>) -> Result<Arc<AppState>> {
|
||||
init_tracing("info");
|
||||
let config = AppConfig::load_and_log(name)?;
|
||||
let state = AppState::initialize(config, pool_override).await?;
|
||||
Ok(Arc::new(state))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use diesel::{pg::PgConnection, result::Error as DieselError};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
error::{AppError, AppResult},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub trait EnsureEntity<T> {
|
||||
fn one(self) -> AppResult<T>;
|
||||
fn maybe(self) -> AppResult<Option<T>>;
|
||||
}
|
||||
|
||||
impl<T> EnsureEntity<T> for Result<T, DieselError> {
|
||||
fn one(self) -> AppResult<T> {
|
||||
self.map_err(AppError::from)
|
||||
}
|
||||
|
||||
fn maybe(self) -> AppResult<Option<T>> {
|
||||
match self {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(DieselError::NotFound) => Ok(None),
|
||||
Err(err) => Err(AppError::from(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn with_tenant_conn<F, T>(&self, tenant_id: Uuid, f: F) -> AppResult<T>
|
||||
where
|
||||
F: FnOnce(&mut PgConnection) -> AppResult<T>,
|
||||
{
|
||||
let mut conn = self.db_for_tenant(tenant_id)?;
|
||||
f(&mut conn)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_bulk_ids(ids: &mut Vec<Uuid>, label: &str) -> AppResult<()> {
|
||||
if ids.is_empty() {
|
||||
return Err(AppError::bad_request(format!("{label} must not be empty")));
|
||||
}
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait IntoJsonResponse<T> {
|
||||
fn into_json(self) -> AppResult<axum::Json<T>>;
|
||||
}
|
||||
|
||||
impl<T> IntoJsonResponse<T> for T {
|
||||
fn into_json(self) -> AppResult<axum::Json<T>> {
|
||||
Ok(axum::Json(self))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_content() -> AppResult<axum::http::StatusCode> {
|
||||
Ok(axum::http::StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
/// Build an inline `Content-Disposition` header value for a given filename.
|
||||
pub fn inline_content_disposition(filename: &str) -> Option<String> {
|
||||
if filename.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sanitized: String = filename
|
||||
.chars()
|
||||
.map(|ch| match ch {
|
||||
'"' | '\\' => '_',
|
||||
_ => ch,
|
||||
})
|
||||
.collect();
|
||||
let encoded = utf8_percent_encode(&sanitized, NON_ALPHANUMERIC);
|
||||
|
||||
Some(format!(
|
||||
"inline; filename=\"{}\"; filename*=UTF-8''{}",
|
||||
sanitized, encoded
|
||||
))
|
||||
}
|
||||
@@ -1,2 +1,8 @@
|
||||
pub mod bootstrap;
|
||||
pub mod db;
|
||||
pub mod http;
|
||||
pub mod json;
|
||||
pub mod storage_paths;
|
||||
pub mod time;
|
||||
pub mod tracing;
|
||||
pub mod validation;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
|
||||
/// Format a timestamp as RFC3339 using UTC.
|
||||
pub fn to_iso(dt: NaiveDateTime) -> String {
|
||||
DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc).to_rfc3339()
|
||||
}
|
||||
|
||||
/// Format a timestamp for HTTP headers (RFC 7231 date).
|
||||
pub fn to_http_date(dt: NaiveDateTime) -> String {
|
||||
DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)
|
||||
.format("%a, %d %b %Y %H:%M:%S GMT")
|
||||
.to_string()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
/// Initialize tracing with an optional default level.
|
||||
///
|
||||
/// Falls back to `default_level` when `RUST_LOG` is not provided.
|
||||
pub fn init_tracing(default_level: &str) {
|
||||
let filter =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.compact()
|
||||
.init();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
/// Ensure an entity exists, returning a bad request error when it does not.
|
||||
pub fn ensure_exists(exists: bool, entity: &str) -> AppResult<()> {
|
||||
if exists {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::bad_request(format!("{entity} does not exist")))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user