From 19d14737bfe5b6a249236586e5e81f82ac6188d8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Dec 2022 11:58:07 +0100 Subject: [PATCH] Implement signups using sea-orm --- crates/collab/Cargo.toml | 2 +- crates/collab/src/db2.rs | 102 +++++++++++- crates/collab/src/db2/signup.rs | 29 +++- crates/collab/src/db2/tests.rs | 272 ++++++++++++++++---------------- 4 files changed, 262 insertions(+), 143 deletions(-) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a268bdd7b0..4cb91ad12d 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -36,7 +36,7 @@ prometheus = "0.13" rand = "0.8" reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" -sea-orm = { version = "0.10", features = ["sqlx-postgres", "runtime-tokio-rustls"] } +sea-orm = { version = "0.10", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] } sea-query = { version = "0.27", features = ["derive"] } sea-query-binder = { version = "0.2", features = ["sqlx-postgres"] } serde = { version = "1.0", features = ["derive", "rc"] } diff --git a/crates/collab/src/db2.rs b/crates/collab/src/db2.rs index 75329f9268..3aa21c6059 100644 --- a/crates/collab/src/db2.rs +++ b/crates/collab/src/db2.rs @@ -36,7 +36,7 @@ use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc}; use tokio::sync::{Mutex, OwnedMutexGuard}; pub use contact::Contact; -pub use signup::Invite; +pub use signup::{Invite, NewSignup, WaitlistSummary}; pub use user::Model as User; pub struct Database { @@ -140,6 +140,11 @@ impl Database { .await } + pub async fn get_user_by_id(&self, id: UserId) -> Result> { + self.transact(|tx| async move { Ok(user::Entity::find_by_id(id).one(&tx).await?) }) + .await + } + pub async fn get_users_by_ids(&self, ids: Vec) -> Result> { self.transact(|tx| async { let tx = tx; @@ -322,7 +327,7 @@ impl Database { } pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> { - self.transact(|mut tx| async move { + self.transact(|tx| async move { let (id_a, id_b, a_to_b) = if sender_id < receiver_id { (sender_id, receiver_id, true) } else { @@ -526,6 +531,99 @@ impl Database { .await } + // signups + + pub async fn create_signup(&self, signup: NewSignup) -> Result<()> { + self.transact(|tx| async { + signup::ActiveModel { + email_address: ActiveValue::set(signup.email_address.clone()), + email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), + email_confirmation_sent: ActiveValue::set(false), + platform_mac: ActiveValue::set(signup.platform_mac), + platform_windows: ActiveValue::set(signup.platform_windows), + platform_linux: ActiveValue::set(signup.platform_linux), + platform_unknown: ActiveValue::set(false), + editor_features: ActiveValue::set(Some(signup.editor_features.clone())), + programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())), + device_id: ActiveValue::set(signup.device_id.clone()), + ..Default::default() + } + .insert(&tx) + .await?; + tx.commit().await?; + Ok(()) + }) + .await + } + + pub async fn get_waitlist_summary(&self) -> Result { + self.transact(|tx| async move { + let query = " + SELECT + COUNT(*) as count, + COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count, + COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count, + COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count, + COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count + FROM ( + SELECT * + FROM signups + WHERE + NOT email_confirmation_sent + ) AS unsent + "; + Ok( + WaitlistSummary::find_by_statement(Statement::from_sql_and_values( + self.pool.get_database_backend(), + query.into(), + vec![], + )) + .one(&tx) + .await? + .ok_or_else(|| anyhow!("invalid result"))?, + ) + }) + .await + } + + pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> { + let emails = invites + .iter() + .map(|s| s.email_address.as_str()) + .collect::>(); + self.transact(|tx| async { + signup::Entity::update_many() + .filter(signup::Column::EmailAddress.is_in(emails.iter().copied())) + .col_expr(signup::Column::EmailConfirmationSent, true.into()) + .exec(&tx) + .await?; + tx.commit().await?; + Ok(()) + }) + .await + } + + pub async fn get_unsent_invites(&self, count: usize) -> Result> { + self.transact(|tx| async move { + Ok(signup::Entity::find() + .select_only() + .column(signup::Column::EmailAddress) + .column(signup::Column::EmailConfirmationCode) + .filter( + signup::Column::EmailConfirmationSent.eq(false).and( + signup::Column::PlatformMac + .eq(true) + .or(signup::Column::PlatformUnknown.eq(true)), + ), + ) + .limit(count as u64) + .into_model() + .all(&tx) + .await?) + }) + .await + } + // invite codes pub async fn create_invite_from_code( diff --git a/crates/collab/src/db2/signup.rs b/crates/collab/src/db2/signup.rs index ad0aa5eb82..8fab8daa36 100644 --- a/crates/collab/src/db2/signup.rs +++ b/crates/collab/src/db2/signup.rs @@ -1,5 +1,6 @@ use super::{SignupId, UserId}; -use sea_orm::entity::prelude::*; +use sea_orm::{entity::prelude::*, FromQueryResult}; +use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "signups")] @@ -17,8 +18,8 @@ pub struct Model { pub platform_linux: bool, pub platform_windows: bool, pub platform_unknown: bool, - pub editor_features: Option, - pub programming_languages: Option, + pub editor_features: Option>, + pub programming_languages: Option>, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -26,8 +27,28 @@ pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, FromQueryResult)] pub struct Invite { pub email_address: String, pub email_confirmation_code: String, } + +#[derive(Clone, Deserialize)] +pub struct NewSignup { + pub email_address: String, + pub platform_mac: bool, + pub platform_windows: bool, + pub platform_linux: bool, + pub editor_features: Vec, + pub programming_languages: Vec, + pub device_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)] +pub struct WaitlistSummary { + pub count: i64, + pub linux_count: i64, + pub mac_count: i64, + pub windows_count: i64, + pub unknown_count: i64, +} diff --git a/crates/collab/src/db2/tests.rs b/crates/collab/src/db2/tests.rs index 468d0074d4..b276bd5057 100644 --- a/crates/collab/src/db2/tests.rs +++ b/crates/collab/src/db2/tests.rs @@ -662,151 +662,151 @@ async fn test_invite_codes() { assert_eq!(invite_count, 1); } -// #[gpui::test] -// async fn test_signups() { -// let test_db = PostgresTestDb::new(build_background_executor()); -// let db = test_db.db(); +#[gpui::test] +async fn test_signups() { + let test_db = TestDb::postgres(build_background_executor()); + let db = test_db.db(); -// // people sign up on the waitlist -// for i in 0..8 { -// db.create_signup(Signup { -// email_address: format!("person-{i}@example.com"), -// platform_mac: true, -// platform_linux: i % 2 == 0, -// platform_windows: i % 4 == 0, -// editor_features: vec!["speed".into()], -// programming_languages: vec!["rust".into(), "c".into()], -// device_id: Some(format!("device_id_{i}")), -// }) -// .await -// .unwrap(); -// } + // people sign up on the waitlist + for i in 0..8 { + db.create_signup(NewSignup { + email_address: format!("person-{i}@example.com"), + platform_mac: true, + platform_linux: i % 2 == 0, + platform_windows: i % 4 == 0, + editor_features: vec!["speed".into()], + programming_languages: vec!["rust".into(), "c".into()], + device_id: Some(format!("device_id_{i}")), + }) + .await + .unwrap(); + } -// assert_eq!( -// db.get_waitlist_summary().await.unwrap(), -// WaitlistSummary { -// count: 8, -// mac_count: 8, -// linux_count: 4, -// windows_count: 2, -// unknown_count: 0, -// } -// ); + assert_eq!( + db.get_waitlist_summary().await.unwrap(), + WaitlistSummary { + count: 8, + mac_count: 8, + linux_count: 4, + windows_count: 2, + unknown_count: 0, + } + ); -// // retrieve the next batch of signup emails to send -// let signups_batch1 = db.get_unsent_invites(3).await.unwrap(); -// let addresses = signups_batch1 -// .iter() -// .map(|s| &s.email_address) -// .collect::>(); -// assert_eq!( -// addresses, -// &[ -// "person-0@example.com", -// "person-1@example.com", -// "person-2@example.com" -// ] -// ); -// assert_ne!( -// signups_batch1[0].email_confirmation_code, -// signups_batch1[1].email_confirmation_code -// ); + // retrieve the next batch of signup emails to send + let signups_batch1 = db.get_unsent_invites(3).await.unwrap(); + let addresses = signups_batch1 + .iter() + .map(|s| &s.email_address) + .collect::>(); + assert_eq!( + addresses, + &[ + "person-0@example.com", + "person-1@example.com", + "person-2@example.com" + ] + ); + assert_ne!( + signups_batch1[0].email_confirmation_code, + signups_batch1[1].email_confirmation_code + ); -// // the waitlist isn't updated until we record that the emails -// // were successfully sent. -// let signups_batch = db.get_unsent_invites(3).await.unwrap(); -// assert_eq!(signups_batch, signups_batch1); + // the waitlist isn't updated until we record that the emails + // were successfully sent. + let signups_batch = db.get_unsent_invites(3).await.unwrap(); + assert_eq!(signups_batch, signups_batch1); -// // once the emails go out, we can retrieve the next batch -// // of signups. -// db.record_sent_invites(&signups_batch1).await.unwrap(); -// let signups_batch2 = db.get_unsent_invites(3).await.unwrap(); -// let addresses = signups_batch2 -// .iter() -// .map(|s| &s.email_address) -// .collect::>(); -// assert_eq!( -// addresses, -// &[ -// "person-3@example.com", -// "person-4@example.com", -// "person-5@example.com" -// ] -// ); + // once the emails go out, we can retrieve the next batch + // of signups. + db.record_sent_invites(&signups_batch1).await.unwrap(); + let signups_batch2 = db.get_unsent_invites(3).await.unwrap(); + let addresses = signups_batch2 + .iter() + .map(|s| &s.email_address) + .collect::>(); + assert_eq!( + addresses, + &[ + "person-3@example.com", + "person-4@example.com", + "person-5@example.com" + ] + ); -// // the sent invites are excluded from the summary. -// assert_eq!( -// db.get_waitlist_summary().await.unwrap(), -// WaitlistSummary { -// count: 5, -// mac_count: 5, -// linux_count: 2, -// windows_count: 1, -// unknown_count: 0, -// } -// ); + // the sent invites are excluded from the summary. + assert_eq!( + db.get_waitlist_summary().await.unwrap(), + WaitlistSummary { + count: 5, + mac_count: 5, + linux_count: 2, + windows_count: 1, + unknown_count: 0, + } + ); -// // user completes the signup process by providing their -// // github account. -// let NewUserResult { -// user_id, -// inviting_user_id, -// signup_device_id, -// .. -// } = db -// .create_user_from_invite( -// &Invite { -// email_address: signups_batch1[0].email_address.clone(), -// email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(), -// }, -// NewUserParams { -// github_login: "person-0".into(), -// github_user_id: 0, -// invite_count: 5, -// }, -// ) -// .await -// .unwrap() -// .unwrap(); -// let user = db.get_user_by_id(user_id).await.unwrap().unwrap(); -// assert!(inviting_user_id.is_none()); -// assert_eq!(user.github_login, "person-0"); -// assert_eq!(user.email_address.as_deref(), Some("person-0@example.com")); -// assert_eq!(user.invite_count, 5); -// assert_eq!(signup_device_id.unwrap(), "device_id_0"); + // user completes the signup process by providing their + // github account. + let NewUserResult { + user_id, + inviting_user_id, + signup_device_id, + .. + } = db + .create_user_from_invite( + &Invite { + email_address: signups_batch1[0].email_address.clone(), + email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(), + }, + NewUserParams { + github_login: "person-0".into(), + github_user_id: 0, + invite_count: 5, + }, + ) + .await + .unwrap() + .unwrap(); + let user = db.get_user_by_id(user_id).await.unwrap().unwrap(); + assert!(inviting_user_id.is_none()); + assert_eq!(user.github_login, "person-0"); + assert_eq!(user.email_address.as_deref(), Some("person-0@example.com")); + assert_eq!(user.invite_count, 5); + assert_eq!(signup_device_id.unwrap(), "device_id_0"); -// // cannot redeem the same signup again. -// assert!(db -// .create_user_from_invite( -// &Invite { -// email_address: signups_batch1[0].email_address.clone(), -// email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(), -// }, -// NewUserParams { -// github_login: "some-other-github_account".into(), -// github_user_id: 1, -// invite_count: 5, -// }, -// ) -// .await -// .unwrap() -// .is_none()); + // cannot redeem the same signup again. + assert!(db + .create_user_from_invite( + &Invite { + email_address: signups_batch1[0].email_address.clone(), + email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(), + }, + NewUserParams { + github_login: "some-other-github_account".into(), + github_user_id: 1, + invite_count: 5, + }, + ) + .await + .unwrap() + .is_none()); -// // cannot redeem a signup with the wrong confirmation code. -// db.create_user_from_invite( -// &Invite { -// email_address: signups_batch1[1].email_address.clone(), -// email_confirmation_code: "the-wrong-code".to_string(), -// }, -// NewUserParams { -// github_login: "person-1".into(), -// github_user_id: 2, -// invite_count: 5, -// }, -// ) -// .await -// .unwrap_err(); -// } + // cannot redeem a signup with the wrong confirmation code. + db.create_user_from_invite( + &Invite { + email_address: signups_batch1[1].email_address.clone(), + email_confirmation_code: "the-wrong-code".to_string(), + }, + NewUserParams { + github_login: "person-1".into(), + github_user_id: 2, + invite_count: 5, + }, + ) + .await + .unwrap_err(); +} fn build_background_executor() -> Arc { Deterministic::new(0).build_background()