From 67b1885b12a00fea725f41133e3f2cd661a644c2 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Sun, 8 Jan 2023 20:16:40 +0800 Subject: [PATCH] feat(core): add search and pagination for roles (#2836) --- packages/core/src/queries/roles.test.ts | 17 ----------- packages/core/src/queries/roles.ts | 38 ++++++++++++++++++++++--- packages/core/src/routes/role.test.ts | 3 +- packages/core/src/routes/role.ts | 34 +++++++++++++++++++--- 4 files changed, 66 insertions(+), 26 deletions(-) diff --git a/packages/core/src/queries/roles.test.ts b/packages/core/src/queries/roles.test.ts index a07a8fd84..ffb0a34f6 100644 --- a/packages/core/src/queries/roles.test.ts +++ b/packages/core/src/queries/roles.test.ts @@ -10,7 +10,6 @@ import { expectSqlAssert } from '#src/utils/test-utils.js'; import { deleteRoleById, - findAllRoles, findRoleById, findRoleByRoleName, findRolesByRoleIds, @@ -35,22 +34,6 @@ jest.spyOn(envSet, 'pool', 'get').mockReturnValue( describe('roles query', () => { const { table, fields } = convertToIdentifiers(Roles); - it('findAllRoles', async () => { - const expectSql = sql` - select ${sql.join(Object.values(fields), sql`, `)} - from ${table} - `; - - mockQuery.mockImplementationOnce(async (sql, values) => { - expectSqlAssert(sql, expectSql.sql); - expect(values).toEqual([]); - - return createMockQueryResult([mockRole]); - }); - - await expect(findAllRoles()).resolves.toEqual([mockRole]); - }); - it('findRolesByRoleIds', async () => { const roleIds = [mockRole.id]; const expectSql = sql` diff --git a/packages/core/src/queries/roles.ts b/packages/core/src/queries/roles.ts index cfd69d7c3..adb0e2391 100644 --- a/packages/core/src/queries/roles.ts +++ b/packages/core/src/queries/roles.ts @@ -1,5 +1,5 @@ import type { CreateRole, Role } from '@logto/schemas'; -import { Roles } from '@logto/schemas'; +import { SearchJointMode, Roles } from '@logto/schemas'; import type { OmitAutoSetFields } from '@logto/shared'; import { conditionalSql, convertToIdentifiers } from '@logto/shared'; import { sql } from 'slonik'; @@ -9,14 +9,44 @@ import { buildInsertInto } from '#src/database/insert-into.js'; import { buildUpdateWhere } from '#src/database/update-where.js'; import envSet from '#src/env-set/index.js'; import { DeletionError } from '#src/errors/SlonikError/index.js'; +import type { Search } from '#src/utils/search.js'; +import { buildConditionsFromSearch } from '#src/utils/search.js'; const { table, fields } = convertToIdentifiers(Roles); -export const findAllRoles = async () => - envSet.pool.any(sql` - select ${sql.join(Object.values(fields), sql`, `)} +const buildRoleConditions = (search: Search) => { + const hasSearch = search.matches.length > 0; + const searchFields = [Roles.fields.id, Roles.fields.name, Roles.fields.description]; + + return conditionalSql( + hasSearch, + () => sql`where ${buildConditionsFromSearch(search, searchFields)}` + ); +}; + +export const defaultUserSearch = { matches: [], isCaseSensitive: false, joint: SearchJointMode.Or }; + +export const countRoles = async (search: Search = defaultUserSearch) => + envSet.pool.one<{ count: number }>(sql` + select count(*) from ${table} + ${buildRoleConditions(search)} `); + +export const findRoles = async (limit: number, offset: number, search: Search) => + envSet.pool.any( + sql` + select ${sql.join( + Object.values(fields).map((field) => sql`${table}.${field}`), + sql`,` + )} + from ${table} + ${buildRoleConditions(search)} + limit ${limit} + offset ${offset} + ` + ); + export const findRolesByRoleIds = async (roleIds: string[]) => roleIds.length > 0 ? envSet.pool.any(sql` diff --git a/packages/core/src/routes/role.test.ts b/packages/core/src/routes/role.test.ts index 95dd1851d..e43d5b118 100644 --- a/packages/core/src/routes/role.test.ts +++ b/packages/core/src/routes/role.test.ts @@ -11,7 +11,8 @@ const { mockEsm, mockEsmWithActual } = createMockUtils(jest); const { findRoleByRoleName, findRoleById, deleteRoleById } = mockEsm( '#src/queries/roles.js', () => ({ - findAllRoles: jest.fn(async (): Promise => [mockRole]), + findRoles: jest.fn(async (): Promise => [mockRole]), + countRoles: jest.fn(async () => ({ count: 10 })), findRoleByRoleName: jest.fn(async (): Promise => undefined), insertRole: jest.fn(async (data) => ({ ...data, diff --git a/packages/core/src/routes/role.ts b/packages/core/src/routes/role.ts index a18667478..dcdc0e549 100644 --- a/packages/core/src/routes/role.ts +++ b/packages/core/src/routes/role.ts @@ -1,10 +1,12 @@ import { buildIdGenerator } from '@logto/core-kit'; import type { ScopeResponse } from '@logto/schemas'; import { Roles } from '@logto/schemas'; +import { tryThat } from '@logto/shared'; import { object, string, z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; +import koaPagination from '#src/middleware/koa-pagination.js'; import { findResourcesByIds } from '#src/queries/resource.js'; import { deleteRolesScope, @@ -12,10 +14,11 @@ import { insertRolesScopes, } from '#src/queries/roles-scopes.js'; import { + countRoles, deleteRoleById, - findAllRoles, findRoleById, findRoleByRoleName, + findRoles, insertRole, updateRoleById, } from '#src/queries/roles.js'; @@ -28,16 +31,39 @@ import { insertUsersRoles, } from '#src/queries/users-roles.js'; import assertThat from '#src/utils/assert-that.js'; +import { parseSearchParamsForSearch } from '#src/utils/search.js'; import type { AuthedRouter } from './types.js'; const roleId = buildIdGenerator(21); export default function roleRoutes(router: T) { - router.get('/roles', async (ctx, next) => { - ctx.body = await findAllRoles(); + router.get('/roles', koaPagination(), async (ctx, next) => { + const { limit, offset } = ctx.pagination; + const { searchParams } = ctx.request.URL; - return next(); + return tryThat( + async () => { + const search = parseSearchParamsForSearch(searchParams); + + const [{ count }, roles] = await Promise.all([ + countRoles(search), + findRoles(limit, offset, search), + ]); + + // Return totalCount to pagination middleware + ctx.pagination.totalCount = count; + ctx.body = roles; + + return next(); + }, + (error) => { + if (error instanceof TypeError) { + throw new RequestError({ code: 'request.invalid_input', details: error.message }, error); + } + throw error; + } + ); }); router.post(