Admin API filter to search users by username
This commit is contained in:
@@ -58,6 +58,13 @@ pub struct FilterParams {
|
||||
#[serde(rename = "filter[legacy-guest]")]
|
||||
legacy_guest: Option<bool>,
|
||||
|
||||
/// Retrieve users where the username matches contains the given string
|
||||
///
|
||||
/// Note that this doesn't change the ordering of the result, which are
|
||||
/// still ordered by ID.
|
||||
#[serde(rename = "filter[search]")]
|
||||
search: Option<String>,
|
||||
|
||||
/// Retrieve the items with the given status
|
||||
///
|
||||
/// Defaults to retrieve all users, including locked ones.
|
||||
@@ -83,6 +90,10 @@ impl std::fmt::Display for FilterParams {
|
||||
write!(f, "{sep}filter[legacy-guest]={legacy_guest}")?;
|
||||
sep = '&';
|
||||
}
|
||||
if let Some(search) = &self.search {
|
||||
write!(f, "{sep}filter[search]={search}")?;
|
||||
sep = '&';
|
||||
}
|
||||
if let Some(status) = self.status {
|
||||
write!(f, "{sep}filter[status]={status}")?;
|
||||
sep = '&';
|
||||
@@ -157,6 +168,11 @@ pub async fn handler(
|
||||
None => filter,
|
||||
};
|
||||
|
||||
let filter = match params.search.as_deref() {
|
||||
Some(search) => filter.matching_search(search),
|
||||
None => filter,
|
||||
};
|
||||
|
||||
let filter = match params.status {
|
||||
Some(UserStatus::Active) => filter.active_only(),
|
||||
Some(UserStatus::Locked) => filter.locked_only(),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- This enables the pg_trgm extension, which is used for search filters
|
||||
-- Starting Posgres 16, this extension is marked as "trusted", meaning it can be
|
||||
-- installed by non-superusers
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- This adds an index on the username field for ILIKE '%search%' operations,
|
||||
-- enabling fuzzy searches of usernames
|
||||
CREATE INDEX CONCURRENTLY users_username_trgm_idx
|
||||
ON users USING gin(username gin_trgm_ops);
|
||||
@@ -11,7 +11,7 @@ use async_trait::async_trait;
|
||||
use mas_data_model::{Clock, User};
|
||||
use mas_storage::user::{UserFilter, UserRepository};
|
||||
use rand::RngCore;
|
||||
use sea_query::{Expr, PostgresQueryBuilder, Query};
|
||||
use sea_query::{Expr, PostgresQueryBuilder, Query, extension::postgres::PgExpr as _};
|
||||
use sea_query_binder::SqlxBinder;
|
||||
use sqlx::PgConnection;
|
||||
use ulid::Ulid;
|
||||
@@ -120,6 +120,9 @@ impl Filter for UserFilter<'_> {
|
||||
self.is_guest()
|
||||
.map(|is_guest| Expr::col((Users::Table, Users::IsGuest)).eq(is_guest)),
|
||||
)
|
||||
.add_option(self.search().map(|search| {
|
||||
Expr::col((Users::Table, Users::Username)).ilike(format!("%{search}%"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -174,6 +174,19 @@ async fn test_user_repo(pool: PgPool) {
|
||||
assert_eq!(repo.user().count(locked).await.unwrap(), 0);
|
||||
assert_eq!(repo.user().count(deactivated).await.unwrap(), 1);
|
||||
|
||||
// Test the search filter
|
||||
assert_eq!(
|
||||
repo.user()
|
||||
.count(all.matching_search("alice"))
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
repo.user().count(all.matching_search("JO")).await.unwrap(),
|
||||
1
|
||||
);
|
||||
|
||||
// Check the list method
|
||||
let list = repo.user().list(all, Pagination::first(10)).await.unwrap();
|
||||
assert_eq!(list.edges.len(), 1);
|
||||
|
||||
@@ -76,10 +76,10 @@ pub struct UserFilter<'a> {
|
||||
state: Option<UserState>,
|
||||
can_request_admin: Option<bool>,
|
||||
is_guest: Option<bool>,
|
||||
_phantom: std::marker::PhantomData<&'a ()>,
|
||||
search: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl UserFilter<'_> {
|
||||
impl<'a> UserFilter<'a> {
|
||||
/// Create a new [`UserFilter`] with default values
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
@@ -135,6 +135,13 @@ impl UserFilter<'_> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter for users that match the given search string
|
||||
#[must_use]
|
||||
pub fn matching_search(mut self, search: &'a str) -> Self {
|
||||
self.search = Some(search);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the state filter
|
||||
///
|
||||
/// Returns [`None`] if no state filter was set
|
||||
@@ -158,6 +165,14 @@ impl UserFilter<'_> {
|
||||
pub fn is_guest(&self) -> Option<bool> {
|
||||
self.is_guest
|
||||
}
|
||||
|
||||
/// Get the search filter
|
||||
///
|
||||
/// Returns [`None`] if no search filter was set
|
||||
#[must_use]
|
||||
pub fn search(&self) -> Option<&'a str> {
|
||||
self.search
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
|
||||
|
||||
@@ -915,6 +915,17 @@
|
||||
},
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "filter[search]",
|
||||
"description": "Retrieve users where the username matches contains the given string\n\nNote that this doesn't change the ordering of the result, which are still ordered by ID.",
|
||||
"schema": {
|
||||
"description": "Retrieve users where the username matches contains the given string\n\nNote that this doesn't change the ordering of the result, which are still ordered by ID.",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "filter[status]",
|
||||
@@ -3893,6 +3904,11 @@
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
},
|
||||
"filter[search]": {
|
||||
"description": "Retrieve users where the username matches contains the given string\n\nNote that this doesn't change the ordering of the result, which are still ordered by ID.",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"filter[status]": {
|
||||
"description": "Retrieve the items with the given status\n\nDefaults to retrieve all users, including locked ones.\n\n* `active`: Only retrieve active users\n\n* `locked`: Only retrieve locked users (includes deactivated users)\n\n* `deactivated`: Only retrieve deactivated users",
|
||||
"$ref": "#/components/schemas/UserStatus",
|
||||
|
||||
Reference in New Issue
Block a user