1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
use super::client::create_client;
use super::consts::{API_URL, PROJECT_METADATA_FILE};
use super::deploy;
use super::errors::{ApiError, CreateError};
use super::graphql::mutations::{
    CurrentPlanLimitReachedError, DuplicateDatabaseRegionsError, InvalidDatabaseRegionsError, ProjectCreate,
    ProjectCreateArguments, ProjectCreateInput, ProjectCreatePayload, ProjectCreateSuccess, SlugTooLongError,
};
use super::graphql::queries::viewer_and_regions::{PersonalAccount, ViewerAndRegions};
use super::types::{Account, DatabaseRegion, ProjectMetadata};
use super::utils::project_linked;
use common::environment::Environment;
use cynic::http::ReqwestExt;
use cynic::Id;
use cynic::{MutationBuilder, QueryBuilder};
use std::iter;
use tokio::fs;

/// # Errors
///
/// See [`ApiError`]
pub async fn get_viewer_data_for_creation() -> Result<(Vec<Account>, Vec<DatabaseRegion>, DatabaseRegion), ApiError> {
    // TODO consider if we want to do this elsewhere
    if project_linked().await? {
        return Err(ApiError::ProjectAlreadyLinked);
    }

    let client = create_client().await?;

    let query = ViewerAndRegions::build(());

    let response = client.post(API_URL).run_graphql(query).await?;

    let response = response.data.expect("must exist");

    let viewer_response = response.viewer.ok_or(ApiError::UnauthorizedOrDeletedUser)?;

    let closest_region = response
        .closest_database_region
        .ok_or(ApiError::UnauthorizedOrDeletedUser)?
        .into();

    let available_regions = response.database_regions.into_iter().map(Into::into).collect();

    let PersonalAccount { id, name, slug } = viewer_response
        .personal_account
        .ok_or(ApiError::IncorrectlyScopedToken)?;

    let personal_account_id = id;

    let personal_account = Account {
        id: personal_account_id.inner().to_owned(),
        name,
        slug,
        personal: true,
    };

    let accounts = iter::once(personal_account)
        .chain(viewer_response.organizations.nodes.iter().map(|organization| Account {
            id: organization.id.inner().to_owned(),
            name: organization.name.clone(),
            slug: organization.slug.clone(),
            personal: false,
        }))
        .collect();

    Ok((accounts, available_regions, closest_region))
}

/// # Errors
///
/// See [`ApiError`]
pub async fn create(account_id: &str, project_slug: &str, database_regions: &[&str]) -> Result<Vec<String>, ApiError> {
    let environment = Environment::get();

    match environment.project_dot_grafbase_path.try_exists() {
        Ok(true) => {}
        Ok(false) => fs::create_dir_all(&environment.project_dot_grafbase_path)
            .await
            .map_err(ApiError::CreateProjectDotGrafbaseFolder)?,
        Err(error) => return Err(ApiError::ReadProjectDotGrafbaseFolder(error)),
    }

    let client = create_client().await?;

    let operation = ProjectCreate::build(ProjectCreateArguments {
        input: ProjectCreateInput {
            account_id: Id::new(account_id),
            database_regions: database_regions.iter().map(ToString::to_string).collect(),
            project_slug: project_slug.to_owned(),
        },
    });

    let response = client.post(API_URL).run_graphql(operation).await?;

    let payload = response.data.ok_or(ApiError::UnauthorizedOrDeletedUser)?.project_create;

    match payload {
        ProjectCreatePayload::ProjectCreateSuccess(ProjectCreateSuccess { project, .. }) => {
            let project_metadata_path = environment.project_dot_grafbase_path.join(PROJECT_METADATA_FILE);

            tokio::fs::write(
                &project_metadata_path,
                ProjectMetadata {
                    account_id: account_id.to_owned(),
                    project_id: project.id.into_inner().clone(),
                }
                .to_string(),
            )
            .await
            .map_err(ApiError::WriteProjectMetadataFile)?;

            deploy::deploy().await?;

            Ok(project.production_branch.domains)
        }
        ProjectCreatePayload::SlugAlreadyExistsError(_) => Err(CreateError::SlugAlreadyExists.into()),
        ProjectCreatePayload::SlugInvalidError(_) => Err(CreateError::SlugInvalid.into()),
        ProjectCreatePayload::SlugTooLongError(SlugTooLongError { max_length, .. }) => {
            Err(CreateError::SlugTooLong { max_length }.into())
        }
        ProjectCreatePayload::AccountDoesNotExistError(_) => Err(CreateError::AccountDoesNotExist.into()),
        ProjectCreatePayload::CurrentPlanLimitReachedError(CurrentPlanLimitReachedError { max, .. }) => {
            Err(CreateError::CurrentPlanLimitReached { max }.into())
        }
        ProjectCreatePayload::DuplicateDatabaseRegionsError(DuplicateDatabaseRegionsError { duplicates, .. }) => {
            Err(CreateError::DuplicateDatabaseRegions { duplicates }.into())
        }
        ProjectCreatePayload::EmptyDatabaseRegionsError(_) => Err(CreateError::EmptyDatabaseRegions.into()),
        ProjectCreatePayload::InvalidDatabaseRegionsError(InvalidDatabaseRegionsError { invalid, .. }) => {
            Err(CreateError::InvalidDatabaseRegions { invalid }.into())
        }
        ProjectCreatePayload::Unknown => Err(CreateError::Unknown.into()),
    }
}