Implement signups using sea-orm
This commit is contained in:
parent
4f864a20a7
commit
19d14737bf
4 changed files with 262 additions and 143 deletions
|
@ -36,7 +36,7 @@ prometheus = "0.13"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||||
scrypt = "0.7"
|
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 = { version = "0.27", features = ["derive"] }
|
||||||
sea-query-binder = { version = "0.2", features = ["sqlx-postgres"] }
|
sea-query-binder = { version = "0.2", features = ["sqlx-postgres"] }
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
|
|
@ -36,7 +36,7 @@ use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc};
|
||||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||||
|
|
||||||
pub use contact::Contact;
|
pub use contact::Contact;
|
||||||
pub use signup::Invite;
|
pub use signup::{Invite, NewSignup, WaitlistSummary};
|
||||||
pub use user::Model as User;
|
pub use user::Model as User;
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
|
@ -140,6 +140,11 @@ impl Database {
|
||||||
.await
|
.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>> {
|
pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
|
||||||
self.transact(|tx| async {
|
self.transact(|tx| async {
|
||||||
let tx = tx;
|
let tx = tx;
|
||||||
|
@ -322,7 +327,7 @@ impl Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
|
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 {
|
let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
|
||||||
(sender_id, receiver_id, true)
|
(sender_id, receiver_id, true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -526,6 +531,99 @@ impl Database {
|
||||||
.await
|
.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
|
// invite codes
|
||||||
|
|
||||||
pub async fn create_invite_from_code(
|
pub async fn create_invite_from_code(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use super::{SignupId, UserId};
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "signups")]
|
#[sea_orm(table_name = "signups")]
|
||||||
|
@ -17,8 +18,8 @@ pub struct Model {
|
||||||
pub platform_linux: bool,
|
pub platform_linux: bool,
|
||||||
pub platform_windows: bool,
|
pub platform_windows: bool,
|
||||||
pub platform_unknown: bool,
|
pub platform_unknown: bool,
|
||||||
pub editor_features: Option<String>,
|
pub editor_features: Option<Vec<String>>,
|
||||||
pub programming_languages: Option<String>,
|
pub programming_languages: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
@ -26,8 +27,28 @@ pub enum Relation {}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq, FromQueryResult)]
|
||||||
pub struct Invite {
|
pub struct Invite {
|
||||||
pub email_address: String,
|
pub email_address: String,
|
||||||
pub email_confirmation_code: 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);
|
assert_eq!(invite_count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[gpui::test]
|
#[gpui::test]
|
||||||
// async fn test_signups() {
|
async fn test_signups() {
|
||||||
// let test_db = PostgresTestDb::new(build_background_executor());
|
let test_db = TestDb::postgres(build_background_executor());
|
||||||
// let db = test_db.db();
|
let db = test_db.db();
|
||||||
|
|
||||||
// // people sign up on the waitlist
|
// people sign up on the waitlist
|
||||||
// for i in 0..8 {
|
for i in 0..8 {
|
||||||
// db.create_signup(Signup {
|
db.create_signup(NewSignup {
|
||||||
// email_address: format!("person-{i}@example.com"),
|
email_address: format!("person-{i}@example.com"),
|
||||||
// platform_mac: true,
|
platform_mac: true,
|
||||||
// platform_linux: i % 2 == 0,
|
platform_linux: i % 2 == 0,
|
||||||
// platform_windows: i % 4 == 0,
|
platform_windows: i % 4 == 0,
|
||||||
// editor_features: vec!["speed".into()],
|
editor_features: vec!["speed".into()],
|
||||||
// programming_languages: vec!["rust".into(), "c".into()],
|
programming_languages: vec!["rust".into(), "c".into()],
|
||||||
// device_id: Some(format!("device_id_{i}")),
|
device_id: Some(format!("device_id_{i}")),
|
||||||
// })
|
})
|
||||||
// .await
|
.await
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// db.get_waitlist_summary().await.unwrap(),
|
db.get_waitlist_summary().await.unwrap(),
|
||||||
// WaitlistSummary {
|
WaitlistSummary {
|
||||||
// count: 8,
|
count: 8,
|
||||||
// mac_count: 8,
|
mac_count: 8,
|
||||||
// linux_count: 4,
|
linux_count: 4,
|
||||||
// windows_count: 2,
|
windows_count: 2,
|
||||||
// unknown_count: 0,
|
unknown_count: 0,
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
|
|
||||||
// // retrieve the next batch of signup emails to send
|
// retrieve the next batch of signup emails to send
|
||||||
// let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
|
let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
|
||||||
// let addresses = signups_batch1
|
let addresses = signups_batch1
|
||||||
// .iter()
|
.iter()
|
||||||
// .map(|s| &s.email_address)
|
.map(|s| &s.email_address)
|
||||||
// .collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// addresses,
|
addresses,
|
||||||
// &[
|
&[
|
||||||
// "person-0@example.com",
|
"person-0@example.com",
|
||||||
// "person-1@example.com",
|
"person-1@example.com",
|
||||||
// "person-2@example.com"
|
"person-2@example.com"
|
||||||
// ]
|
]
|
||||||
// );
|
);
|
||||||
// assert_ne!(
|
assert_ne!(
|
||||||
// signups_batch1[0].email_confirmation_code,
|
signups_batch1[0].email_confirmation_code,
|
||||||
// signups_batch1[1].email_confirmation_code
|
signups_batch1[1].email_confirmation_code
|
||||||
// );
|
);
|
||||||
|
|
||||||
// // the waitlist isn't updated until we record that the emails
|
// the waitlist isn't updated until we record that the emails
|
||||||
// // were successfully sent.
|
// were successfully sent.
|
||||||
// let signups_batch = db.get_unsent_invites(3).await.unwrap();
|
let signups_batch = db.get_unsent_invites(3).await.unwrap();
|
||||||
// assert_eq!(signups_batch, signups_batch1);
|
assert_eq!(signups_batch, signups_batch1);
|
||||||
|
|
||||||
// // once the emails go out, we can retrieve the next batch
|
// once the emails go out, we can retrieve the next batch
|
||||||
// // of signups.
|
// of signups.
|
||||||
// db.record_sent_invites(&signups_batch1).await.unwrap();
|
db.record_sent_invites(&signups_batch1).await.unwrap();
|
||||||
// let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
|
let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
|
||||||
// let addresses = signups_batch2
|
let addresses = signups_batch2
|
||||||
// .iter()
|
.iter()
|
||||||
// .map(|s| &s.email_address)
|
.map(|s| &s.email_address)
|
||||||
// .collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// addresses,
|
addresses,
|
||||||
// &[
|
&[
|
||||||
// "person-3@example.com",
|
"person-3@example.com",
|
||||||
// "person-4@example.com",
|
"person-4@example.com",
|
||||||
// "person-5@example.com"
|
"person-5@example.com"
|
||||||
// ]
|
]
|
||||||
// );
|
);
|
||||||
|
|
||||||
// // the sent invites are excluded from the summary.
|
// the sent invites are excluded from the summary.
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// db.get_waitlist_summary().await.unwrap(),
|
db.get_waitlist_summary().await.unwrap(),
|
||||||
// WaitlistSummary {
|
WaitlistSummary {
|
||||||
// count: 5,
|
count: 5,
|
||||||
// mac_count: 5,
|
mac_count: 5,
|
||||||
// linux_count: 2,
|
linux_count: 2,
|
||||||
// windows_count: 1,
|
windows_count: 1,
|
||||||
// unknown_count: 0,
|
unknown_count: 0,
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
|
|
||||||
// // user completes the signup process by providing their
|
// user completes the signup process by providing their
|
||||||
// // github account.
|
// github account.
|
||||||
// let NewUserResult {
|
let NewUserResult {
|
||||||
// user_id,
|
user_id,
|
||||||
// inviting_user_id,
|
inviting_user_id,
|
||||||
// signup_device_id,
|
signup_device_id,
|
||||||
// ..
|
..
|
||||||
// } = db
|
} = db
|
||||||
// .create_user_from_invite(
|
.create_user_from_invite(
|
||||||
// &Invite {
|
&Invite {
|
||||||
// email_address: signups_batch1[0].email_address.clone(),
|
email_address: signups_batch1[0].email_address.clone(),
|
||||||
// email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
|
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
|
||||||
// },
|
},
|
||||||
// NewUserParams {
|
NewUserParams {
|
||||||
// github_login: "person-0".into(),
|
github_login: "person-0".into(),
|
||||||
// github_user_id: 0,
|
github_user_id: 0,
|
||||||
// invite_count: 5,
|
invite_count: 5,
|
||||||
// },
|
},
|
||||||
// )
|
)
|
||||||
// .await
|
.await
|
||||||
// .unwrap()
|
.unwrap()
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
// let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||||
// assert!(inviting_user_id.is_none());
|
assert!(inviting_user_id.is_none());
|
||||||
// assert_eq!(user.github_login, "person-0");
|
assert_eq!(user.github_login, "person-0");
|
||||||
// assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
|
assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
|
||||||
// assert_eq!(user.invite_count, 5);
|
assert_eq!(user.invite_count, 5);
|
||||||
// assert_eq!(signup_device_id.unwrap(), "device_id_0");
|
assert_eq!(signup_device_id.unwrap(), "device_id_0");
|
||||||
|
|
||||||
// // cannot redeem the same signup again.
|
// cannot redeem the same signup again.
|
||||||
// assert!(db
|
assert!(db
|
||||||
// .create_user_from_invite(
|
.create_user_from_invite(
|
||||||
// &Invite {
|
&Invite {
|
||||||
// email_address: signups_batch1[0].email_address.clone(),
|
email_address: signups_batch1[0].email_address.clone(),
|
||||||
// email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
|
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
|
||||||
// },
|
},
|
||||||
// NewUserParams {
|
NewUserParams {
|
||||||
// github_login: "some-other-github_account".into(),
|
github_login: "some-other-github_account".into(),
|
||||||
// github_user_id: 1,
|
github_user_id: 1,
|
||||||
// invite_count: 5,
|
invite_count: 5,
|
||||||
// },
|
},
|
||||||
// )
|
)
|
||||||
// .await
|
.await
|
||||||
// .unwrap()
|
.unwrap()
|
||||||
// .is_none());
|
.is_none());
|
||||||
|
|
||||||
// // cannot redeem a signup with the wrong confirmation code.
|
// cannot redeem a signup with the wrong confirmation code.
|
||||||
// db.create_user_from_invite(
|
db.create_user_from_invite(
|
||||||
// &Invite {
|
&Invite {
|
||||||
// email_address: signups_batch1[1].email_address.clone(),
|
email_address: signups_batch1[1].email_address.clone(),
|
||||||
// email_confirmation_code: "the-wrong-code".to_string(),
|
email_confirmation_code: "the-wrong-code".to_string(),
|
||||||
// },
|
},
|
||||||
// NewUserParams {
|
NewUserParams {
|
||||||
// github_login: "person-1".into(),
|
github_login: "person-1".into(),
|
||||||
// github_user_id: 2,
|
github_user_id: 2,
|
||||||
// invite_count: 5,
|
invite_count: 5,
|
||||||
// },
|
},
|
||||||
// )
|
)
|
||||||
// .await
|
.await
|
||||||
// .unwrap_err();
|
.unwrap_err();
|
||||||
// }
|
}
|
||||||
|
|
||||||
fn build_background_executor() -> Arc<Background> {
|
fn build_background_executor() -> Arc<Background> {
|
||||||
Deterministic::new(0).build_background()
|
Deterministic::new(0).build_background()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue