@@ -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<Option<user::Model>> {
+ 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<UserId>) -> Result<Vec<user::Model>> {
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<WaitlistSummary> {
+ 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::<Vec<_>>();
+ 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<Vec<Invite>> {
+ 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(
@@ -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<String>,
- pub programming_languages: Option<String>,
+ pub editor_features: Option<Vec<String>>,
+ pub programming_languages: Option<Vec<String>>,
}
#[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<String>,
+ pub programming_languages: Vec<String>,
+ pub device_id: Option<String>,
+}
+
+#[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,
+}
@@ -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();
-
-// // 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();
-// }
-
-// 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::<Vec<_>>();
-// 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);
-
-// // 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::<Vec<_>>();
-// 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,
-// }
-// );
-
-// // 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 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();
-// }
+#[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(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,
+ }
+ );
+
+ // 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::<Vec<_>>();
+ 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);
+
+ // 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::<Vec<_>>();
+ 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,
+ }
+ );
+
+ // 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 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<Background> {
Deterministic::new(0).build_background()