use std::sync::Arc;
use fraiseql_core::db::traits::DatabaseAdapter;
use super::{Server, ServerError};
use crate::{schema::loader::CompiledSchemaLoader, subsystems::loader::build_functions_subsystem};
impl<A: DatabaseAdapter + Clone + Send + Sync + 'static> Server<A> {
pub(super) async fn prepare_functions_runtime(&mut self) -> Result<(), ServerError> {
let extended = CompiledSchemaLoader::new(&self.config.schema_path)
.load_extended()
.await
.map_err(|error| {
ServerError::ConfigError(format!("failed to load functions config: {error}"))
})?;
let Some(functions_config) = extended.functions else {
return Ok(()); };
if functions_config.definitions.is_empty() {
return Ok(());
}
let subsystem = build_functions_subsystem(functions_config).map_err(|error| {
ServerError::ConfigError(format!("functions-runtime setup failed: {error}"))
})?;
let idempotency_key = self.build_idempotency_key();
if idempotency_key.is_some() {
tracing::info!("idempotency tokens are HMAC-signed (VERP-ready send-ids)");
}
let mut hooks =
subsystem.into_before_mutation_hooks().with_idempotency_key(idempotency_key);
if let Some((resolver, transport)) = self.build_send_email_wiring().await? {
hooks = hooks.with_email(resolver, transport);
tracing::info!(
"send_email host op enabled (host-owned from, per-connected-account SMTP)"
);
}
let function_count = hooks.module_registry.len();
self.functions_hooks = Some(Arc::new(hooks));
tracing::info!(functions = function_count, "functions-runtime dispatch enabled");
Ok(())
}
fn build_idempotency_key(&self) -> Option<Arc<[u8]>> {
let env_name = self.config.hmac_secret_env.as_deref()?;
if let Some(secret) = std::env::var(env_name).ok().filter(|secret| !secret.is_empty()) {
let subkey = fraiseql_observers::derive_idempotency_subkey(secret.as_bytes());
Some(Arc::from(subkey.as_slice()))
} else {
tracing::warn!(
env = env_name,
"hmac_secret_env is set but the environment variable is empty/unset — \
idempotency tokens stay unsigned and VERP send-correlation is disabled"
);
None
}
}
#[allow(clippy::unused_async)]
async fn build_send_email_wiring(
&self,
) -> Result<
Option<(
Arc<dyn fraiseql_functions::SenderIdentityResolver>,
Arc<dyn fraiseql_functions::EmailTransport>,
)>,
ServerError,
> {
#[cfg(feature = "inbound-email")]
{
let tracker = match self.db_pool.as_ref() {
Some(pool) => {
let tracker = crate::inbound::email::PgSendTracker::new(pool.clone());
tracker.init().await.map_err(|error| {
ServerError::ConfigError(format!(
"failed to initialize send-tracking schema: {error}"
))
})?;
Some(Arc::new(tracker) as Arc<dyn crate::inbound::email::SendTracker>)
},
None => None,
};
let address_hash_key = self.build_address_hash_key();
let Some(transport) = crate::inbound::email::build_email_transport(
&self.config.mailbox,
|name| std::env::var(name).ok(),
tracker,
address_hash_key,
) else {
return Ok(None);
};
Ok(Some((self.build_sender_resolver(), transport)))
}
#[cfg(not(feature = "inbound-email"))]
{
Ok(None)
}
}
#[cfg(feature = "inbound-email")]
pub(super) fn build_address_hash_key(&self) -> Option<Arc<[u8]>> {
let env_name = self.config.hmac_secret_env.as_deref()?;
let secret = std::env::var(env_name).ok().filter(|secret| !secret.is_empty())?;
let key = fraiseql_observers::derive_address_hash_key(secret.as_bytes());
Some(Arc::from(key.as_slice()))
}
#[cfg(feature = "inbound-email")]
fn build_sender_resolver(&self) -> Arc<dyn fraiseql_functions::SenderIdentityResolver> {
#[cfg(feature = "auth")]
if let Some(sender) =
self.config.identity.as_ref().and_then(|identity| identity.sender.as_ref())
{
if sender.enabled {
if let Some(pool) = self.enrichment_pool.as_ref() {
let resolver = crate::identity::resolver::IdentityResolver::postgres(
sender.clone(),
pool.clone(),
);
return Arc::new(crate::identity::sender::DbSenderIdentityResolver::new(
resolver,
SENDING_ADDRESS_FIELD,
Some(DISPLAY_NAME_FIELD.to_string()),
));
}
tracing::warn!(
"[identity.sender] is enabled but no auth database pool is available — \
send_email falls back to the login-email sender"
);
}
}
Arc::new(fraiseql_functions::LoginEmailSender)
}
}
#[cfg(all(feature = "inbound-email", feature = "auth"))]
const SENDING_ADDRESS_FIELD: &str = "sending_address";
#[cfg(all(feature = "inbound-email", feature = "auth"))]
const DISPLAY_NAME_FIELD: &str = "display_name";