0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

feat: add case insensitive user search (#2300)

Co-authored-by: wangsijie <wangsijie94@gmail.com>
Co-authored-by: Charles Zhao <charleszhao@silverhand.io>
Co-authored-by: wangsijie <wangsijie@silverhand.io>
This commit is contained in:
İhsan Güldür 2022-11-04 05:47:54 +03:00 committed by GitHub
parent 492ce312be
commit 938608e72b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 20 deletions

View file

@ -240,7 +240,7 @@ describe('user query', () => {
const expectSql = sql`
select count(*)
from ${table}
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${fields.username} like $3 or ${fields.name} like $4
where ${fields.primaryEmail} ilike $1 or ${fields.primaryPhone} ilike $2 or ${fields.username} ilike $3 or ${fields.name} ilike $4
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
@ -259,7 +259,7 @@ describe('user query', () => {
select count(*)
from ${table}
where not (${fields.roleNames}::jsonb?$1)
and (${fields.primaryEmail} like $2 or ${fields.primaryPhone} like $3 or ${fields.username} like $4 or ${fields.name} like $5)
and (${fields.primaryEmail} ilike $2 or ${fields.primaryPhone} ilike $3 or ${fields.username} ilike $4 or ${fields.name} ilike $5)
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
@ -278,6 +278,24 @@ describe('user query', () => {
await expect(countUsers(search, true)).resolves.toEqual(dbvalue);
});
it('countUsers with isCaseSensitive', async () => {
const search = 'foo';
const expectSql = sql`
select count(*)
from ${table}
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${fields.username} like $3 or ${fields.name} like $4
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`]);
return createMockQueryResult([dbvalue]);
});
await expect(countUsers(search, undefined, true)).resolves.toEqual(dbvalue);
});
it('findUsers', async () => {
const search = 'foo';
const limit = 100;
@ -285,9 +303,9 @@ describe('user query', () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${
where ${fields.primaryEmail} ilike $1 or ${fields.primaryPhone} ilike $2 or ${
fields.username
} like $3 or ${fields.name} like $4
} ilike $3 or ${fields.name} ilike $4
limit $5
offset $6
`;
@ -317,9 +335,9 @@ describe('user query', () => {
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where not (${fields.roleNames}::jsonb?$1)
and (${fields.primaryEmail} like $2 or ${fields.primaryPhone} like $3 or ${
and (${fields.primaryEmail} ilike $2 or ${fields.primaryPhone} ilike $3 or ${
fields.username
} like $4 or ${fields.name} like $5)
} ilike $4 or ${fields.name} ilike $5)
limit $6
offset $7
`;
@ -342,6 +360,37 @@ describe('user query', () => {
await expect(findUsers(limit, offset, search, true)).resolves.toEqual([dbvalue]);
});
it('findUsers with isCaseSensitive', async () => {
const search = 'foo';
const limit = 100;
const offset = 1;
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${
fields.username
} like $3 or ${fields.name} like $4
limit $5
offset $6
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([
`%${search}%`,
`%${search}%`,
`%${search}%`,
`%${search}%`,
limit,
offset,
]);
return createMockQueryResult([dbvalue]);
});
await expect(findUsers(limit, offset, search, undefined, true)).resolves.toEqual([dbvalue]);
});
it('updateUserById', async () => {
const username = 'Joe';
const id = 'foo';

View file

@ -84,44 +84,64 @@ export const hasUserWithIdentity = async (target: string, userId: string) =>
`
);
const buildUserSearchConditionSql = (search: string) => {
const buildUserSearchConditionSql = (search: string, isCaseSensitive = false) => {
const searchFields = [fields.primaryEmail, fields.primaryPhone, fields.username, fields.name];
const conditions = searchFields.map((filedName) => sql`${filedName} like ${'%' + search + '%'}`);
return sql`${sql.join(conditions, sql` or `)}`;
return sql`${sql.join(
searchFields.map(
(filedName) =>
sql`${filedName} ${isCaseSensitive ? sql`like` : sql`ilike`} ${'%' + search + '%'}`
),
sql` or `
)}`;
};
const buildUserConditions = (search?: string, hideAdminUser?: boolean) => {
const buildUserConditions = (
search?: string,
hideAdminUser?: boolean,
isCaseSensitive?: boolean
) => {
if (hideAdminUser) {
return sql`
where not (${fields.roleNames}::jsonb?${UserRole.Admin})
${conditionalSql(search, (search) => sql`and (${buildUserSearchConditionSql(search)})`)}
${conditionalSql(
search,
(search) => sql`and (${buildUserSearchConditionSql(search, isCaseSensitive)})`
)}
`;
}
return sql`
${conditionalSql(search, (search) => sql`where ${buildUserSearchConditionSql(search)}`)}
${conditionalSql(
search,
(search) => sql`where ${buildUserSearchConditionSql(search, isCaseSensitive)}`
)}
`;
};
export const countUsers = async (search?: string, hideAdminUser?: boolean) =>
export const countUsers = async (
search?: string,
hideAdminUser?: boolean,
isCaseSensitive?: boolean
) =>
envSet.pool.one<{ count: number }>(sql`
select count(*)
from ${table}
${buildUserConditions(search, hideAdminUser)}
${buildUserConditions(search, hideAdminUser, isCaseSensitive)}
`);
export const findUsers = async (
limit: number,
offset: number,
search?: string,
hideAdminUser?: boolean
hideAdminUser?: boolean,
isCaseSensitive?: boolean
) =>
envSet.pool.any<User>(
sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
${buildUserConditions(search, hideAdminUser)}
${buildUserConditions(search, hideAdminUser, isCaseSensitive)}
limit ${limit}
offset ${offset}
`

View file

@ -27,18 +27,23 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
'/users',
koaPagination(),
koaGuard({
query: object({ search: string().optional(), hideAdminUser: literal('true').optional() }),
query: object({
search: string().optional(),
hideAdminUser: literal('true').optional(),
isCaseSensitive: literal('true').optional(),
}),
}),
async (ctx, next) => {
const { limit, offset } = ctx.pagination;
const {
query: { search, hideAdminUser: _hideAdminUser },
query: { search, hideAdminUser: _hideAdminUser, isCaseSensitive: _isCaseSensitive },
} = ctx.guard;
const hideAdminUser = _hideAdminUser === 'true';
const isCaseSensitive = _isCaseSensitive === 'true';
const [{ count }, users] = await Promise.all([
countUsers(search, hideAdminUser),
findUsers(limit, offset, search, hideAdminUser),
countUsers(search, hideAdminUser, isCaseSensitive),
findUsers(limit, offset, search, hideAdminUser, isCaseSensitive),
]);
ctx.pagination.totalCount = count;