use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::{directive::DirectiveDefinition, mutation::MutationDefinition, query::QueryDefinition};
use crate::{
compiler::fact_table::FactTableMetadata,
schema::{
config_types::{
ChangelogConfig, DebugConfig, FederationConfig, GrpcConfig, McpConfig,
NamingConvention, ObserversConfig, RestConfig, SessionVariablesConfig,
SubscriptionsConfig, ValidationConfig,
},
graphql_type_defs::{
EnumDefinition, InputObjectDefinition, InterfaceDefinition, TypeDefinition,
UnionDefinition,
},
hierarchy::HierarchiesConfig,
observer_types::ObserverDefinition,
security_config::SecurityConfig,
source_types::SourceDefinition,
subscription_types::SubscriptionDefinition,
},
validation::CustomTypeRegistry,
};
pub const CURRENT_FRAISEQL_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProducerVersion(Option<String>);
impl Default for ProducerVersion {
fn default() -> Self {
Self(Some(CURRENT_FRAISEQL_VERSION.to_string()))
}
}
impl ProducerVersion {
#[must_use]
pub const fn unstamped() -> Self {
Self(None)
}
#[must_use]
pub fn get(&self) -> Option<&str> {
self.0.as_deref()
}
#[must_use]
pub const fn is_unstamped(&self) -> bool {
self.0.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubscribableEntity {
pub entity_type: String,
pub tables: Vec<String>,
#[serde(default, skip_serializing_if = "core::ops::Not::not")]
pub pre_image: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct CompiledSchema {
#[serde(default)]
pub types: Vec<TypeDefinition>,
#[serde(default)]
pub enums: Vec<EnumDefinition>,
#[serde(default)]
pub input_types: Vec<InputObjectDefinition>,
#[serde(default)]
pub interfaces: Vec<InterfaceDefinition>,
#[serde(default)]
pub unions: Vec<UnionDefinition>,
#[serde(default)]
pub queries: Vec<QueryDefinition>,
#[serde(default)]
pub mutations: Vec<MutationDefinition>,
#[serde(default)]
pub subscriptions: Vec<SubscriptionDefinition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub directives: Vec<DirectiveDefinition>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub fact_tables: HashMap<String, FactTableMetadata>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub observers: Vec<ObserverDefinition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sources: Vec<SourceDefinition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subscribable: Vec<SubscribableEntity>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub operation_cost_weights: HashMap<String, usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub federation: Option<FederationConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub security: Option<SecurityConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth: Option<AuthClientConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observers_config: Option<ObserversConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subscriptions_config: Option<SubscriptionsConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validation_config: Option<ValidationConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub debug_config: Option<DebugConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp_config: Option<McpConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rest_config: Option<RestConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grpc_config: Option<GrpcConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub changelog: Option<ChangelogConfig>,
#[serde(default)]
pub session_variables: SessionVariablesConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locale: Option<crate::schema::LocaleConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hierarchies_config: Option<HierarchiesConfig>,
#[serde(default)]
pub naming_convention: NamingConvention,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub naming_acronyms: Vec<String>,
#[serde(
default = "ProducerVersion::unstamped",
skip_serializing_if = "ProducerVersion::is_unstamped"
)]
pub fraiseql_version: ProducerVersion,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema_sdl: Option<String>,
#[serde(skip)]
pub derived: DerivedState,
}
#[derive(Debug, Clone, Default)]
pub struct DerivedState {
pub custom_scalars: CustomTypeRegistry,
pub query_index: HashMap<String, usize>,
pub mutation_index: HashMap<String, usize>,
pub subscription_index: HashMap<String, usize>,
pub where_relation_fields: fraiseql_db::where_clause::RelationFieldMaps,
pub fact_rows: Vec<crate::schema::TypeDefinition>,
pub fact_roots: Vec<crate::schema::fact_table_surface::FactRoot>,
}
impl PartialEq for DerivedState {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl CompiledSchema {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthClientConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pkce: Option<PkceClientConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub social: Option<SocialAuthConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<LocalAuthConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session: Option<SessionConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database: Option<AuthDatabaseConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthDatabaseConfig {
pub url_env: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pool_min: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pool_max: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acquire_timeout_secs: Option<u64>,
}
impl AuthDatabaseConfig {
pub const DEFAULT_ACQUIRE_TIMEOUT_SECS: u64 = 30;
pub const DEFAULT_POOL_MAX: u32 = 5;
#[must_use]
pub fn new(url_env: impl Into<String>) -> Self {
Self {
url_env: url_env.into(),
pool_min: None,
pool_max: None,
acquire_timeout_secs: None,
}
}
pub fn check(&self) -> Result<(), String> {
let name = self.url_env.trim();
if name.is_empty() {
return Err("[auth.database] url_env is empty: name the environment variable \
that holds the auth role's connection URL"
.to_string());
}
let is_name = name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& !name.starts_with(|c: char| c.is_ascii_digit());
if !is_name {
return Err("[auth.database] url_env is not an environment variable name: it \
names the variable that holds the URL, and must not hold the URL \
itself"
.to_string());
}
let max = self.pool_max.unwrap_or(Self::DEFAULT_POOL_MAX);
if max == 0 {
return Err("[auth.database] pool_max = 0 leaves the pool no connection".to_string());
}
if let Some(min) = self.pool_min {
if min > max {
return Err(format!("[auth.database] pool_min = {min} is above pool_max = {max}"));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub absolute_ttl_secs: Option<u64>,
}
impl SessionConfig {
pub const DEFAULT_TTL_SECS: u64 = 3_600;
pub const MIN_TTL_SECS: u64 = 60;
pub fn check(&self) -> Result<(), String> {
if let Some(ttl) = self.ttl_secs {
if ttl < Self::MIN_TTL_SECS {
return Err(format!(
"[auth.session] ttl_secs = {ttl} is under {} seconds: the token \
validator accepts a token for 60 seconds past its expiry, so a shorter \
lifetime does not mean what it says",
Self::MIN_TTL_SECS
));
}
}
if let Some(absolute) = self.absolute_ttl_secs {
let ttl = self.ttl_secs.unwrap_or(Self::DEFAULT_TTL_SECS);
if absolute <= ttl {
return Err(format!(
"[auth.session] absolute_ttl_secs = {absolute} is not longer than the \
access token's lifetime ({ttl} s): a session could never be refreshed"
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LocalAuthConfig {
#[serde(default)]
pub password: bool,
#[serde(default)]
pub otp: bool,
#[serde(default)]
pub mfa: bool,
#[serde(default)]
pub anonymous: bool,
#[serde(default)]
pub email_verification: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mfa_issuer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email_from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email_from_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<Box<super::LocalEmailConfig>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reset_url_template: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub magic_link_template: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub otp_allowed_query: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verification_url_template: Option<String>,
#[serde(default, skip_serializing_if = "SessionTransport::is_json")]
pub session_transport: SessionTransport,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SessionTransport {
#[default]
Json,
Cookie,
}
impl SessionTransport {
#[must_use]
pub const fn is_json(&self) -> bool {
matches!(self, Self::Json)
}
}
impl LocalAuthConfig {
pub fn check_session_transport(&self, session: Option<&SessionConfig>) -> Result<(), String> {
let refreshable = session.and_then(|s| s.absolute_ttl_secs).is_some();
if self.session_transport == SessionTransport::Cookie && !refreshable {
return Err("[auth.local] session_transport = \"cookie\" requires [auth.session] \
absolute_ttl_secs: it mounts the logout route, and without that \
nothing could clear the HttpOnly session cookies"
.to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PkceClientConfig {
pub discovery_url: String,
pub client_id: String,
pub client_secret_env: String,
pub server_redirect_uri: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SocialAuthConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub redirect_uri_allowlist: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub google: Option<GoogleSocialConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub github: Option<GitHubSocialConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub apple: Option<AppleSocialConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discord: Option<DiscordSocialConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub facebook: Option<FacebookSocialConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GoogleSocialConfig {
pub client_id: String,
pub client_secret_env: String,
pub redirect_uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discovery_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GitHubSocialConfig {
pub client_id: String,
pub client_secret_env: String,
pub redirect_uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_base_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AppleSocialConfig {
pub client_id: String,
pub team_id: String,
pub key_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub private_key_env: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub private_key_path: Option<String>,
pub redirect_uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DiscordSocialConfig {
pub client_id: String,
pub client_secret_env: String,
pub redirect_uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FacebookSocialConfig {
pub client_id: String,
pub client_secret_env: String,
pub redirect_uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph_base_url: Option<String>,
}