use std::collections::HashSet;
use tracing::info;
use super::{Result, Server, ServerError};
use crate::capability::REGISTRY;
#[allow(dead_code)] pub struct BootCx<'a> {
pub server: &'a Server,
}
pub struct ProvisionStep {
pub(super) name: &'static str,
pub(super) pool: sqlx::PgPool,
pub(super) scripts: Vec<&'static str>,
}
impl ProvisionStep {
pub fn new(name: &'static str, pool: &sqlx::PgPool, scripts: &[&'static str]) -> Self {
Self {
name,
pool: pool.clone(),
scripts: scripts.to_vec(),
}
}
}
pub(super) fn without_repeats(steps: Vec<ProvisionStep>) -> Vec<ProvisionStep> {
let mut seen = HashSet::new();
steps
.into_iter()
.map(|mut step| {
step.scripts.retain(|script| seen.insert(*script));
step
})
.filter(|step| !step.scripts.is_empty())
.collect()
}
pub(super) async fn run(steps: Vec<ProvisionStep>) -> Result<()> {
for step in without_repeats(steps) {
crate::migration_lock::run_migrations(&step.pool, &step.scripts)
.await
.map_err(|e| {
ServerError::Database(format!("Failed to provision the {} schema: {e}", step.name))
})?;
info!(step = step.name, scripts = step.scripts.len(), "Boot schema ready");
}
Ok(())
}
impl Server {
pub(super) fn provision_steps(&self) -> Vec<ProvisionStep> {
let mut steps = Vec::new();
if let Some(rbac) = self.rbac.as_ref() {
steps.push(ProvisionStep::new(
"RBAC",
rbac.pool(),
&[crate::api::rbac_management::db_backend::RBAC_SCHEMA_SQL],
));
}
if let Some(tokens) = self.admin_tokens.as_ref() {
steps.push(ProvisionStep::new(
"admin-token",
tokens.pool(),
&[crate::api::admin_principal::SCHEMA_SQL],
));
}
if let Some(store) = self.api_key_authenticator.as_ref().and_then(|a| a.postgres_store()) {
steps.push(ProvisionStep::new(
"API-key",
store.pool(),
&[crate::api_key::postgres::PG_API_KEY_SCHEMA_SQL],
));
}
#[cfg(feature = "auth")]
self.push_auth_steps(&mut steps);
if let (Some(_), Some(pool)) = (self.async_operations.as_ref(), self.db_pool.as_ref()) {
steps.push(ProvisionStep::new(
"async operations",
pool,
&[crate::async_operations::store::ASYNC_OPERATIONS_SCHEMA_SQL],
));
}
let cx = BootCx { server: self };
for registration in REGISTRY.iter().filter(|registration| registration.compiled_in) {
steps.extend(registration.capability.provision(&cx));
}
steps
}
#[cfg(feature = "auth")]
fn push_auth_steps(&self, steps: &mut Vec<ProvisionStep>) {
use fraiseql_auth::{account_linking, session_postgres::SESSIONS_SCHEMA_SQL};
let Some(pool) = self.enrichment_pool.as_ref() else {
return;
};
#[cfg(feature = "auth-saml")]
if self.saml_state.is_some() {
steps.push(ProvisionStep::new(
"SAML login",
pool,
&[SESSIONS_SCHEMA_SQL, account_linking::SCHEMA_SQL],
));
}
if self.config.scim.as_ref().is_some_and(|scim| scim.enabled) {
steps.push(ProvisionStep::new(
"SCIM",
pool,
&[
account_linking::SCHEMA_SQL,
SESSIONS_SCHEMA_SQL,
fraiseql_auth::scim::PG_SCIM_SCHEMA_SQL,
],
));
}
if self.otp_state.is_some() {
steps.push(ProvisionStep::new(
"[auth.local] OTP",
pool,
&[
SESSIONS_SCHEMA_SQL,
account_linking::SCHEMA_SQL,
fraiseql_auth::PG_OTP_SCHEMA_SQL,
],
));
}
if self.mfa_state.is_some() {
steps.push(ProvisionStep::new(
"[auth.local] MFA",
pool,
&[
SESSIONS_SCHEMA_SQL,
account_linking::SCHEMA_SQL,
fraiseql_auth::PG_MFA_SCHEMA_SQL,
],
));
}
if self.local_password_state.is_some() {
steps.push(ProvisionStep::new(
"[auth.local] password",
pool,
&[
account_linking::SCHEMA_SQL,
fraiseql_auth::PASSWORD_SCHEMA_SQL,
fraiseql_auth::PASSWORD_RESET_SCHEMA_SQL,
fraiseql_auth::EMAIL_VERIFICATION_SCHEMA_SQL,
SESSIONS_SCHEMA_SQL,
],
));
}
if self.anon_signup_state.is_some() {
steps.push(ProvisionStep::new("[auth.local] anonymous", pool, &[SESSIONS_SCHEMA_SQL]));
}
if self.social_login.is_some() {
steps.push(ProvisionStep::new(
"social login",
pool,
&[SESSIONS_SCHEMA_SQL, account_linking::SCHEMA_SQL],
));
}
}
}