mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 18:14:49 +00:00
feat: user search (#6302)
* feat: user search * refactor: use sqlx macro * refactor: better name for escaped query * style: cleanup unused import * chore: update sqlx query cache * fix: tests --------- Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
This commit is contained in:
@@ -57,6 +57,13 @@ pub struct DBUser {
|
||||
pub is_subscribed_to_newsletter: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct DBSearchUser {
|
||||
pub id: DBUserId,
|
||||
pub username: String,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct Pride26CampaignDonation {
|
||||
pub last_donated_at: DateTime<Utc>,
|
||||
@@ -264,6 +271,44 @@ impl DBUser {
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
pub async fn search<'a, E>(
|
||||
query: &str,
|
||||
exec: E,
|
||||
) -> Result<Vec<DBSearchUser>, sqlx::Error>
|
||||
where
|
||||
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if query.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let lowercase_query = query.to_lowercase();
|
||||
let escaped_query = format!("{}%", escape_like(&lowercase_query));
|
||||
|
||||
let users = sqlx::query!(
|
||||
"
|
||||
SELECT id, username, avatar_url
|
||||
FROM users
|
||||
WHERE LOWER(username) LIKE $1 ESCAPE '\'
|
||||
ORDER BY LOWER(username) = $2 DESC, LOWER(username), username
|
||||
LIMIT 25
|
||||
",
|
||||
escaped_query,
|
||||
lowercase_query
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|row| DBSearchUser {
|
||||
id: DBUserId(row.id),
|
||||
username: row.username,
|
||||
avatar_url: row.avatar_url,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub async fn get_by_email<'a, E>(
|
||||
email: &str,
|
||||
exec: E,
|
||||
@@ -1044,3 +1089,14 @@ impl DBUser {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_like(query: &str) -> String {
|
||||
let mut escaped = String::with_capacity(query.len());
|
||||
for ch in query.chars() {
|
||||
if matches!(ch, '\\' | '%' | '_') {
|
||||
escaped.push('\\');
|
||||
}
|
||||
escaped.push(ch);
|
||||
}
|
||||
escaped
|
||||
}
|
||||
|
||||
@@ -73,6 +73,13 @@ pub struct User {
|
||||
pub github_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct SearchUser {
|
||||
pub id: UserId,
|
||||
pub username: String,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UserCampaigns {
|
||||
pub pride_26: Option<Pride26CampaignDonation>,
|
||||
@@ -87,7 +94,9 @@ pub struct UserPayoutData {
|
||||
pub balance: Decimal,
|
||||
}
|
||||
|
||||
use crate::database::models::user_item::{DBUser, Pride26CampaignDonation};
|
||||
use crate::database::models::user_item::{
|
||||
DBSearchUser, DBUser, Pride26CampaignDonation,
|
||||
};
|
||||
|
||||
impl From<DBUser> for User {
|
||||
fn from(data: DBUser) -> Self {
|
||||
@@ -116,6 +125,16 @@ impl From<DBUser> for User {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DBSearchUser> for SearchUser {
|
||||
fn from(data: DBSearchUser) -> Self {
|
||||
Self {
|
||||
id: data.id.into(),
|
||||
username: data.username,
|
||||
avatar_url: data.avatar_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn from_full(db_user: DBUser) -> Self {
|
||||
let mut auth_providers = Vec::new();
|
||||
|
||||
@@ -39,6 +39,7 @@ use validator::Validate;
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route("user", web::get().to(user_auth_get));
|
||||
cfg.route("users", web::get().to(users_get));
|
||||
cfg.route("users/search", web::get().to(users_search));
|
||||
cfg.route("user_email", web::get().to(admin_user_email));
|
||||
|
||||
cfg.service(
|
||||
@@ -327,6 +328,26 @@ pub struct UserIds {
|
||||
pub ids: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UserSearchQuery {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
pub async fn users_search(
|
||||
web::Query(query): web::Query<UserSearchQuery>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<web::Json<Vec<crate::models::users::SearchUser>>, ApiError> {
|
||||
let query = query.query.trim();
|
||||
let users = DBUser::search(query, &**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to search users")?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok(web::Json(users))
|
||||
}
|
||||
|
||||
pub async fn users_get(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<UserIds>,
|
||||
|
||||
Reference in New Issue
Block a user