0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-03 22:15:32 -05:00

feat(core): get /users with search (#270)

This commit is contained in:
Xiao Yijun 2022-02-24 12:29:34 +08:00 committed by GitHub
parent 89a185c845
commit e63ca4c06f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 154 additions and 24 deletions

View file

@ -1,11 +1,10 @@
import { User, CreateUser, Users } from '@logto/schemas'; import { User, CreateUser, Users } from '@logto/schemas';
import { sql } from 'slonik'; import { sql } from 'slonik';
import { buildFindMany } from '@/database/find-many';
import { buildInsertInto } from '@/database/insert-into'; import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool'; import pool from '@/database/pool';
import { buildUpdateWhere } from '@/database/update-where'; import { buildUpdateWhere } from '@/database/update-where';
import { convertToIdentifiers, getTotalRowCount, OmitAutoSetFields } from '@/database/utils'; import { conditionalSql, convertToIdentifiers, OmitAutoSetFields } from '@/database/utils';
import { DeletionError } from '@/errors/SlonikError'; import { DeletionError } from '@/errors/SlonikError';
const { table, fields } = convertToIdentifiers(Users); const { table, fields } = convertToIdentifiers(Users);
@ -86,12 +85,30 @@ export const hasUserWithIdentity = async (connectorId: string, userId: string) =
export const insertUser = buildInsertInto<CreateUser, User>(pool, Users, { returning: true }); export const insertUser = buildInsertInto<CreateUser, User>(pool, Users, { returning: true });
export const findTotalNumberOfUsers = async () => getTotalRowCount(table); const buildUserSearchConditionSql = (search: string) => {
const searchFields = [fields.primaryEmail, fields.primaryPhone, fields.username, fields.name];
const conditions = searchFields.map((filedName) => sql`${filedName} like ${'%' + search + '%'}`);
const findUserMany = buildFindMany<CreateUser, User>(pool, Users); return sql`${sql.join(conditions, sql` or `)}`;
};
export const findAllUsers = async (limit: number, offset: number) => export const countUsers = async (search?: string) =>
findUserMany({ limit, offset }); pool.one<{ count: number }>(sql`
select count(*)
from ${table}
${conditionalSql(search, (search) => sql`where ${buildUserSearchConditionSql(search)}`)}
`);
export const findUsers = async (limit: number, offset: number, search?: string) =>
pool.many<User>(
sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
${conditionalSql(search, (search) => sql`where ${buildUserSearchConditionSql(search)}`)}
limit ${limit}
offset ${offset}
`
);
const updateUser = buildUpdateWhere<CreateUser, User>(pool, Users, true); const updateUser = buildUpdateWhere<CreateUser, User>(pool, Users, true);

View file

@ -1,14 +1,27 @@
import { CreateUser, User } from '@logto/schemas'; import { CreateUser, User, userInfoSelectFields } from '@logto/schemas';
import pick from 'lodash.pick';
import { hasUser, findUserById } from '@/queries/user'; import { hasUser, findUserById } from '@/queries/user';
import { mockUser, mockUserResponse } from '@/utils/mock'; import { mockUser, mockUserList, mockUserListResponse, mockUserResponse } from '@/utils/mock';
import { createRequester } from '@/utils/test-utils'; import { createRequester } from '@/utils/test-utils';
import adminUserRoutes from './admin-user'; import adminUserRoutes from './admin-user';
const filterUsersWithSearch = (users: User[], search: string) =>
users.filter((user) =>
[user.username, user.primaryEmail, user.primaryPhone, user.name].some((value) =>
value ? !value.includes(search) : false
)
);
jest.mock('@/queries/user', () => ({ jest.mock('@/queries/user', () => ({
findTotalNumberOfUsers: jest.fn(async () => ({ count: 10 })), countUsers: jest.fn(async (search) => ({
findAllUsers: jest.fn(async (): Promise<User[]> => [mockUser]), count: search ? filterUsersWithSearch(mockUserList, search).length : mockUserList.length,
})),
findUsers: jest.fn(
async (limit, offset, search): Promise<User[]> =>
search ? filterUsersWithSearch(mockUserList, search) : mockUserList
),
findUserById: jest.fn(async (): Promise<User> => mockUser), findUserById: jest.fn(async (): Promise<User> => mockUser),
hasUser: jest.fn(async () => false), hasUser: jest.fn(async () => false),
updateUserById: jest.fn( updateUserById: jest.fn(
@ -37,11 +50,28 @@ jest.mock('@/lib/user', () => ({
describe('adminUserRoutes', () => { describe('adminUserRoutes', () => {
const userRequest = createRequester({ authedRoutes: adminUserRoutes }); const userRequest = createRequester({ authedRoutes: adminUserRoutes });
afterEach(() => {
jest.clearAllMocks();
});
it('GET /users', async () => { it('GET /users', async () => {
const response = await userRequest.get('/users'); const response = await userRequest.get('/users');
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
expect(response.body).toEqual([mockUserResponse]); expect(response.body).toEqual(mockUserListResponse);
expect(response.header).toHaveProperty('total-number', '10'); expect(response.header).toHaveProperty('total-number', `${mockUserList.length}`);
});
it('GET /users should return matched data', async () => {
const search = 'foo';
const response = await userRequest.get('/users').send({ search });
expect(response.status).toEqual(200);
expect(response.body).toEqual(
filterUsersWithSearch(mockUserList, search).map((user) => pick(user, ...userInfoSelectFields))
);
expect(response.header).toHaveProperty(
'total-number',
`${filterUsersWithSearch(mockUserList, search).length}`
);
}); });
it('GET /users/:userId', async () => { it('GET /users/:userId', async () => {

View file

@ -11,8 +11,8 @@ import { findRolesByRoleNames } from '@/queries/roles';
import { import {
clearUserCustomDataById, clearUserCustomDataById,
deleteUserById, deleteUserById,
findAllUsers, findUsers,
findTotalNumberOfUsers, countUsers,
findUserById, findUserById,
hasUser, hasUser,
insertUser, insertUser,
@ -23,19 +23,27 @@ import assertThat from '@/utils/assert-that';
import { AuthedRouter } from './types'; import { AuthedRouter } from './types';
export default function adminUserRoutes<T extends AuthedRouter>(router: T) { export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
router.get('/users', koaPagination(), async (ctx, next) => { router.get(
const { limit, offset } = ctx.pagination; '/users',
koaPagination(),
koaGuard({ query: object({ search: string().optional() }) }),
async (ctx, next) => {
const { limit, offset } = ctx.pagination;
const {
query: { search },
} = ctx.guard;
const [{ count }, users] = await Promise.all([ const [{ count }, users] = await Promise.all([
findTotalNumberOfUsers(), countUsers(search),
findAllUsers(limit, offset), findUsers(limit, offset, search),
]); ]);
ctx.pagination.totalCount = count; ctx.pagination.totalCount = count;
ctx.body = users.map((user) => pick(user, ...userInfoSelectFields)); ctx.body = users.map((user) => pick(user, ...userInfoSelectFields));
return next(); return next();
}); }
);
router.get( router.get(
'/users/:userId', '/users/:userId',

View file

@ -30,6 +30,81 @@ export const mockUser: User = {
export const mockUserResponse = pick(mockUser, ...userInfoSelectFields); export const mockUserResponse = pick(mockUser, ...userInfoSelectFields);
export const mockUserList: User[] = [
{
id: '1',
username: 'foo1',
primaryEmail: 'foo1@logto.io',
primaryPhone: '111111',
roleNames: ['admin'],
passwordEncrypted: null,
passwordEncryptionMethod: null,
passwordEncryptionSalt: null,
name: null,
avatar: null,
identities: {},
customData: {},
},
{
id: '2',
username: 'foo2',
primaryEmail: 'foo2@logto.io',
primaryPhone: '111111',
roleNames: ['admin'],
passwordEncrypted: null,
passwordEncryptionMethod: null,
passwordEncryptionSalt: null,
name: null,
avatar: null,
identities: {},
customData: {},
},
{
id: '3',
username: 'foo3',
primaryEmail: 'foo3@logto.io',
primaryPhone: '111111',
roleNames: ['admin'],
passwordEncrypted: null,
passwordEncryptionMethod: null,
passwordEncryptionSalt: null,
name: null,
avatar: null,
identities: {},
customData: {},
},
{
id: '4',
username: 'bar1',
primaryEmail: 'bar1@logto.io',
primaryPhone: '111111',
roleNames: ['admin'],
passwordEncrypted: null,
passwordEncryptionMethod: null,
passwordEncryptionSalt: null,
name: null,
avatar: null,
identities: {},
customData: {},
},
{
id: '5',
username: 'bar2',
primaryEmail: 'bar2@logto.io',
primaryPhone: '111111',
roleNames: ['admin'],
passwordEncrypted: null,
passwordEncryptionMethod: null,
passwordEncryptionSalt: null,
name: null,
avatar: null,
identities: {},
customData: {},
},
];
export const mockUserListResponse = mockUserList.map((user) => pick(user, ...userInfoSelectFields));
export const mockApplication: Application = { export const mockApplication: Application = {
id: 'foo', id: 'foo',
name: 'foo', name: 'foo',