mirror of
https://github.com/zed-industries/zed.git
synced 2024-10-26 08:31:04 +00:00
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"
|
||||
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"] }
|
||||
|
|
|
@ -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();
|
||||
#[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()
|
||||
|
|
Loading…
Reference in a new issue