diff --git a/.sqlx/query-507a60f98f6da63620abc28a5a83772f29ffcfdfcc8d30bfa7c611d73073ca16.json b/.sqlx/query-507a60f98f6da63620abc28a5a83772f29ffcfdfcc8d30bfa7c611d73073ca16.json new file mode 100644 index 0000000000..e69117bdc7 --- /dev/null +++ b/.sqlx/query-507a60f98f6da63620abc28a5a83772f29ffcfdfcc8d30bfa7c611d73073ca16.json @@ -0,0 +1,146 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n ur.id as `id: u64`,\n ur.invited_user_token as `invited_user_token: UserToken`,\n invited.username as `invited_username: String`,\n invited.display_name as `invited_display_name: String`,\n invited.email_address as `invited_email_address: String`,\n ur.referrer_user_token as `referrer_user_token: UserToken`,\n referrer.username as `referrer_username: String`,\n referrer.display_name as `referrer_display_name: String`,\n ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`,\n ur.maybe_referral_url,\n ur.maybe_landing_url,\n ur.created_at\nFROM user_referrals ur\nJOIN users invited ON invited.token = ur.invited_user_token\nJOIN users referrer ON referrer.token = ur.referrer_user_token\nORDER BY ur.id DESC\nLIMIT ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: u64", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "invited_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "invited_username: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 3, + "name": "invited_display_name: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 4, + "name": "invited_email_address: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "referrer_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 6, + "name": "referrer_username: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 7, + "name": "referrer_display_name: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 8, + "name": "maybe_referral_code_token: UserReferralCodeToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 9, + "name": "maybe_referral_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 10, + "name": "maybe_landing_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 11, + "name": "created_at", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY", + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + false + ] + }, + "hash": "507a60f98f6da63620abc28a5a83772f29ffcfdfcc8d30bfa7c611d73073ca16" +} diff --git a/.sqlx/query-b2049fa1c9f76c73a9a22144de716b47dccdd35caeefb3721e75ec9aaf4614f0.json b/.sqlx/query-b2049fa1c9f76c73a9a22144de716b47dccdd35caeefb3721e75ec9aaf4614f0.json new file mode 100644 index 0000000000..61590767f8 --- /dev/null +++ b/.sqlx/query-b2049fa1c9f76c73a9a22144de716b47dccdd35caeefb3721e75ec9aaf4614f0.json @@ -0,0 +1,146 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n ur.id as `id: u64`,\n ur.invited_user_token as `invited_user_token: UserToken`,\n invited.username as `invited_username: String`,\n invited.display_name as `invited_display_name: String`,\n invited.email_address as `invited_email_address: String`,\n ur.referrer_user_token as `referrer_user_token: UserToken`,\n referrer.username as `referrer_username: String`,\n referrer.display_name as `referrer_display_name: String`,\n ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`,\n ur.maybe_referral_url,\n ur.maybe_landing_url,\n ur.created_at\nFROM user_referrals ur\nJOIN users invited ON invited.token = ur.invited_user_token\nJOIN users referrer ON referrer.token = ur.referrer_user_token\nWHERE ur.id < ?\nORDER BY ur.id DESC\nLIMIT ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: u64", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "invited_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "invited_username: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 3, + "name": "invited_display_name: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 4, + "name": "invited_email_address: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "referrer_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 6, + "name": "referrer_username: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 7, + "name": "referrer_display_name: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 8, + "name": "maybe_referral_code_token: UserReferralCodeToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 9, + "name": "maybe_referral_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 10, + "name": "maybe_landing_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 11, + "name": "created_at", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY", + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + false + ] + }, + "hash": "b2049fa1c9f76c73a9a22144de716b47dccdd35caeefb3721e75ec9aaf4614f0" +} diff --git a/.sqlx/query-e73dfa3af6a1a8f510f084435da86571e096f3d7e597fb01d6397d380b135341.json b/.sqlx/query-e73dfa3af6a1a8f510f084435da86571e096f3d7e597fb01d6397d380b135341.json new file mode 100644 index 0000000000..b6fa156bef --- /dev/null +++ b/.sqlx/query-e73dfa3af6a1a8f510f084435da86571e096f3d7e597fb01d6397d380b135341.json @@ -0,0 +1,146 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n ur.id as `id: u64`,\n ur.invited_user_token as `invited_user_token: UserToken`,\n invited.username as `invited_username: String`,\n invited.display_name as `invited_display_name: String`,\n invited.email_address as `invited_email_address: String`,\n ur.referrer_user_token as `referrer_user_token: UserToken`,\n referrer.username as `referrer_username: String`,\n referrer.display_name as `referrer_display_name: String`,\n ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`,\n ur.maybe_referral_url,\n ur.maybe_landing_url,\n ur.created_at\nFROM user_referrals ur\nJOIN users invited ON invited.token = ur.invited_user_token\nJOIN users referrer ON referrer.token = ur.referrer_user_token\nWHERE referrer.username = ?\nORDER BY ur.id DESC\nLIMIT ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: u64", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "invited_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "invited_username: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 3, + "name": "invited_display_name: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 4, + "name": "invited_email_address: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "referrer_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 6, + "name": "referrer_username: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 7, + "name": "referrer_display_name: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 8, + "name": "maybe_referral_code_token: UserReferralCodeToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 9, + "name": "maybe_referral_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 10, + "name": "maybe_landing_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 11, + "name": "created_at", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY", + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + false + ] + }, + "hash": "e73dfa3af6a1a8f510f084435da86571e096f3d7e597fb01d6397d380b135341" +} diff --git a/.sqlx/query-fb021a5fe7a6c0e284968d917f9d612953629d20a06efccc3fce3d930a7fe266.json b/.sqlx/query-fb021a5fe7a6c0e284968d917f9d612953629d20a06efccc3fce3d930a7fe266.json new file mode 100644 index 0000000000..d11663376a --- /dev/null +++ b/.sqlx/query-fb021a5fe7a6c0e284968d917f9d612953629d20a06efccc3fce3d930a7fe266.json @@ -0,0 +1,146 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n ur.id as `id: u64`,\n ur.invited_user_token as `invited_user_token: UserToken`,\n invited.username as `invited_username: String`,\n invited.display_name as `invited_display_name: String`,\n invited.email_address as `invited_email_address: String`,\n ur.referrer_user_token as `referrer_user_token: UserToken`,\n referrer.username as `referrer_username: String`,\n referrer.display_name as `referrer_display_name: String`,\n ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`,\n ur.maybe_referral_url,\n ur.maybe_landing_url,\n ur.created_at\nFROM user_referrals ur\nJOIN users invited ON invited.token = ur.invited_user_token\nJOIN users referrer ON referrer.token = ur.referrer_user_token\nWHERE referrer.username = ?\n AND ur.id < ?\nORDER BY ur.id DESC\nLIMIT ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: u64", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "invited_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "invited_username: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 3, + "name": "invited_display_name: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 4, + "name": "invited_email_address: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "referrer_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 6, + "name": "referrer_username: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 7, + "name": "referrer_display_name: String", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 8, + "name": "maybe_referral_code_token: UserReferralCodeToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 9, + "name": "maybe_referral_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 10, + "name": "maybe_landing_url", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 11, + "name": "created_at", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY", + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + false + ] + }, + "hash": "fb021a5fe7a6c0e284968d917f9d612953629d20a06efccc3fce3d930a7fe266" +} diff --git a/crates/schema/database/mysql_queries/src/queries/user_referrals/list_global_user_referrals.rs b/crates/schema/database/mysql_queries/src/queries/user_referrals/list_global_user_referrals.rs new file mode 100644 index 0000000000..c4bc882691 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/user_referrals/list_global_user_referrals.rs @@ -0,0 +1,122 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; +use sqlx::MySqlPool; +use tokens::tokens::user_referral_codes::UserReferralCodeToken; +use tokens::tokens::users::UserToken; + +pub struct UserReferralListItem { + pub id: u64, + pub invited_user_token: UserToken, + pub invited_username: String, + pub invited_display_name: String, + pub invited_email_address: String, + pub referrer_user_token: UserToken, + pub referrer_username: String, + pub referrer_display_name: String, + pub maybe_referral_code_token: Option, + pub maybe_referral_url: Option, + pub maybe_landing_url: Option, + pub created_at: DateTime, +} + +struct RawRow { + id: u64, + invited_user_token: UserToken, + invited_username: String, + invited_display_name: String, + invited_email_address: String, + referrer_user_token: UserToken, + referrer_username: String, + referrer_display_name: String, + maybe_referral_code_token: Option, + maybe_referral_url: Option, + maybe_landing_url: Option, + created_at: NaiveDateTime, +} + +pub struct ListGlobalUserReferralsArgs<'a> { + pub maybe_cursor_id: Option, + pub limit: u32, + pub mysql_pool: &'a MySqlPool, +} + +pub async fn list_global_user_referrals( + args: ListGlobalUserReferralsArgs<'_>, +) -> Result, sqlx::Error> { + let limit = args.limit as i64; + + let rows = match args.maybe_cursor_id { + Some(cursor_id) => { + sqlx::query_as!( + RawRow, + r#" +SELECT + ur.id as `id: u64`, + ur.invited_user_token as `invited_user_token: UserToken`, + invited.username as `invited_username: String`, + invited.display_name as `invited_display_name: String`, + invited.email_address as `invited_email_address: String`, + ur.referrer_user_token as `referrer_user_token: UserToken`, + referrer.username as `referrer_username: String`, + referrer.display_name as `referrer_display_name: String`, + ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`, + ur.maybe_referral_url, + ur.maybe_landing_url, + ur.created_at +FROM user_referrals ur +JOIN users invited ON invited.token = ur.invited_user_token +JOIN users referrer ON referrer.token = ur.referrer_user_token +WHERE ur.id < ? +ORDER BY ur.id DESC +LIMIT ? + "#, + cursor_id as u64, + limit, + ) + .fetch_all(args.mysql_pool) + .await? + } + None => { + sqlx::query_as!( + RawRow, + r#" +SELECT + ur.id as `id: u64`, + ur.invited_user_token as `invited_user_token: UserToken`, + invited.username as `invited_username: String`, + invited.display_name as `invited_display_name: String`, + invited.email_address as `invited_email_address: String`, + ur.referrer_user_token as `referrer_user_token: UserToken`, + referrer.username as `referrer_username: String`, + referrer.display_name as `referrer_display_name: String`, + ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`, + ur.maybe_referral_url, + ur.maybe_landing_url, + ur.created_at +FROM user_referrals ur +JOIN users invited ON invited.token = ur.invited_user_token +JOIN users referrer ON referrer.token = ur.referrer_user_token +ORDER BY ur.id DESC +LIMIT ? + "#, + limit, + ) + .fetch_all(args.mysql_pool) + .await? + } + }; + + Ok(rows.into_iter().map(|r| UserReferralListItem { + id: r.id, + invited_user_token: r.invited_user_token, + invited_username: r.invited_username, + invited_display_name: r.invited_display_name, + invited_email_address: r.invited_email_address, + referrer_user_token: r.referrer_user_token, + referrer_username: r.referrer_username, + referrer_display_name: r.referrer_display_name, + maybe_referral_code_token: r.maybe_referral_code_token, + maybe_referral_url: r.maybe_referral_url, + maybe_landing_url: r.maybe_landing_url, + created_at: r.created_at.and_utc(), + }).collect()) +} diff --git a/crates/schema/database/mysql_queries/src/queries/user_referrals/list_user_referrals_for_user.rs b/crates/schema/database/mysql_queries/src/queries/user_referrals/list_user_referrals_for_user.rs new file mode 100644 index 0000000000..d8f1d53fb4 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/user_referrals/list_user_referrals_for_user.rs @@ -0,0 +1,114 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; +use sqlx::MySqlPool; +use tokens::tokens::user_referral_codes::UserReferralCodeToken; +use tokens::tokens::users::UserToken; + +use crate::queries::user_referrals::list_global_user_referrals::UserReferralListItem; + +struct RawRow { + id: u64, + invited_user_token: UserToken, + invited_username: String, + invited_display_name: String, + invited_email_address: String, + referrer_user_token: UserToken, + referrer_username: String, + referrer_display_name: String, + maybe_referral_code_token: Option, + maybe_referral_url: Option, + maybe_landing_url: Option, + created_at: NaiveDateTime, +} + +pub struct ListUserReferralsForUserArgs<'a> { + pub referrer_username: &'a str, + pub maybe_cursor_id: Option, + pub limit: u32, + pub mysql_pool: &'a MySqlPool, +} + +pub async fn list_user_referrals_for_user( + args: ListUserReferralsForUserArgs<'_>, +) -> Result, sqlx::Error> { + let limit = args.limit as i64; + + let rows = match args.maybe_cursor_id { + Some(cursor_id) => { + sqlx::query_as!( + RawRow, + r#" +SELECT + ur.id as `id: u64`, + ur.invited_user_token as `invited_user_token: UserToken`, + invited.username as `invited_username: String`, + invited.display_name as `invited_display_name: String`, + invited.email_address as `invited_email_address: String`, + ur.referrer_user_token as `referrer_user_token: UserToken`, + referrer.username as `referrer_username: String`, + referrer.display_name as `referrer_display_name: String`, + ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`, + ur.maybe_referral_url, + ur.maybe_landing_url, + ur.created_at +FROM user_referrals ur +JOIN users invited ON invited.token = ur.invited_user_token +JOIN users referrer ON referrer.token = ur.referrer_user_token +WHERE referrer.username = ? + AND ur.id < ? +ORDER BY ur.id DESC +LIMIT ? + "#, + args.referrer_username, + cursor_id as u64, + limit, + ) + .fetch_all(args.mysql_pool) + .await? + } + None => { + sqlx::query_as!( + RawRow, + r#" +SELECT + ur.id as `id: u64`, + ur.invited_user_token as `invited_user_token: UserToken`, + invited.username as `invited_username: String`, + invited.display_name as `invited_display_name: String`, + invited.email_address as `invited_email_address: String`, + ur.referrer_user_token as `referrer_user_token: UserToken`, + referrer.username as `referrer_username: String`, + referrer.display_name as `referrer_display_name: String`, + ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`, + ur.maybe_referral_url, + ur.maybe_landing_url, + ur.created_at +FROM user_referrals ur +JOIN users invited ON invited.token = ur.invited_user_token +JOIN users referrer ON referrer.token = ur.referrer_user_token +WHERE referrer.username = ? +ORDER BY ur.id DESC +LIMIT ? + "#, + args.referrer_username, + limit, + ) + .fetch_all(args.mysql_pool) + .await? + } + }; + + Ok(rows.into_iter().map(|r| UserReferralListItem { + id: r.id, + invited_user_token: r.invited_user_token, + invited_username: r.invited_username, + invited_display_name: r.invited_display_name, + invited_email_address: r.invited_email_address, + referrer_user_token: r.referrer_user_token, + referrer_username: r.referrer_username, + referrer_display_name: r.referrer_display_name, + maybe_referral_code_token: r.maybe_referral_code_token, + maybe_referral_url: r.maybe_referral_url, + maybe_landing_url: r.maybe_landing_url, + created_at: r.created_at.and_utc(), + }).collect()) +} diff --git a/crates/schema/database/mysql_queries/src/queries/user_referrals/mod.rs b/crates/schema/database/mysql_queries/src/queries/user_referrals/mod.rs index 6d0ef4ea86..2f134d0aaf 100644 --- a/crates/schema/database/mysql_queries/src/queries/user_referrals/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/user_referrals/mod.rs @@ -1 +1,3 @@ pub mod insert_user_referral; +pub mod list_global_user_referrals; +pub mod list_user_referrals_for_user; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/mod.rs index 10042dd601..917fcf3520 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/mod.rs @@ -7,6 +7,7 @@ pub mod staff_audit_logs; pub mod user; pub mod user_bans; pub mod user_feature_flags; +pub mod user_referrals; pub mod user_sessions; pub mod wallet_ledger_entries; pub mod wallets; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/mod.rs new file mode 100644 index 0000000000..93a3accdb6 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/mod.rs @@ -0,0 +1,2 @@ +pub mod moderator_list_global_user_referrals_handler; +pub mod moderator_list_user_referrals_for_user_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/moderator_list_global_user_referrals_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/moderator_list_global_user_referrals_handler.rs new file mode 100644 index 0000000000..95eb5740aa --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/moderator_list_global_user_referrals_handler.rs @@ -0,0 +1,160 @@ +use std::sync::Arc; + +use actix_web::web::{Json, Query}; +use actix_web::{web, HttpRequest}; +use chrono::{DateTime, Utc}; +use log::warn; +use utoipa::{IntoParams, ToSchema}; + +use mysql_queries::queries::user_referrals::list_global_user_referrals::{ + list_global_user_referrals, ListGlobalUserReferralsArgs, +}; +use tokens::tokens::user_referral_codes::UserReferralCodeToken; +use tokens::tokens::users::UserToken; + +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::web_utils::user_session::require_moderator::{ + require_moderator, UseDatabase, +}; +use crate::state::server_state::ServerState; + +const CURSOR_NAME: &str = "modusrref"; +const DEFAULT_LIMIT: u32 = 100; +const MAX_LIMIT: u32 = 1000; + +// --- Request --- + +#[derive(Deserialize, ToSchema, IntoParams)] +pub struct ListGlobalUserReferralsQueryParams { + pub cursor: Option, + pub limit: Option, +} + +// --- Response --- + +#[derive(Serialize, ToSchema)] +pub struct ListGlobalUserReferralsSuccessResponse { + pub success: bool, + pub referrals: Vec, + pub maybe_cursor: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct UserReferralResponse { + pub invited_user: InvitedUserDetails, + pub referrer_user: ReferrerUserDetails, + pub maybe_referral_code_token: Option, + pub maybe_referral_url: Option, + pub maybe_landing_url: Option, + pub created_at: DateTime, +} + +#[derive(Serialize, ToSchema)] +pub struct InvitedUserDetails { + pub token: UserToken, + pub username: String, + pub display_name: String, + pub email_address: String, +} + +#[derive(Serialize, ToSchema)] +pub struct ReferrerUserDetails { + pub token: UserToken, + pub username: String, + pub display_name: String, +} + +// --- Handler --- + +/// List all user referrals globally. Moderators only. +#[utoipa::path( + get, + tag = "Moderation", + path = "/v1/moderation/user_referrals/list", + params( + ListGlobalUserReferralsQueryParams, + ), + responses( + (status = 200, description = "Success", body = ListGlobalUserReferralsSuccessResponse), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn moderator_list_global_user_referrals_handler( + http_request: HttpRequest, + query: Query, + server_state: web::Data>, +) -> Result, AdvancedCommonWebError> { + + let _user_session = require_moderator( + &http_request, + &server_state, + UseDatabase::GrabNewConnection, + ).await.map_err(|err| { + warn!("Moderator check failed: {:?}", err); + AdvancedCommonWebError::NotAuthorized + })?; + + let limit = query.limit + .unwrap_or(DEFAULT_LIMIT) + .min(MAX_LIMIT); + + let maybe_cursor_id = match &query.cursor { + None => None, + Some(cursor_str) => { + let decoded = server_state.opaque_cursors + .decode_cursor_expecting_name(CURSOR_NAME, cursor_str) + .map_err(|err| { + warn!("Failed to decode cursor: {:?}", err); + AdvancedCommonWebError::BadInputWithSimpleMessage( + "Invalid cursor".to_string()) + })?; + decoded.last_id + } + }; + + let records = list_global_user_referrals( + ListGlobalUserReferralsArgs { + maybe_cursor_id, + limit, + mysql_pool: &server_state.mysql_pool, + }, + ).await.map_err(|err| { + warn!("Failed to list user referrals: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + let maybe_cursor = records.last().map(|last| { + server_state.opaque_cursors + .encode_last_id_cursor(CURSOR_NAME, last.id) + }).transpose().map_err(|err| { + warn!("Failed to encode cursor: {:?}", err); + AdvancedCommonWebError::server_error_with_message("Failed to encode cursor") + })?; + + let referrals = records.into_iter().map(|r| { + UserReferralResponse { + invited_user: InvitedUserDetails { + token: r.invited_user_token, + username: r.invited_username, + display_name: r.invited_display_name, + email_address: r.invited_email_address, + }, + referrer_user: ReferrerUserDetails { + token: r.referrer_user_token, + username: r.referrer_username, + display_name: r.referrer_display_name, + }, + maybe_referral_code_token: r.maybe_referral_code_token, + maybe_referral_url: r.maybe_referral_url, + maybe_landing_url: r.maybe_landing_url, + created_at: r.created_at, + } + }).collect(); + + Ok(Json(ListGlobalUserReferralsSuccessResponse { + success: true, + referrals, + maybe_cursor, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/moderator_list_user_referrals_for_user_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/moderator_list_user_referrals_for_user_handler.rs new file mode 100644 index 0000000000..aa2bebf3af --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_referrals/moderator_list_user_referrals_for_user_handler.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use actix_web::web::{Json, Path, Query}; +use actix_web::{web, HttpRequest}; +use log::warn; +use utoipa::{IntoParams, ToSchema}; + +use mysql_queries::queries::user_referrals::list_user_referrals_for_user::{ + list_user_referrals_for_user, ListUserReferralsForUserArgs, +}; + +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::endpoints::moderation::user_referrals::moderator_list_global_user_referrals_handler::{ + InvitedUserDetails, ReferrerUserDetails, UserReferralResponse, +}; +use crate::http_server::web_utils::user_session::require_moderator::{ + require_moderator, UseDatabase, +}; +use crate::state::server_state::ServerState; + +const CURSOR_NAME: &str = "modusrrefu"; +const DEFAULT_LIMIT: u32 = 100; +const MAX_LIMIT: u32 = 1000; + +// --- Request --- + +#[derive(Deserialize, ToSchema, IntoParams)] +pub struct ListUserReferralsForUserQueryParams { + pub cursor: Option, + pub limit: Option, +} + +#[derive(Deserialize, ToSchema)] +pub struct ListUserReferralsForUserPathInfo { + pub username: String, +} + +// --- Response --- + +#[derive(Serialize, ToSchema)] +pub struct ListUserReferralsForUserSuccessResponse { + pub success: bool, + pub referrals: Vec, + pub maybe_cursor: Option, +} + +// --- Handler --- + +/// List user referrals for a specific referrer user. Moderators only. +#[utoipa::path( + get, + tag = "Moderation", + path = "/v1/moderation/user_referrals/user/{username}/list", + params( + ("username" = String, description = "The referrer's username"), + ListUserReferralsForUserQueryParams, + ), + responses( + (status = 200, description = "Success", body = ListUserReferralsForUserSuccessResponse), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn moderator_list_user_referrals_for_user_handler( + http_request: HttpRequest, + path: Path, + query: Query, + server_state: web::Data>, +) -> Result, AdvancedCommonWebError> { + + let _user_session = require_moderator( + &http_request, + &server_state, + UseDatabase::GrabNewConnection, + ).await.map_err(|err| { + warn!("Moderator check failed: {:?}", err); + AdvancedCommonWebError::NotAuthorized + })?; + + let limit = query.limit + .unwrap_or(DEFAULT_LIMIT) + .min(MAX_LIMIT); + + let maybe_cursor_id = match &query.cursor { + None => None, + Some(cursor_str) => { + let decoded = server_state.opaque_cursors + .decode_cursor_expecting_name(CURSOR_NAME, cursor_str) + .map_err(|err| { + warn!("Failed to decode cursor: {:?}", err); + AdvancedCommonWebError::BadInputWithSimpleMessage( + "Invalid cursor".to_string()) + })?; + decoded.last_id + } + }; + + let referrer_username = path.username.trim().to_lowercase(); + + let records = list_user_referrals_for_user( + ListUserReferralsForUserArgs { + referrer_username: &referrer_username, + maybe_cursor_id, + limit, + mysql_pool: &server_state.mysql_pool, + }, + ).await.map_err(|err| { + warn!("Failed to list user referrals for user: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + let maybe_cursor = records.last().map(|last| { + server_state.opaque_cursors + .encode_last_id_cursor(CURSOR_NAME, last.id) + }).transpose().map_err(|err| { + warn!("Failed to encode cursor: {:?}", err); + AdvancedCommonWebError::server_error_with_message("Failed to encode cursor") + })?; + + let referrals = records.into_iter().map(|r| { + UserReferralResponse { + invited_user: InvitedUserDetails { + token: r.invited_user_token, + username: r.invited_username, + display_name: r.invited_display_name, + email_address: r.invited_email_address, + }, + referrer_user: ReferrerUserDetails { + token: r.referrer_user_token, + username: r.referrer_username, + display_name: r.referrer_display_name, + }, + maybe_referral_code_token: r.maybe_referral_code_token, + maybe_referral_url: r.maybe_referral_url, + maybe_landing_url: r.maybe_landing_url, + created_at: r.created_at, + } + }).collect(); + + Ok(Json(ListUserReferralsForUserSuccessResponse { + success: true, + referrals, + maybe_cursor, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs index 6dd4c1bae5..9656206f35 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs @@ -42,6 +42,8 @@ use crate::http_server::endpoints::moderation::user_feature_flags::moderator_edi use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_all_available_user_feature_flags_handler::moderator_list_all_available_user_feature_flags_handler; use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_user_feature_flags_handler::moderator_list_user_feature_flags_handler; use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler; +use crate::http_server::endpoints::moderation::user_referrals::moderator_list_global_user_referrals_handler::moderator_list_global_user_referrals_handler; +use crate::http_server::endpoints::moderation::user_referrals::moderator_list_user_referrals_for_user_handler::moderator_list_user_referrals_for_user_handler; use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler; use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler; @@ -132,6 +134,16 @@ pub fn add_moderator_routes (app: App) -> App .route(web::head().to(|| HttpResponse::Ok())) ) ) + .service(web::scope("/user_referrals") + .service(web::resource("/list") + .route(web::get().to(moderator_list_global_user_referrals_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/user/{username}/list") + .route(web::get().to(moderator_list_user_referrals_for_user_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) .service(web::scope("/user_sessions") .service(web::resource("/impersonate") .route(web::post().to(moderator_user_session_impersonation_request_handler))