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 { sql } from 'slonik';
import { buildFindMany } from '@/database/find-many';
import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool';
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';
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 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) =>
findUserMany({ limit, offset });
export const countUsers = async (search?: string) =>
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);

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 { mockUser, mockUserResponse } from '@/utils/mock';
import { mockUser, mockUserList, mockUserListResponse, mockUserResponse } from '@/utils/mock';
import { createRequester } from '@/utils/test-utils';
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', () => ({
findTotalNumberOfUsers: jest.fn(async () => ({ count: 10 })),
findAllUsers: jest.fn(async (): Promise<User[]> => [mockUser]),
countUsers: jest.fn(async (search) => ({
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),
hasUser: jest.fn(async () => false),
updateUserById: jest.fn(
@ -37,11 +50,28 @@ jest.mock('@/lib/user', () => ({
describe('adminUserRoutes', () => {
const userRequest = createRequester({ authedRoutes: adminUserRoutes });
afterEach(() => {
jest.clearAllMocks();
});
it('GET /users', async () => {
const response = await userRequest.get('/users');
expect(response.status).toEqual(200);
expect(response.body).toEqual([mockUserResponse]);
expect(response.header).toHaveProperty('total-number', '10');
expect(response.body).toEqual(mockUserListResponse);
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 () => {

View file

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

View file

@ -30,6 +30,81 @@ export const mockUser: User = {
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 = {
id: 'foo',
name: 'foo',