feat(admin): implement multi-field search with customizable search mode

This commit is contained in:
Yasser5711
2025-11-14 17:29:43 +01:00
parent 0885653257
commit 07ad83fa1f
3 changed files with 155 additions and 5 deletions

View File

@@ -130,6 +130,17 @@ type listUsers = {
* The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`.
*/
searchOperator?: "contains" | "starts_with" | "ends_with" = "contains"
/**
* The fields to search in when `searchField` is not provided.
* Example: ["email", "name"]
*/
searchInFields?: string[] = ["email"]
/**
* How to combine multiple fields from `searchInFields`.
* - OR: match any field (default)
* - AND: match all fields
*/
searchMode?: "OR" | "AND" = "OR"
/**
* The number of users to return. Defaults to 100.
*/

View File

@@ -323,7 +323,109 @@ describe("Admin plugin", async () => {
});
expect(res.data?.users.length).toBe(1);
});
it("should allow multi-field search with OR (default) using searchInFields", async () => {
const user1 = await client.admin.createUser(
{
name: "user1",
email: "user2@user4.com",
password: "password",
role: "user",
},
{
headers: adminHeaders,
},
);
const user2 = await client.admin.createUser(
{
name: "user5",
email: "user6@user1.com",
password: "password",
role: "user",
},
{
headers: adminHeaders,
},
);
expect(user1.data?.user.id).toBeDefined();
expect(user2.data?.user.id).toBeDefined();
const users = [user1.data?.user, user2.data?.user];
const res = await client.admin.listUsers({
query: {
searchValue: "user1",
searchInFields: ["email", "name"],
searchOperator: "contains",
},
fetchOptions: {
headers: adminHeaders,
},
});
expect(res.error).toBeNull();
expect(res.data?.users).toEqual(users);
for (const user of res.data?.users ?? []) {
await client.admin.removeUser(
{
userId: user.id,
},
{
headers: adminHeaders,
},
);
}
});
it("should support searchMode AND when using searchInFields", async () => {
const user1 = await client.admin.createUser(
{
name: "user1",
email: "user2@user4.com",
password: "password",
role: "user",
},
{
headers: adminHeaders,
},
);
const user2 = await client.admin.createUser(
{
name: "user5",
email: "user6@user1.com",
password: "password",
role: "user",
},
{
headers: adminHeaders,
},
);
expect(user1.data?.user.id).toBeDefined();
expect(user2.data?.user.id).toBeDefined();
const users = [user1.data?.user, user2.data?.user];
const res = await client.admin.listUsers({
query: {
searchValue: "user1",
searchInFields: ["email", "name"],
searchOperator: "contains",
searchMode: "AND",
},
fetchOptions: {
headers: adminHeaders,
},
});
expect(res.error).toBeNull();
expect(res.data?.users).toEqual([]);
for (const user of res.data?.users ?? []) {
await client.admin.removeUser(
{
userId: user.id,
},
{
headers: adminHeaders,
},
);
}
});
it("should allow to filter users by role", async () => {
const res = await client.admin.listUsers({
query: {

View File

@@ -618,6 +618,24 @@ export const admin = <O extends AdminOptions>(options?: O | undefined) => {
'The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`. Eg: "contains"',
})
.optional(),
searchInFields: z
.union([z.array(z.string()), z.string()])
.optional()
.transform((value) => {
if (!value) return undefined;
if (Array.isArray(value)) return value;
return value.split(",").map((v) => v.trim());
})
.meta({
description:
"Fields to search across if searchField is not provided",
}),
searchMode: z.enum(["OR", "AND"]).optional().default("OR").meta({
description:
"Combine multi-field search with OR (default) or AND",
}),
limit: z
.string()
.meta({
@@ -722,11 +740,30 @@ export const admin = <O extends AdminOptions>(options?: O | undefined) => {
const where: Where[] = [];
if (ctx.query?.searchValue) {
where.push({
field: ctx.query.searchField || "email",
operator: ctx.query.searchOperator || "contains",
value: ctx.query.searchValue,
});
const operator = ctx.query.searchOperator || "contains";
const searchValue = ctx.query.searchValue;
if (ctx.query.searchField) {
where.push({
field: ctx.query.searchField,
operator,
value: searchValue,
});
} else {
const fields = ctx.query.searchInFields?.length
? ctx.query.searchInFields
: ["email"]; // default
const connector = ctx.query.searchMode || "OR";
fields.forEach((field) => {
where.push({
field,
operator,
value: searchValue,
connector,
});
});
}
}
if (ctx.query?.filterValue) {