use std::sync::Arc;
use fraiseql_core::{db::postgres::PostgresAdapter, schema::CompiledSchema};
use crate::{Server, ServerConfig, capability::REGISTRY, schema::loader::FunctionsConfig};
#[allow(clippy::cognitive_complexity)] pub async fn build_postgres_server(
config: ServerConfig,
schema: CompiledSchema,
functions: Option<FunctionsConfig>,
) -> anyhow::Result<Server> {
refuse_functions_this_build_cannot_run(functions.as_ref())?;
let adapter = Arc::new(crate::server_config::build_postgres_adapter(&config).await?);
if config.validate_sql_sources {
use crate::sql_source_check as check;
let unbacked = check::find_unbacked_sources(&schema, adapter.as_ref()).await?;
let unusable = check::find_unusable_sources(&schema, adapter.as_ref()).await?;
if !unbacked.is_empty() || !unusable.is_empty() {
let role = check::connected_role(adapter.as_ref()).await?;
anyhow::bail!("{}", check::format_source_problems(&unbacked, &unusable, &role));
}
tracing::info!(
sources = schema.queries.len() + schema.mutations.len(),
"sql_source validation passed: all declared sources are backed"
);
}
let db_pool = build_observer_pool(&config).await?;
let storage_state = crate::server_config::build_storage_state(&config)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
if let Some(state) = &storage_state {
tracing::info!(
buckets = state.buckets.load().len(),
"Object storage configured; mounting /storage/v1/*"
);
}
let pg_revocation_manager =
crate::token_revocation::build_postgres_revocation_manager(&config.database_url, &schema)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
let tenancy_runtime_enabled = config.tenancy.runtime.enabled;
let database_tls = config.postgres_tls().map_err(|e| anyhow::anyhow!(e))?;
let tenant_read_replica_policy = config.read_replica_policy();
let tenant_vector_scan = config.vector_scan();
#[cfg(feature = "arrow")]
{
use crate::arrow::create_flight_service;
let flight_service = create_flight_service(adapter.clone(), &config.flight_upload_tables);
crate::arrow::register_flight_views(&flight_service, &config.flight_views).await;
if config.flight_upload_tables.is_empty() {
tracing::info!(
"Arrow Flight service initialized with real database adapter (Upload disabled: \
flight_upload_tables is empty)"
);
} else {
tracing::warn!(
tables = ?config.flight_upload_tables,
"Arrow Flight service initialized with real database adapter; Upload is ENABLED \
for these tables — clients may INSERT into them without passing the mutation \
pipeline's authorizer or field RBAC"
);
}
let server =
Server::with_flight_service(config, schema, adapter, db_pool, Some(flight_service))
.await?;
#[cfg(feature = "functions-runtime")]
let server = server.with_functions_config(functions);
let tenant_factory = tenancy_runtime_enabled.then(|| {
crate::tenancy::make_executor_factory::<
fraiseql_core::cache::CachedDatabaseAdapter<PostgresAdapter>,
>(
database_tls.clone(), tenant_read_replica_policy.clone(), tenant_vector_scan
)
});
let server = match storage_state {
Some(state) => server.with_storage_state(state),
None => server,
};
let server = match tenant_factory {
Some(factory) => server.with_tenant_executor_factory(factory),
None => server,
};
let server = match pg_revocation_manager {
Some(manager) => server.with_revocation_manager(manager),
None => server,
};
let server = enable_rest_writes(server);
#[allow(clippy::needless_return)]
return Ok(server);
}
#[cfg(not(feature = "arrow"))]
{
let has_relay_queries = schema.queries.iter().any(|q| q.relay);
let server = if has_relay_queries {
Server::with_relay_pagination(config, schema, adapter, db_pool).await?
} else {
Server::new(config, schema, adapter, db_pool).await?
};
#[cfg(feature = "functions-runtime")]
let server = server.with_functions_config(functions);
let tenant_factory = tenancy_runtime_enabled.then(|| {
crate::tenancy::make_executor_factory::<
fraiseql_core::cache::CachedDatabaseAdapter<PostgresAdapter>,
>(
database_tls.clone(), tenant_read_replica_policy.clone(), tenant_vector_scan
)
});
let server = match storage_state {
Some(state) => server.with_storage_state(state),
None => server,
};
let server = match tenant_factory {
Some(factory) => server.with_tenant_executor_factory(factory),
None => server,
};
let server = match pg_revocation_manager {
Some(manager) => server.with_revocation_manager(manager),
None => server,
};
let server = enable_rest_writes(server);
Ok(server)
}
}
pub fn refuse_functions_this_build_cannot_run(
functions: Option<&FunctionsConfig>,
) -> anyhow::Result<()> {
let Some(entry) = REGISTRY.iter().find(|entry| entry.meta.name == "functions") else {
anyhow::bail!("the capability registry has no `functions` entry");
};
let declared: Vec<&str> = functions
.iter()
.flat_map(|functions| &functions.definitions)
.map(|definition| definition.name.as_str())
.collect();
if entry.compiled_in || declared.is_empty() {
return Ok(());
}
anyhow::bail!(
"the schema declares functions this build cannot run ({}): it was built without the \
`{}` feature, so none of them would ever fire. To run them, {}.",
declared.join(", "),
entry.meta.feature,
entry.meta.remedy()
)
}
#[cfg(feature = "observers")]
async fn build_observer_pool(config: &ServerConfig) -> anyhow::Result<Option<sqlx::PgPool>> {
use std::time::Duration;
use sqlx::postgres::PgPoolOptions;
let pool_cfg = config.observers.as_ref().map(|o| o.runtime.pool.clone()).unwrap_or_default();
tracing::info!(
min = pool_cfg.min_connections,
max = pool_cfg.max_connections,
timeout_secs = pool_cfg.acquire_timeout_secs,
"Initializing observer PostgreSQL pool"
);
let pool = PgPoolOptions::new()
.min_connections(pool_cfg.min_connections)
.max_connections(pool_cfg.max_connections)
.acquire_timeout(Duration::from_secs(pool_cfg.acquire_timeout_secs))
.connect(&config.database_url)
.await?;
Ok(Some(pool))
}
#[cfg(not(feature = "observers"))]
#[allow(clippy::unused_async)] async fn build_observer_pool(_config: &ServerConfig) -> anyhow::Result<Option<sqlx::PgPool>> {
Ok(None)
}
#[cfg(feature = "rest")]
fn enable_rest_writes(server: Server) -> Server {
server.with_rest_write_surface()
}
#[cfg(not(feature = "rest"))]
const fn enable_rest_writes(server: Server) -> Server {
server
}