use std::sync::Arc;
use fraiseql_core::{
cache::{CacheConfig, CachedDatabaseAdapter, QueryResultCache},
db::{
postgres::{PoolPrewarmConfig, PostgresAdapter},
traits::DatabaseAdapter,
},
runtime::Executor,
schema::{CompiledSchema, TenancyMode},
};
use fraiseql_error::{FraiseQLError, Result};
use serde::Deserialize;
use tracing::info;
use super::schema_isolation;
#[derive(Debug, Clone, Deserialize)]
pub struct TenantPoolConfig {
pub connection_string: String,
#[serde(default = "default_max_connections")]
pub max_connections: u32,
#[serde(default = "default_connect_timeout")]
pub connect_timeout_secs: u64,
#[serde(default = "default_idle_timeout")]
pub idle_timeout_secs: u64,
}
const fn default_max_connections() -> u32 {
10
}
const fn default_connect_timeout() -> u64 {
5
}
const fn default_idle_timeout() -> u64 {
300
}
#[async_trait::async_trait]
pub trait FromPoolConfig: DatabaseAdapter + Sized {
async fn from_pool_config(config: &TenantPoolConfig) -> Result<Self>;
}
#[async_trait::async_trait]
impl FromPoolConfig for PostgresAdapter {
async fn from_pool_config(config: &TenantPoolConfig) -> Result<Self> {
Self::with_pool_config(
&config.connection_string,
PoolPrewarmConfig {
min_size: 0,
#[allow(clippy::cast_possible_truncation)]
max_size: config.max_connections as usize,
timeout_secs: Some(config.connect_timeout_secs),
},
)
.await
}
}
#[async_trait::async_trait]
impl<A: FromPoolConfig> FromPoolConfig for CachedDatabaseAdapter<A> {
async fn from_pool_config(config: &TenantPoolConfig) -> Result<Self> {
let inner = A::from_pool_config(config).await?;
let cache = QueryResultCache::new(CacheConfig::default());
Ok(Self::new(inner, cache, String::new()))
}
}
#[doc(hidden)] pub async fn create_tenant_executor<A: FromPoolConfig>(
tenant_key: &str,
schema_json: &str,
pool_config: &TenantPoolConfig,
) -> Result<Arc<Executor<A>>> {
let schema =
CompiledSchema::from_json(schema_json, false).map_err(|e| FraiseQLError::Parse {
message: format!("Invalid compiled schema JSON: {e}"),
location: String::new(),
})?;
schema
.validate_format_version()
.map_err(|msg| FraiseQLError::validation(format!("Incompatible compiled schema: {msg}")))?;
let tenancy_mode = schema.tenancy_mode();
let adapter = A::from_pool_config(pool_config).await?;
if tenancy_mode == TenancyMode::Schema {
info!(tenant_key, "provisioning schema for tenant (schema isolation mode)");
schema_isolation::provision_tenant_schema(tenant_key, &adapter).await?;
schema_isolation::configure_search_path(tenant_key, &adapter).await?;
}
Ok(Arc::new(Executor::new(schema, Arc::new(adapter))))
}
pub async fn destroy_tenant_schema(tenant_key: &str, adapter: &dyn DatabaseAdapter) -> Result<()> {
schema_isolation::drop_tenant_schema(tenant_key, adapter).await
}