grafbase_local_backend/api/
create.rs1use 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
19pub async fn get_viewer_data_for_creation() -> Result<(Vec<Account>, Vec<DatabaseRegion>, DatabaseRegion), ApiError> {
23 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
70pub 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}