Add waitlist summary API

Max Brunsfeld created

Change summary

crates/collab/src/api.rs      |  9 ++++++++-
crates/collab/src/db.rs       | 37 +++++++++++++++++++++++++++++++++++++
crates/collab/src/db_tests.rs | 25 +++++++++++++++++++++++--
3 files changed, 68 insertions(+), 3 deletions(-)

Detailed changes

crates/collab/src/api.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     auth,
-    db::{Invite, NewUserParams, ProjectId, Signup, User, UserId},
+    db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
     rpc::{self, ResultExt},
     AppState, Error, Result,
 };
@@ -46,6 +46,7 @@ pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Bod
         .route("/user_activity/counts", get(get_active_user_counts))
         .route("/project_metadata", get(get_project_metadata))
         .route("/signups", post(create_signup))
+        .route("/signups_summary", get(get_waitlist_summary))
         .route("/user_invites", post(create_invite_from_code))
         .route("/unsent_invites", get(get_unsent_invites))
         .route("/sent_invites", post(record_sent_invites))
@@ -439,6 +440,12 @@ async fn create_signup(
     Ok(())
 }
 
+async fn get_waitlist_summary(
+    Extension(app): Extension<Arc<AppState>>,
+) -> Result<Json<WaitlistSummary>> {
+    Ok(Json(app.db.get_waitlist_summary().await?))
+}
+
 #[derive(Deserialize)]
 pub struct CreateInviteFromCodeParams {
     invite_code: String,

crates/collab/src/db.rs 🔗

@@ -35,6 +35,7 @@ pub trait Db: Send + Sync {
     async fn create_invite_from_code(&self, code: &str, email_address: &str) -> Result<Invite>;
 
     async fn create_signup(&self, signup: Signup) -> Result<()>;
+    async fn get_waitlist_summary(&self) -> Result<WaitlistSummary>;
     async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>>;
     async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()>;
     async fn create_user_from_invite(
@@ -384,6 +385,26 @@ impl Db for PostgresDb {
         Ok(())
     }
 
+    async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
+        Ok(sqlx::query_as(
+            "
+            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
+            FROM (
+                SELECT *
+                FROM signups
+                WHERE
+                    NOT email_confirmation_sent
+            ) AS unsent
+            ",
+        )
+        .fetch_one(&self.pool)
+        .await?)
+    }
+
     async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>> {
         Ok(sqlx::query_as(
             "
@@ -1630,6 +1651,18 @@ pub struct Signup {
     pub programming_languages: Vec<String>,
 }
 
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromRow)]
+pub struct WaitlistSummary {
+    #[sqlx(default)]
+    pub count: i64,
+    #[sqlx(default)]
+    pub linux_count: i64,
+    #[sqlx(default)]
+    pub mac_count: i64,
+    #[sqlx(default)]
+    pub windows_count: i64,
+}
+
 #[derive(FromRow, PartialEq, Debug, Serialize, Deserialize)]
 pub struct Invite {
     pub email_address: String,
@@ -1812,6 +1845,10 @@ mod test {
             unimplemented!()
         }
 
+        async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
+            unimplemented!()
+        }
+
         async fn get_unsent_invites(&self, _count: usize) -> Result<Vec<Invite>> {
             unimplemented!()
         }

crates/collab/src/db_tests.rs 🔗

@@ -966,8 +966,8 @@ async fn test_signups() {
         db.create_signup(Signup {
             email_address: format!("person-{i}@example.com"),
             platform_mac: true,
-            platform_linux: true,
-            platform_windows: false,
+            platform_linux: i % 2 == 0,
+            platform_windows: i % 4 == 0,
             editor_features: vec!["speed".into()],
             programming_languages: vec!["rust".into(), "c".into()],
         })
@@ -975,6 +975,16 @@ async fn test_signups() {
         .unwrap();
     }
 
+    assert_eq!(
+        db.get_waitlist_summary().await.unwrap(),
+        WaitlistSummary {
+            count: 8,
+            mac_count: 8,
+            linux_count: 4,
+            windows_count: 2,
+        }
+    );
+
     // retrieve the next batch of signup emails to send
     let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
     let addresses = signups_batch1
@@ -1016,6 +1026,17 @@ async fn test_signups() {
         ]
     );
 
+    // 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,
+        }
+    );
+
     // user completes the signup process by providing their
     // github account.
     let (user_id, inviter_id) = db