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};
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,
})
}
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
))
})
}