Detailed changes
@@ -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)
+);
@@ -0,0 +1,5 @@
+CREATE TABLE contributors (
+ user_id INTEGER REFERENCES users(id),
+ signed_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (user_id)
+);
@@ -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("/panic", post(trace_panic))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
+ .route("/contributors", get(get_contributors).post(add_contributor))
.layer(
ServiceBuilder::new()
.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)]
struct AuthenticatedUserParams {
- github_user_id: Option<i32>,
+ github_user_id: i32,
github_login: String,
github_email: Option<String>,
}
@@ -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<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(
+ ¶ms.github_login,
+ params.github_user_id,
+ params.github_email.as_deref(),
+ )
+ .await?)
+}
+
#[derive(Deserialize)]
struct CreateAccessTokenQueryParams {
public_key: String,
@@ -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;
@@ -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
+ }
+}
@@ -72,53 +72,61 @@ impl Database {
pub async fn get_or_create_user_by_github_account(
&self,
github_login: &str,
- github_user_id: Option<i32>,
+ github_user_id: i32,
github_email: Option<&str>,
- ) -> Result<Option<User>> {
+ ) -> Result<User> {
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<User> {
+ 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<Vec<User>> {
@@ -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;
@@ -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()
+ }
+}
@@ -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<super::access_token::Entity> for Entity {
@@ -1,5 +1,6 @@
mod buffer_tests;
mod channel_tests;
+mod contributor_tests;
mod db_tests;
mod feature_flag_tests;
mod message_tests;
@@ -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()]
+ );
+}
@@ -106,33 +106,24 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
.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));