use std::{collections::HashMap, sync::Arc};
use fraiseql_storage::{
StorageMetadataRepo, StorageRlsEvaluator, StorageState,
config::{BucketAccess, BucketConfig, StorageConfig},
};
use sqlx::postgres::PgPoolOptions;
use super::{ServerConfig, StorageSectionConfig};
const STORAGE_METADATA_POOL_MAX: u32 = 5;
#[derive(Debug, Clone)]
pub struct ResolvedStorage {
pub backend: StorageConfig,
pub bucket: BucketConfig,
}
pub fn resolve_storage_section(config: &ServerConfig) -> Result<Option<ResolvedStorage>, String> {
resolve_from_map(&config.storage)
}
fn resolve_from_map(
storage: &HashMap<String, StorageSectionConfig>,
) -> Result<Option<ResolvedStorage>, String> {
let mut iter = storage.iter();
let Some((name, section)) = iter.next() else {
return Ok(None);
};
if iter.next().is_some() {
let mut names: Vec<&str> = storage.keys().map(String::as_str).collect();
names.sort_unstable();
return Err(format!(
"multiple [storage.<name>] sections configured ({}); the fraiseql-server binary \
currently supports a single storage backend — configure exactly one [storage.<name>].",
names.join(", "),
));
}
let access = parse_access(section.access.as_deref())?;
let backend = StorageConfig {
backend: section.backend.clone(),
path: section.path.clone(),
bucket: section.bucket.clone(),
region: section.region.clone(),
endpoint: section.endpoint.clone(),
project_id: section.project_id.clone(),
account_name: section.account_name.clone(),
};
let bucket = BucketConfig {
name: name.clone(),
max_object_bytes: section.max_object_bytes,
allowed_mime_types: section.allowed_mime_types.clone(),
access,
transform_presets: None,
serve_inline: section.serve_inline.unwrap_or(false),
};
Ok(Some(ResolvedStorage { backend, bucket }))
}
pub async fn build_storage_state(config: &ServerConfig) -> Result<Option<StorageState>, String> {
let Some(resolved) = resolve_storage_section(config)? else {
return Ok(None);
};
let bucket_name = resolved.bucket.name.clone();
let pool = PgPoolOptions::new()
.max_connections(STORAGE_METADATA_POOL_MAX)
.connect(&config.database_url)
.await
.map_err(|e| {
format!("storage: failed to connect to PostgreSQL for object metadata: {e}")
})?;
sqlx::raw_sql(fraiseql_storage::migrations::storage_migration_sql())
.execute(&pool)
.await
.map_err(|e| format!("storage: failed to ensure the object-metadata table exists: {e}"))?;
let backend = fraiseql_storage::create_backend(&resolved.backend).await.map_err(|e| {
format!("storage: failed to create backend for bucket '{bucket_name}': {e}")
})?;
let mut buckets = HashMap::new();
buckets.insert(bucket_name, resolved.bucket);
Ok(Some(StorageState {
backend: Arc::new(backend),
metadata: Arc::new(StorageMetadataRepo::new(pool)),
rls: StorageRlsEvaluator::new(),
buckets: Arc::new(buckets),
}))
}
fn parse_access(access: Option<&str>) -> Result<BucketAccess, String> {
let Some(value) = access else {
return Ok(BucketAccess::Private);
};
match value.to_ascii_lowercase().as_str() {
"private" => Ok(BucketAccess::Private),
"public_read" | "public-read" => Ok(BucketAccess::PublicRead),
other => Err(format!(
"invalid storage access policy {other:?}; expected \"private\" or \"public_read\""
)),
}
}