grafbase_local_backend/api/
create.rs

1use super::client::create_client;
2use super::consts::{API_URL, PROJECT_METADATA_FILE};
3use super::deploy;
4use super::errors::{ApiError, CreateError};
5use super::graphql::mutations::{
6    CurrentPlanLimitReachedError, DuplicateDatabaseRegionsError, InvalidDatabaseRegionsError, ProjectCreate,
7    ProjectCreateArguments, ProjectCreateInput, ProjectCreatePayload, ProjectCreateSuccess, SlugTooLongError,
8};
9use super::graphql::queries::viewer_and_regions::{PersonalAccount, ViewerAndRegions};
10use super::types::{Account, DatabaseRegion, ProjectMetadata};
11use super::utils::project_linked;
12use common::environment::Environment;
13use cynic::http::ReqwestExt;
14use cynic::Id;
15use cynic::{MutationBuilder, QueryBuilder};
16use std::iter;
17use tokio::fs;
18
19/// # Errors
20///
21/// See [`ApiError`]
22pub async fn get_viewer_data_for_creation() -> Result<(Vec<Account>, Vec<DatabaseRegion>, DatabaseRegion), ApiError> {
23    // TODO consider if we want to do this elsewhere
24    if project_linked().await? {
25        return Err(ApiError::ProjectAlreadyLinked);
26    }
27
28    let client = create_client().await?;
29
30    let query = ViewerAndRegions::build(());
31
32    let response = client.post(API_URL).run_graphql(query).await?;
33
34    let response = response.data.expect("must exist");
35
36    let viewer_response = response.viewer.ok_or(ApiError::UnauthorizedOrDeletedUser)?;
37
38    let closest_region = response
39        .closest_database_region
40        .ok_or(ApiError::UnauthorizedOrDeletedUser)?
41        .into();
42
43    let available_regions = response.database_regions.into_iter().map(Into::into).collect();
44
45    let PersonalAccount { id, name, slug } = viewer_response
46        .personal_account
47        .ok_or(ApiError::IncorrectlyScopedToken)?;
48
49    let personal_account_id = id;
50
51    let personal_account = Account {
52        id: personal_account_id.inner().to_owned(),
53        name,
54        slug,
55        personal: true,
56    };
57
58    let accounts = iter::once(personal_account)
59        .chain(viewer_response.organizations.nodes.iter().map(|organization| Account {
60            id: organization.id.inner().to_owned(),
61            name: organization.name.clone(),
62            slug: organization.slug.clone(),
63            personal: false,
64        }))
65        .collect();
66
67    Ok((accounts, available_regions, closest_region))
68}
69
70/// # Errors
71///
72/// See [`ApiError`]
73pub async fn create(account_id: &str, project_slug: &str, database_regions: &[&str]) -> Result<Vec<String>, ApiError> {
74    let environment = Environment::get();
75
76    match environment.project_dot_grafbase_path.try_exists() {
77        Ok(true) => {}
78        Ok(false) => fs::create_dir_all(&environment.project_dot_grafbase_path)
79            .await
80            .map_err(ApiError::CreateProjectDotGrafbaseFolder)?,
81        Err(error) => return Err(ApiError::ReadProjectDotGrafbaseFolder(error)),
82    }
83
84    let client = create_client().await?;
85
86    let operation = ProjectCreate::build(ProjectCreateArguments {
87        input: ProjectCreateInput {
88            account_id: Id::new(account_id),
89            database_regions: database_regions.iter().map(ToString::to_string).collect(),
90            project_slug: project_slug.to_owned(),
91        },
92    });
93
94    let response = client.post(API_URL).run_graphql(operation).await?;
95
96    let payload = response.data.ok_or(ApiError::UnauthorizedOrDeletedUser)?.project_create;
97
98    match payload {
99        ProjectCreatePayload::ProjectCreateSuccess(ProjectCreateSuccess { project, .. }) => {
100            let project_metadata_path = environment.project_dot_grafbase_path.join(PROJECT_METADATA_FILE);
101
102            tokio::fs::write(
103                &project_metadata_path,
104                ProjectMetadata {
105                    account_id: account_id.to_owned(),
106                    project_id: project.id.into_inner().clone(),
107                }
108                .to_string(),
109            )
110            .await
111            .map_err(ApiError::WriteProjectMetadataFile)?;
112
113            deploy::deploy().await?;
114
115            Ok(project.production_branch.domains)
116        }
117        ProjectCreatePayload::SlugAlreadyExistsError(_) => Err(CreateError::SlugAlreadyExists.into()),
118        ProjectCreatePayload::SlugInvalidError(_) => Err(CreateError::SlugInvalid.into()),
119        ProjectCreatePayload::SlugTooLongError(SlugTooLongError { max_length, .. }) => {
120            Err(CreateError::SlugTooLong { max_length }.into())
121        }
122        ProjectCreatePayload::AccountDoesNotExistError(_) => Err(CreateError::AccountDoesNotExist.into()),
123        ProjectCreatePayload::CurrentPlanLimitReachedError(CurrentPlanLimitReachedError { max, .. }) => {
124            Err(CreateError::CurrentPlanLimitReached { max }.into())
125        }
126        ProjectCreatePayload::DuplicateDatabaseRegionsError(DuplicateDatabaseRegionsError { duplicates, .. }) => {
127            Err(CreateError::DuplicateDatabaseRegions { duplicates }.into())
128        }
129        ProjectCreatePayload::EmptyDatabaseRegionsError(_) => Err(CreateError::EmptyDatabaseRegions.into()),
130        ProjectCreatePayload::InvalidDatabaseRegionsError(InvalidDatabaseRegionsError { invalid, .. }) => {
131            Err(CreateError::InvalidDatabaseRegions { invalid }.into())
132        }
133        ProjectCreatePayload::Unknown => Err(CreateError::Unknown.into()),
134    }
135}