Implement signups using sea-orm

This commit is contained in:
Antonio Scandurra 2022-12-01 11:58:07 +01:00
parent 4f864a20a7
commit 19d14737bf
4 changed files with 262 additions and 143 deletions

View file

@ -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"] }

View file

@ -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(

View file

@ -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,
}

View file

@ -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::<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
// );
// 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);
// 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"
// ]
// );
// 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,
// }
// );
// 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<Background> {
Deterministic::new(0).build_background()