athena_rs 3.3.0

Database gateway API
Documentation
use serde_json::Value;
use uuid::Uuid;

use super::constants::DEFAULT_NEON_API_BASE_URL;
use super::error::ProvisioningError;
use super::payload_json::extract_connection_uri;
use super::types::{NeonConnectionParams, NeonProjectCreateParams, NeonProjectCreateResult};

/// Create a Neon project and return identifiers for subsequent provisioning.
pub async fn create_neon_project(
    params: NeonProjectCreateParams,
) -> Result<NeonProjectCreateResult, ProvisioningError> {
    if params.api_key.trim().is_empty() {
        return Err(ProvisioningError::InvalidInput(
            "neon api_key must not be empty".to_string(),
        ));
    }

    let base: String = params
        .api_base_url
        .unwrap_or_else(|| DEFAULT_NEON_API_BASE_URL.to_string());
    let url = format!("{}/projects", base);

    let payload: Value = if let Some(payload) = params.project_payload {
        payload
    } else {
        let name: String = params
            .project_name
            .filter(|value| !value.trim().is_empty())
            .unwrap_or_else(|| format!("athena-{}", Uuid::new_v4().simple()));
        serde_json::json!({
            "project": {
                "name": name
            }
        })
    };

    let response: reqwest::Response = reqwest::Client::new()
        .post(url)
        .bearer_auth(params.api_key)
        .json(&payload)
        .send()
        .await
        .map_err(|err| ProvisioningError::Execution(format!("neon api request failed: {}", err)))?;
    let status: reqwest::StatusCode = response.status();
    let body: Value = response.json().await.map_err(|err| {
        ProvisioningError::Execution(format!("failed to parse neon api response: {}", err))
    })?;

    if !status.is_success() {
        return Err(ProvisioningError::Execution(format!(
            "neon api returned status {}: {}",
            status, body
        )));
    }

    let project_id: String = body
        .pointer("/project/id")
        .or_else(|| body.pointer("/id"))
        .and_then(Value::as_str)
        .map(str::to_string)
        .ok_or_else(|| {
            ProvisioningError::Execution(format!(
                "neon project create response missing project id: {}",
                body
            ))
        })?;

    let branch_id: Option<String> = body
        .pointer("/project/default_branch_id")
        .or_else(|| body.pointer("/project/default_branch/id"))
        .and_then(Value::as_str)
        .map(str::to_string);

    Ok(NeonProjectCreateResult {
        project_id,
        branch_id,
        raw: body,
    })
}

/// Resolve a Neon Postgres connection URI from Neon API.
pub async fn fetch_neon_connection_uri(
    params: NeonConnectionParams,
) -> Result<String, ProvisioningError> {
    if params.api_key.trim().is_empty() {
        return Err(ProvisioningError::InvalidInput(
            "neon api_key must not be empty".to_string(),
        ));
    }
    if params.project_id.trim().is_empty() {
        return Err(ProvisioningError::InvalidInput(
            "neon project_id must not be empty".to_string(),
        ));
    }

    let base = params
        .api_base_url
        .unwrap_or_else(|| DEFAULT_NEON_API_BASE_URL.to_string());
    let url = format!("{}/projects/{}/connection_uri", base, params.project_id);

    let client = reqwest::Client::new();
    let mut req = client.get(url).bearer_auth(params.api_key);
    if let Some(branch_id) = params
        .branch_id
        .as_ref()
        .filter(|value| !value.trim().is_empty())
    {
        req = req.query(&[("branch_id", branch_id)]);
    }
    if let Some(database_name) = params
        .database_name
        .as_ref()
        .filter(|value| !value.trim().is_empty())
    {
        req = req.query(&[("database_name", database_name)]);
    }
    if let Some(role_name) = params
        .role_name
        .as_ref()
        .filter(|value| !value.trim().is_empty())
    {
        req = req.query(&[("role_name", role_name)]);
    }
    if let Some(endpoint_id) = params
        .endpoint_id
        .as_ref()
        .filter(|value| !value.trim().is_empty())
    {
        req = req.query(&[("endpoint_id", endpoint_id)]);
    }

    let response = req
        .send()
        .await
        .map_err(|err| ProvisioningError::Execution(format!("neon api request failed: {}", err)))?;
    let status = response.status();
    let body: Value = response.json().await.map_err(|err| {
        ProvisioningError::Execution(format!("failed to parse neon api response: {}", err))
    })?;

    if !status.is_success() {
        return Err(ProvisioningError::Execution(format!(
            "neon api returned status {}: {}",
            status, body
        )));
    }

    extract_connection_uri(&body).ok_or_else(|| {
        ProvisioningError::Execution(format!(
            "neon api response did not include a postgres connection URI: {}",
            body
        ))
    })
}