From 1981de4caee62954a316a825222c4fa8fa020af8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 22 Jan 2024 10:48:33 -0800 Subject: [PATCH] Add REST APIs for getting and adding contributors Co-authored-by: Mikayla --- .../20221109000000_test_schema.sql | 6 ++ .../20240122174606_add_contributors.sql | 5 ++ crates/collab/src/api.rs | 24 ++++- crates/collab/src/db/queries.rs | 1 + crates/collab/src/db/queries/contributors.rs | 50 +++++++++++ crates/collab/src/db/queries/users.rs | 90 ++++++++++--------- crates/collab/src/db/tables.rs | 1 + crates/collab/src/db/tables/contributor.rs | 30 +++++++ crates/collab/src/db/tables/user.rs | 2 + crates/collab/src/db/tests.rs | 1 + .../collab/src/db/tests/contributor_tests.rs | 37 ++++++++ crates/collab/src/db/tests/db_tests.rs | 15 +--- 12 files changed, 206 insertions(+), 56 deletions(-) create mode 100644 crates/collab/migrations/20240122174606_add_contributors.sql create mode 100644 crates/collab/src/db/queries/contributors.rs create mode 100644 crates/collab/src/db/tables/contributor.rs create mode 100644 crates/collab/src/db/tests/contributor_tests.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 8d8f523c94..14657fe682 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -344,3 +344,9 @@ CREATE INDEX "index_notifications_on_recipient_id_is_read_kind_entity_id" ON "notifications" ("recipient_id", "is_read", "kind", "entity_id"); + +CREATE TABLE contributors ( + user_id INTEGER REFERENCES users(id), + signed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id) +); diff --git a/crates/collab/migrations/20240122174606_add_contributors.sql b/crates/collab/migrations/20240122174606_add_contributors.sql new file mode 100644 index 0000000000..16bec82d4f --- /dev/null +++ b/crates/collab/migrations/20240122174606_add_contributors.sql @@ -0,0 +1,5 @@ +CREATE TABLE contributors ( + user_id INTEGER REFERENCES users(id), + signed_at TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id) +); diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 6bdbd7357f..88c813ecc3 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -25,6 +25,7 @@ pub fn routes(rpc_server: Arc, state: Arc) -> Router(req: Request, next: Next) -> impl IntoR #[derive(Debug, Deserialize)] struct AuthenticatedUserParams { - github_user_id: Option, + github_user_id: i32, github_login: String, github_email: Option, } @@ -88,8 +89,7 @@ async fn get_authenticated_user( params.github_user_id, params.github_email.as_deref(), ) - .await? - .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?; + .await?; let metrics_id = app.db.get_user_metrics_id(user.id).await?; return Ok(Json(AuthenticatedUserResponse { user, metrics_id })); } @@ -133,6 +133,24 @@ async fn get_rpc_server_snapshot( Ok(ErasedJson::pretty(rpc_server.snapshot().await)) } +async fn get_contributors(Extension(app): Extension>) -> Result>> { + Ok(Json(app.db.get_contributors().await?)) +} + +async fn add_contributor( + Json(params): Json, + Extension(app): Extension>, +) -> Result<()> { + Ok(app + .db + .add_contributor( + ¶ms.github_login, + params.github_user_id, + params.github_email.as_deref(), + ) + .await?) +} + #[derive(Deserialize)] struct CreateAccessTokenQueryParams { public_key: String, diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 629e26f1a9..f6bba13ede 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -4,6 +4,7 @@ pub mod access_tokens; pub mod buffers; pub mod channels; pub mod contacts; +pub mod contributors; pub mod messages; pub mod notifications; pub mod projects; diff --git a/crates/collab/src/db/queries/contributors.rs b/crates/collab/src/db/queries/contributors.rs new file mode 100644 index 0000000000..593409670b --- /dev/null +++ b/crates/collab/src/db/queries/contributors.rs @@ -0,0 +1,50 @@ +use super::*; + +impl Database { + /// Retrieves the GitHub logins of all users who have signed the CLA. + pub async fn get_contributors(&self) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryGithubLogin { + GithubLogin, + } + + Ok(contributor::Entity::find() + .inner_join(user::Entity) + .order_by_asc(contributor::Column::SignedAt) + .select_only() + .column(user::Column::GithubLogin) + .into_values::<_, QueryGithubLogin>() + .all(&*tx) + .await?) + }) + .await + } + + /// Records that a given user has signed the CLA. + pub async fn add_contributor( + &self, + github_login: &str, + github_user_id: i32, + github_email: Option<&str>, + ) -> Result<()> { + self.transaction(|tx| async move { + let user = self + .get_or_create_user_by_github_account_tx( + github_login, + github_user_id, + github_email, + &*tx, + ) + .await?; + contributor::ActiveModel { + user_id: ActiveValue::Set(user.id), + signed_at: ActiveValue::NotSet, + } + .insert(&*tx) + .await?; + Ok(()) + }) + .await + } +} diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index d6dfe48042..4249f06617 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -72,53 +72,61 @@ impl Database { pub async fn get_or_create_user_by_github_account( &self, github_login: &str, - github_user_id: Option, + github_user_id: i32, github_email: Option<&str>, - ) -> Result> { + ) -> Result { self.transaction(|tx| async move { - let tx = &*tx; - if let Some(github_user_id) = github_user_id { - if let Some(user_by_github_user_id) = user::Entity::find() - .filter(user::Column::GithubUserId.eq(github_user_id)) - .one(tx) - .await? - { - let mut user_by_github_user_id = user_by_github_user_id.into_active_model(); - user_by_github_user_id.github_login = ActiveValue::set(github_login.into()); - Ok(Some(user_by_github_user_id.update(tx).await?)) - } else if let Some(user_by_github_login) = user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(tx) - .await? - { - let mut user_by_github_login = user_by_github_login.into_active_model(); - user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id)); - Ok(Some(user_by_github_login.update(tx).await?)) - } else { - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(github_email.map(|email| email.into())), - github_login: ActiveValue::set(github_login.into()), - github_user_id: ActiveValue::set(Some(github_user_id)), - admin: ActiveValue::set(false), - invite_count: ActiveValue::set(0), - invite_code: ActiveValue::set(None), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .exec_with_returning(&*tx) - .await?; - Ok(Some(user)) - } - } else { - Ok(user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(tx) - .await?) - } + self.get_or_create_user_by_github_account_tx( + github_login, + github_user_id, + github_email, + &*tx, + ) + .await }) .await } + pub async fn get_or_create_user_by_github_account_tx( + &self, + github_login: &str, + github_user_id: i32, + github_email: Option<&str>, + tx: &DatabaseTransaction, + ) -> Result { + if let Some(user_by_github_user_id) = user::Entity::find() + .filter(user::Column::GithubUserId.eq(github_user_id)) + .one(tx) + .await? + { + let mut user_by_github_user_id = user_by_github_user_id.into_active_model(); + user_by_github_user_id.github_login = ActiveValue::set(github_login.into()); + Ok(user_by_github_user_id.update(tx).await?) + } else if let Some(user_by_github_login) = user::Entity::find() + .filter(user::Column::GithubLogin.eq(github_login)) + .one(tx) + .await? + { + let mut user_by_github_login = user_by_github_login.into_active_model(); + user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id)); + Ok(user_by_github_login.update(tx).await?) + } else { + let user = user::Entity::insert(user::ActiveModel { + email_address: ActiveValue::set(github_email.map(|email| email.into())), + github_login: ActiveValue::set(github_login.into()), + github_user_id: ActiveValue::set(Some(github_user_id)), + admin: ActiveValue::set(false), + invite_count: ActiveValue::set(0), + invite_code: ActiveValue::set(None), + metrics_id: ActiveValue::set(Uuid::new_v4()), + ..Default::default() + }) + .exec_with_returning(&*tx) + .await?; + Ok(user) + } + } + /// get_all_users returns the next page of users. To get more call again with /// the same limit and the page incremented by 1. pub async fn get_all_users(&self, page: u32, limit: u32) -> Result> { diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 4f28ce4fbd..646447c91f 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -9,6 +9,7 @@ pub mod channel_member; pub mod channel_message; pub mod channel_message_mention; pub mod contact; +pub mod contributor; pub mod feature_flag; pub mod follower; pub mod language_server; diff --git a/crates/collab/src/db/tables/contributor.rs b/crates/collab/src/db/tables/contributor.rs new file mode 100644 index 0000000000..3ae96a62d9 --- /dev/null +++ b/crates/collab/src/db/tables/contributor.rs @@ -0,0 +1,30 @@ +use crate::db::UserId; +use sea_orm::entity::prelude::*; +use serde::Serialize; + +/// A user who has signed the CLA. +#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)] +#[sea_orm(table_name = "contributors")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + pub signed_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 53866b5c54..5ab7f17a01 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -31,6 +31,8 @@ pub enum Relation { ChannelMemberships, #[sea_orm(has_many = "super::user_feature::Entity")] UserFeatures, + #[sea_orm(has_one = "super::contributor::Entity")] + Contributor, } impl Related for Entity { diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 56e37abc1d..4a9c98f022 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,5 +1,6 @@ mod buffer_tests; mod channel_tests; +mod contributor_tests; mod db_tests; mod feature_flag_tests; mod message_tests; diff --git a/crates/collab/src/db/tests/contributor_tests.rs b/crates/collab/src/db/tests/contributor_tests.rs new file mode 100644 index 0000000000..1985229f2f --- /dev/null +++ b/crates/collab/src/db/tests/contributor_tests.rs @@ -0,0 +1,37 @@ +use super::Database; +use crate::{db::NewUserParams, test_both_dbs}; +use std::sync::Arc; + +test_both_dbs!( + test_contributors, + test_contributors_postgres, + test_contributors_sqlite +); + +async fn test_contributors(db: &Arc) { + db.create_user( + &format!("user1@example.com"), + false, + NewUserParams { + github_login: format!("user1"), + github_user_id: 1, + }, + ) + .await + .unwrap() + .user_id; + + assert_eq!(db.get_contributors().await.unwrap(), Vec::::new()); + + db.add_contributor("user1", 1, None).await.unwrap(); + assert_eq!( + db.get_contributors().await.unwrap(), + vec!["user1".to_string()] + ); + + db.add_contributor("user2", 2, None).await.unwrap(); + assert_eq!( + db.get_contributors().await.unwrap(), + vec!["user1".to_string(), "user2".to_string()] + ); +} diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 3e1bdede71..a14799005b 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -106,33 +106,24 @@ async fn test_get_or_create_user_by_github_account(db: &Arc) { .user_id; let user = db - .get_or_create_user_by_github_account("login1", None, None) + .get_or_create_user_by_github_account("login1", 1, None) .await - .unwrap() .unwrap(); assert_eq!(user.id, user_id1); assert_eq!(&user.github_login, "login1"); assert_eq!(user.github_user_id, Some(101)); - assert!(db - .get_or_create_user_by_github_account("non-existent-login", None, None) - .await - .unwrap() - .is_none()); - let user = db - .get_or_create_user_by_github_account("the-new-login2", Some(102), None) + .get_or_create_user_by_github_account("the-new-login2", 102, None) .await - .unwrap() .unwrap(); assert_eq!(user.id, user_id2); assert_eq!(&user.github_login, "the-new-login2"); assert_eq!(user.github_user_id, Some(102)); let user = db - .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) + .get_or_create_user_by_github_account("login3", 103, Some("user3@example.com")) .await - .unwrap() .unwrap(); assert_eq!(&user.github_login, "login3"); assert_eq!(user.github_user_id, Some(103));