Add REST APIs for getting and adding contributors

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-22 10:48:33 -08:00
parent 5b906e731d
commit 1981de4cae
12 changed files with 206 additions and 56 deletions

View file

@ -344,3 +344,9 @@ CREATE INDEX
"index_notifications_on_recipient_id_is_read_kind_entity_id" "index_notifications_on_recipient_id_is_read_kind_entity_id"
ON "notifications" ON "notifications"
("recipient_id", "is_read", "kind", "entity_id"); ("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)
);

View file

@ -0,0 +1,5 @@
CREATE TABLE contributors (
user_id INTEGER REFERENCES users(id),
signed_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id)
);

View file

@ -25,6 +25,7 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
.route("/users/:id/access_tokens", post(create_access_token)) .route("/users/:id/access_tokens", post(create_access_token))
.route("/panic", post(trace_panic)) .route("/panic", post(trace_panic))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.route("/contributors", get(get_contributors).post(add_contributor))
.layer( .layer(
ServiceBuilder::new() ServiceBuilder::new()
.layer(Extension(state)) .layer(Extension(state))
@ -66,7 +67,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct AuthenticatedUserParams { struct AuthenticatedUserParams {
github_user_id: Option<i32>, github_user_id: i32,
github_login: String, github_login: String,
github_email: Option<String>, github_email: Option<String>,
} }
@ -88,8 +89,7 @@ async fn get_authenticated_user(
params.github_user_id, params.github_user_id,
params.github_email.as_deref(), params.github_email.as_deref(),
) )
.await? .await?;
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
let metrics_id = app.db.get_user_metrics_id(user.id).await?; let metrics_id = app.db.get_user_metrics_id(user.id).await?;
return Ok(Json(AuthenticatedUserResponse { user, metrics_id })); return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
} }
@ -133,6 +133,24 @@ async fn get_rpc_server_snapshot(
Ok(ErasedJson::pretty(rpc_server.snapshot().await)) Ok(ErasedJson::pretty(rpc_server.snapshot().await))
} }
async fn get_contributors(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<String>>> {
Ok(Json(app.db.get_contributors().await?))
}
async fn add_contributor(
Json(params): Json<AuthenticatedUserParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<()> {
Ok(app
.db
.add_contributor(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),
)
.await?)
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct CreateAccessTokenQueryParams { struct CreateAccessTokenQueryParams {
public_key: String, public_key: String,

View file

@ -4,6 +4,7 @@ pub mod access_tokens;
pub mod buffers; pub mod buffers;
pub mod channels; pub mod channels;
pub mod contacts; pub mod contacts;
pub mod contributors;
pub mod messages; pub mod messages;
pub mod notifications; pub mod notifications;
pub mod projects; pub mod projects;

View file

@ -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<Vec<String>> {
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
}
}

View file

@ -72,12 +72,28 @@ impl Database {
pub async fn get_or_create_user_by_github_account( pub async fn get_or_create_user_by_github_account(
&self, &self,
github_login: &str, github_login: &str,
github_user_id: Option<i32>, github_user_id: i32,
github_email: Option<&str>, github_email: Option<&str>,
) -> Result<Option<User>> { ) -> Result<User> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let tx = &*tx; self.get_or_create_user_by_github_account_tx(
if let Some(github_user_id) = github_user_id { 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<User> {
if let Some(user_by_github_user_id) = user::Entity::find() if let Some(user_by_github_user_id) = user::Entity::find()
.filter(user::Column::GithubUserId.eq(github_user_id)) .filter(user::Column::GithubUserId.eq(github_user_id))
.one(tx) .one(tx)
@ -85,7 +101,7 @@ impl Database {
{ {
let mut user_by_github_user_id = user_by_github_user_id.into_active_model(); 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()); user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
Ok(Some(user_by_github_user_id.update(tx).await?)) Ok(user_by_github_user_id.update(tx).await?)
} else if let Some(user_by_github_login) = user::Entity::find() } else if let Some(user_by_github_login) = user::Entity::find()
.filter(user::Column::GithubLogin.eq(github_login)) .filter(user::Column::GithubLogin.eq(github_login))
.one(tx) .one(tx)
@ -93,7 +109,7 @@ impl Database {
{ {
let mut user_by_github_login = user_by_github_login.into_active_model(); 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)); user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
Ok(Some(user_by_github_login.update(tx).await?)) Ok(user_by_github_login.update(tx).await?)
} else { } else {
let user = user::Entity::insert(user::ActiveModel { let user = user::Entity::insert(user::ActiveModel {
email_address: ActiveValue::set(github_email.map(|email| email.into())), email_address: ActiveValue::set(github_email.map(|email| email.into())),
@ -107,16 +123,8 @@ impl Database {
}) })
.exec_with_returning(&*tx) .exec_with_returning(&*tx)
.await?; .await?;
Ok(Some(user)) Ok(user)
} }
} else {
Ok(user::Entity::find()
.filter(user::Column::GithubLogin.eq(github_login))
.one(tx)
.await?)
}
})
.await
} }
/// get_all_users returns the next page of users. To get more call again with /// get_all_users returns the next page of users. To get more call again with

View file

@ -9,6 +9,7 @@ pub mod channel_member;
pub mod channel_message; pub mod channel_message;
pub mod channel_message_mention; pub mod channel_message_mention;
pub mod contact; pub mod contact;
pub mod contributor;
pub mod feature_flag; pub mod feature_flag;
pub mod follower; pub mod follower;
pub mod language_server; pub mod language_server;

View file

@ -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<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}

View file

@ -31,6 +31,8 @@ pub enum Relation {
ChannelMemberships, ChannelMemberships,
#[sea_orm(has_many = "super::user_feature::Entity")] #[sea_orm(has_many = "super::user_feature::Entity")]
UserFeatures, UserFeatures,
#[sea_orm(has_one = "super::contributor::Entity")]
Contributor,
} }
impl Related<super::access_token::Entity> for Entity { impl Related<super::access_token::Entity> for Entity {

View file

@ -1,5 +1,6 @@
mod buffer_tests; mod buffer_tests;
mod channel_tests; mod channel_tests;
mod contributor_tests;
mod db_tests; mod db_tests;
mod feature_flag_tests; mod feature_flag_tests;
mod message_tests; mod message_tests;

View file

@ -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<Database>) {
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::<String>::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()]
);
}

View file

@ -106,33 +106,24 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
.user_id; .user_id;
let user = db let user = db
.get_or_create_user_by_github_account("login1", None, None) .get_or_create_user_by_github_account("login1", 1, None)
.await .await
.unwrap()
.unwrap(); .unwrap();
assert_eq!(user.id, user_id1); assert_eq!(user.id, user_id1);
assert_eq!(&user.github_login, "login1"); assert_eq!(&user.github_login, "login1");
assert_eq!(user.github_user_id, Some(101)); 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 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 .await
.unwrap()
.unwrap(); .unwrap();
assert_eq!(user.id, user_id2); assert_eq!(user.id, user_id2);
assert_eq!(&user.github_login, "the-new-login2"); assert_eq!(&user.github_login, "the-new-login2");
assert_eq!(user.github_user_id, Some(102)); assert_eq!(user.github_user_id, Some(102));
let user = db 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 .await
.unwrap()
.unwrap(); .unwrap();
assert_eq!(&user.github_login, "login3"); assert_eq!(&user.github_login, "login3");
assert_eq!(user.github_user_id, Some(103)); assert_eq!(user.github_user_id, Some(103));