use crate::error::Error;
use crate::user::User;
use postrust_auth::{JwtCache, JwtConfig, JwtKeyError};
use postrust_core::{AppConfig, IsolationLevel, SchemaCache};
use postrust_engine::State;
use sqlx::{PgPool, Postgres, Transaction};
use tokio::sync::{RwLock, RwLockReadGuard};
#[non_exhaustive]
pub struct AppState {
pub pool: PgPool,
pub schema_cache: RwLock<SchemaCache>,
pub config: AppConfig,
pub jwt_config: JwtConfig,
pub jwt_cache: Option<JwtCache>,
}
impl AppState {
pub fn new(
pool: PgPool,
schema_cache: SchemaCache,
config: AppConfig,
) -> Result<Self, JwtKeyError> {
let jwt_config = crate::jwks::attach(postrust_engine::jwt_config(&config)?, &config)?;
Ok(Self::with_jwt(pool, schema_cache, config, jwt_config))
}
pub(crate) fn with_jwt(
pool: PgPool,
schema_cache: SchemaCache,
config: AppConfig,
jwt_config: JwtConfig,
) -> Self {
Self {
pool,
schema_cache: RwLock::new(schema_cache),
jwt_config,
jwt_cache: JwtCache::new(config.jwt_cache_enabled, config.jwt_cache_max_lifetime),
config,
}
}
pub async fn schema_cache(&self) -> RwLockReadGuard<'_, SchemaCache> {
self.schema_cache.read().await
}
pub async fn reload_schema(&self) -> Result<(), postrust_core::Error> {
let new_cache = SchemaCache::load_with_search_path(
&self.pool,
&self.config.db_schemas,
&self.config.db_extra_search_path,
)
.await?;
let mut guard = self.schema_cache.write().await;
*guard = new_cache;
Ok(())
}
pub fn default_schema(&self) -> &str {
self.config.default_schema()
}
pub fn schemas(&self) -> &[String] {
&self.config.db_schemas
}
pub async fn begin_as(&self, user: &User) -> Result<Transaction<'static, Postgres>, Error> {
self.begin_as_with(user, TxOptions::default()).await
}
pub async fn begin_as_with(
&self,
user: &User,
options: TxOptions,
) -> Result<Transaction<'static, Postgres>, Error> {
let mut tx = self.pool.begin().await.map_err(|e| {
let e = postrust_core::db::DbError::from(e);
tracing::error!("Could not begin a transaction: {}", e);
postrust_core::Error::ConnectionPool(e.to_string())
})?;
user.session(&self.config)
.read_only(options.read_only)
.isolation(options.isolation)
.pre_request(options.run_pre_request)
.apply_sqlx(&mut tx)
.await
.map_err(|e| {
tracing::debug!("Refused before the query: {}", e);
Error::from(e)
})?;
Ok(tx)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct TxOptions {
pub read_only: bool,
pub isolation: Option<IsolationLevel>,
pub run_pre_request: bool,
}
impl Default for TxOptions {
fn default() -> Self {
Self {
read_only: false,
isolation: None,
run_pre_request: true,
}
}
}
impl TxOptions {
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
pub fn isolation(mut self, isolation: IsolationLevel) -> Self {
self.isolation = Some(isolation);
self
}
pub fn run_pre_request(mut self, run_pre_request: bool) -> Self {
self.run_pre_request = run_pre_request;
self
}
}
impl State for AppState {
type Db = PgPool;
type SchemaCache<'a> = RwLockReadGuard<'a, SchemaCache>;
fn db(&self) -> &PgPool {
&self.pool
}
async fn schema_cache(&self) -> RwLockReadGuard<'_, SchemaCache> {
self.schema_cache.read().await
}
fn config(&self) -> &AppConfig {
&self.config
}
fn jwt_config(&self) -> &JwtConfig {
&self.jwt_config
}
fn jwt_cache(&self) -> Option<&JwtCache> {
self.jwt_cache.as_ref()
}
}