athena_rs 3.3.0

Database gateway API
Documentation
use serde_json::Value;

use super::constants::DEFAULT_RAILWAY_GRAPHQL_URL;
use super::error::ProvisioningError;
use super::payload_json::extract_connection_uri;
use super::types::{
    RailwayConnectionParams, RailwayPluginCreateParams, RailwayPluginCreateResult,
    RailwayProjectCreateParams, RailwayProjectCreateResult, RailwayServiceCreateParams,
    RailwayServiceCreateResult,
};

/// Create a Railway project using GraphQL.
pub async fn create_railway_project(
    params: RailwayProjectCreateParams,
) -> Result<RailwayProjectCreateResult, ProvisioningError> {
    let query = r#"
mutation projectCreate($input: ProjectCreateInput!) {
  projectCreate(input: $input) {
    id
    baseEnvironmentId
    name
  }
}
"#;

    let body: Value = railway_graphql_request(
        &params.api_key,
        params.graphql_url.as_deref(),
        query,
        serde_json::json!({ "input": params.project_input }),
    )
    .await?;

    let data: &Value = body.get("data").unwrap_or(&body);
    let project: &Value = data.get("projectCreate").unwrap_or(data);
    let project_id: String = project
        .get("id")
        .and_then(Value::as_str)
        .map(str::to_string)
        .ok_or_else(|| {
            ProvisioningError::Execution(format!(
                "railway projectCreate response missing id: {}",
                body
            ))
        })?;

    let base_environment_id: Option<String> = project
        .get("baseEnvironmentId")
        .and_then(Value::as_str)
        .map(str::to_string);

    Ok(RailwayProjectCreateResult {
        project_id,
        base_environment_id,
        raw: body,
    })
}

/// Create a Railway service using GraphQL.
pub async fn create_railway_service(
    params: RailwayServiceCreateParams,
) -> Result<RailwayServiceCreateResult, ProvisioningError> {
    let query = r#"
mutation serviceCreate($input: ServiceCreateInput!) {
  serviceCreate(input: $input) {
    id
    name
    projectId
  }
}
"#;

    let body: Value = railway_graphql_request(
        &params.api_key,
        params.graphql_url.as_deref(),
        query,
        serde_json::json!({ "input": params.service_input }),
    )
    .await?;

    let data: &Value = body.get("data").unwrap_or(&body);
    let service: &Value = data.get("serviceCreate").unwrap_or(data);
    let service_id: String = service
        .get("id")
        .and_then(Value::as_str)
        .map(str::to_string)
        .ok_or_else(|| {
            ProvisioningError::Execution(format!(
                "railway serviceCreate response missing id: {}",
                body
            ))
        })?;

    Ok(RailwayServiceCreateResult {
        service_id,
        raw: body,
    })
}

/// Create a Railway plugin (e.g. Postgres) using GraphQL.
pub async fn create_railway_plugin(
    params: RailwayPluginCreateParams,
) -> Result<RailwayPluginCreateResult, ProvisioningError> {
    let query = r#"
mutation pluginCreate($input: PluginCreateInput!) {
  pluginCreate(input: $input) {
    id
    name
    status
  }
}
"#;

    let body = railway_graphql_request(
        &params.api_key,
        params.graphql_url.as_deref(),
        query,
        serde_json::json!({ "input": params.plugin_input }),
    )
    .await?;

    let data = body.get("data").unwrap_or(&body);
    let plugin = data.get("pluginCreate").unwrap_or(data);
    let plugin_id = plugin
        .get("id")
        .and_then(Value::as_str)
        .map(str::to_string)
        .ok_or_else(|| {
            ProvisioningError::Execution(format!(
                "railway pluginCreate response missing id: {}",
                body
            ))
        })?;

    Ok(RailwayPluginCreateResult {
        plugin_id,
        raw: body,
    })
}

/// Resolve Railway base environment id for a project.
pub async fn fetch_railway_project_base_environment_id(
    api_key: &str,
    project_id: &str,
    graphql_url: Option<&str>,
) -> Result<Option<String>, ProvisioningError> {
    if api_key.trim().is_empty() {
        return Err(ProvisioningError::InvalidInput(
            "railway api_key must not be empty".to_string(),
        ));
    }
    if project_id.trim().is_empty() {
        return Err(ProvisioningError::InvalidInput(
            "railway project_id must not be empty".to_string(),
        ));
    }

    let query = r#"
query project($id: String!) {
  project(id: $id) {
    id
    baseEnvironmentId
  }
}
"#;

    let body: Value = railway_graphql_request(
        api_key,
        graphql_url,
        query,
        serde_json::json!({ "id": project_id }),
    )
    .await?;
    let base_environment_id = body
        .pointer("/data/project/baseEnvironmentId")
        .or_else(|| body.pointer("/project/baseEnvironmentId"))
        .and_then(Value::as_str)
        .map(str::to_string);
    Ok(base_environment_id)
}

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

    let graphql_url = params
        .graphql_url
        .unwrap_or_else(|| DEFAULT_RAILWAY_GRAPHQL_URL.to_string());
    let query = r#"
query variables($environmentId: String!, $pluginId: String, $projectId: String!, $serviceId: String, $unrendered: Boolean) {
  variables(
    environmentId: $environmentId
    pluginId: $pluginId
    projectId: $projectId
    serviceId: $serviceId
    unrendered: $unrendered
  )
}
"#;

    let variables = serde_json::json!({
        "environmentId": params.environment_id,
        "pluginId": params.plugin_id,
        "projectId": params.project_id,
        "serviceId": params.service_id,
        "unrendered": false
    });

    let body =
        railway_graphql_request(&params.api_key, Some(&graphql_url), query, variables).await?;

    let data = body.get("data").unwrap_or(&body);
    extract_connection_uri(data).ok_or_else(|| {
        ProvisioningError::Execution(format!(
            "railway api response did not include a postgres connection URI: {}",
            body
        ))
    })
}

async fn railway_graphql_request(
    api_key: &str,
    graphql_url: Option<&str>,
    query: &str,
    variables: Value,
) -> Result<Value, ProvisioningError> {
    if api_key.trim().is_empty() {
        return Err(ProvisioningError::InvalidInput(
            "railway api_key must not be empty".to_string(),
        ));
    }
    let url = graphql_url.unwrap_or(DEFAULT_RAILWAY_GRAPHQL_URL);
    let payload = serde_json::json!({
        "query": query,
        "variables": variables,
    });

    let response = reqwest::Client::new()
        .post(url)
        .bearer_auth(api_key)
        .json(&payload)
        .send()
        .await
        .map_err(|err| {
            ProvisioningError::Execution(format!("railway api request failed: {}", err))
        })?;

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

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

    if let Some(errors) = body.get("errors") {
        return Err(ProvisioningError::Execution(format!(
            "railway api returned graphql errors: {}",
            errors
        )));
    }

    Ok(body)
}