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:
parent
89a185c845
commit
e63ca4c06f
4 changed files with 154 additions and 24 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) => {
|
||||
const { limit, offset } = ctx.pagination;
|
||||
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),
|
||||
]);
|
||||
const [{ count }, users] = await Promise.all([
|
||||
countUsers(search),
|
||||
findUsers(limit, offset, search),
|
||||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = users.map((user) => pick(user, ...userInfoSelectFields));
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = users.map((user) => pick(user, ...userInfoSelectFields));
|
||||
|
||||
return next();
|
||||
});
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/users/:userId',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue