use anyhow::Result;
use fraiseql_core::db::postgres::{PostgresSslMode, PostgresTlsConfig};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct TlsRuntimeConfig {
pub enabled: bool,
pub cert_file: String,
pub key_file: String,
pub min_version: String,
}
impl Default for TlsRuntimeConfig {
fn default() -> Self {
Self {
enabled: false,
cert_file: String::new(),
key_file: String::new(),
min_version: "1.2".to_string(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct CorsRuntimeConfig {
pub origins: Vec<String>,
pub credentials: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct ServerRuntimeConfig {
pub host: String,
pub port: u16,
pub request_timeout_ms: u64,
pub keep_alive_secs: u64,
pub cors: CorsRuntimeConfig,
pub tls: TlsRuntimeConfig,
}
impl Default for ServerRuntimeConfig {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 8080,
request_timeout_ms: 30_000,
keep_alive_secs: 75,
cors: CorsRuntimeConfig::default(),
tls: TlsRuntimeConfig::default(),
}
}
}
impl ServerRuntimeConfig {
pub fn validate(&self) -> Result<()> {
if self.port == 0 {
anyhow::bail!("[server] port must be non-zero");
}
if self.tls.enabled {
if self.tls.cert_file.is_empty() {
anyhow::bail!("[server.tls] cert_file is required when tls.enabled = true");
}
if self.tls.key_file.is_empty() {
anyhow::bail!("[server.tls] key_file is required when tls.enabled = true");
}
if self.tls.min_version != "1.2" && self.tls.min_version != "1.3" {
anyhow::bail!(
"[server.tls] min_version must be \"1.2\" or \"1.3\", got \"{}\"",
self.tls.min_version
);
}
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct DatabaseRuntimeConfig {
pub url: Option<String>,
pub pool_min: usize,
pub pool_max: usize,
pub connect_timeout_ms: u64,
pub idle_timeout_ms: u64,
pub ssl_mode: Option<String>,
}
impl Default for DatabaseRuntimeConfig {
fn default() -> Self {
Self {
url: None,
pool_min: 2,
pool_max: 20,
connect_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
ssl_mode: None,
}
}
}
impl DatabaseRuntimeConfig {
pub fn validate(&self) -> Result<()> {
if self.pool_min > self.pool_max {
anyhow::bail!(
"[database] pool_min ({}) must be <= pool_max ({})",
self.pool_min,
self.pool_max
);
}
if self.connect_timeout_ms == 0 {
anyhow::bail!(
"[database] connect_timeout_ms must be greater than 0; a zero timeout makes \
every connection attempt fail immediately"
);
}
self.postgres_tls()?;
Ok(())
}
pub fn from_document(document: &toml::Value) -> Result<Self> {
document.get("database").map_or_else(
|| Ok(Self::default()),
|section| section.clone().try_into().map_err(|e| anyhow::anyhow!("[database]: {e}")),
)
}
pub fn postgres_tls(&self) -> Result<PostgresTlsConfig> {
let mode: Option<PostgresSslMode> =
self.ssl_mode.as_deref().map(str::parse).transpose().map_err(
|e: fraiseql_core::error::FraiseQLError| {
anyhow::anyhow!("[database] ssl_mode: {e}")
},
)?;
Ok(PostgresTlsConfig {
mode,
ca_bundle_path: None,
})
}
#[must_use]
pub const fn pool_timeout_secs(&self) -> u64 {
let secs = self.connect_timeout_ms.div_ceil(1_000);
if secs == 0 { 1 } else { secs }
}
}