mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 00:22:43 -05:00
feat(admin): implement multi-field search with customizable search mode
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user