From 4ffd4c048028567f701e5a3d6a507907b63a0151 Mon Sep 17 00:00:00 2001 From: "IceHe.xyz" Date: Mon, 16 May 2022 14:43:33 +0800 Subject: [PATCH] feat(core): get /logs (#823) * feat(core): get /logs * chore(core): rename userRequest to logRequest in UTs --- packages/core/src/queries/log.ts | 43 ++++++++++++++++++++++- packages/core/src/routes/init.ts | 2 ++ packages/core/src/routes/log.test.ts | 51 ++++++++++++++++++++++++++++ packages/core/src/routes/log.ts | 38 +++++++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/routes/log.test.ts create mode 100644 packages/core/src/routes/log.ts diff --git a/packages/core/src/queries/log.ts b/packages/core/src/queries/log.ts index 354bd5470..6adc16315 100644 --- a/packages/core/src/queries/log.ts +++ b/packages/core/src/queries/log.ts @@ -1,5 +1,46 @@ -import { CreateLog, Logs } from '@logto/schemas'; +import { CreateLog, Log, Logs } from '@logto/schemas'; +import { sql } from 'slonik'; import { buildInsertInto } from '@/database/insert-into'; +import { conditionalSql, convertToIdentifiers } from '@/database/utils'; +import envSet from '@/env-set'; + +const { table, fields } = convertToIdentifiers(Logs); export const insertLog = buildInsertInto(Logs); + +export interface LogCondition { + logType?: string; + applicationId?: string; + userId?: string; +} + +const buildLogConditionSql = (logCondition: LogCondition) => + conditionalSql(logCondition, ({ logType, applicationId, userId }) => { + const subConditions = [ + conditionalSql(logType, (logType) => sql`${fields.type}=${logType}`), + conditionalSql(userId, (userId) => sql`${fields.payload}->>'userId'=${userId}`), + conditionalSql( + applicationId, + (applicationId) => sql`${fields.payload}->>'applicationId'=${applicationId}` + ), + ].filter(({ sql }) => sql); + + return sql`where ${sql.join(subConditions, sql` and `)}`; + }); + +export const countLogs = async (condition: LogCondition) => + envSet.pool.one<{ count: number }>(sql` + select count(*) + from ${table} + ${buildLogConditionSql(condition)} + `); + +export const findLogs = async (limit: number, offset: number, logCondition: LogCondition) => + envSet.pool.any(sql` + select ${sql.join(Object.values(fields), sql`,`)} + from ${table} + ${buildLogConditionSql(logCondition)} + limit ${limit} + offset ${offset} + `); diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index 173ae123c..308bca0b1 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -15,6 +15,7 @@ import statusRoutes from '@/routes/status'; import swaggerRoutes from '@/routes/swagger'; import adminUserRoutes from './admin-user'; +import logRoutes from './log'; import roleRoutes from './role'; import { AnonymousRouter, AuthedRouter } from './types'; @@ -35,6 +36,7 @@ const createRouters = (provider: Provider) => { resourceRoutes(authedRouter); signInExperiencesRoutes(authedRouter); adminUserRoutes(authedRouter); + logRoutes(authedRouter); roleRoutes(authedRouter); return [sessionRouter, anonymousRouter, authedRouter]; diff --git a/packages/core/src/routes/log.test.ts b/packages/core/src/routes/log.test.ts new file mode 100644 index 000000000..5bc654958 --- /dev/null +++ b/packages/core/src/routes/log.test.ts @@ -0,0 +1,51 @@ +import { LogCondition } from '@/queries/log'; +import logRoutes from '@/routes/log'; +import { createRequester } from '@/utils/test-utils'; + +const mockLogs = [{ id: 1 }, { id: 2 }]; + +/* eslint-disable @typescript-eslint/no-unused-vars */ +const countLogs = jest.fn(async (condition: LogCondition) => ({ + count: mockLogs.length, +})); +const findLogs = jest.fn( + async (limit: number, offset: number, condition: LogCondition) => mockLogs +); +/* eslint-enable @typescript-eslint/no-unused-vars */ + +jest.mock('@/queries/log', () => ({ + countLogs: async (condition: LogCondition) => countLogs(condition), + findLogs: async (limit: number, offset: number, condition: LogCondition) => + findLogs(limit, offset, condition), +})); + +describe('logRoutes', () => { + const logRequest = createRequester({ authedRoutes: logRoutes }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('GET /logs', () => { + it('should call countLogs and findLogs with correct parameters', async () => { + const userId = 'userIdValue'; + const applicationId = 'foo'; + const logType = 'SignInUsernamePassword'; + const page = 1; + const pageSize = 5; + + await logRequest.get( + `/logs?userId=${userId}&applicationId=${applicationId}&logType=${logType}&page=${page}&page_size=${pageSize}` + ); + expect(countLogs).toHaveBeenCalledWith({ userId, applicationId, logType }); + expect(findLogs).toHaveBeenCalledWith(5, 0, { userId, applicationId, logType }); + }); + + it('should return correct response', async () => { + const response = await logRequest.get(`/logs`); + expect(response.status).toEqual(200); + expect(response.body).toEqual(mockLogs); + expect(response.header).toHaveProperty('total-number', `${mockLogs.length}`); + }); + }); +}); diff --git a/packages/core/src/routes/log.ts b/packages/core/src/routes/log.ts new file mode 100644 index 000000000..5f35b48ed --- /dev/null +++ b/packages/core/src/routes/log.ts @@ -0,0 +1,38 @@ +import { object, string } from 'zod'; + +import koaGuard from '@/middleware/koa-guard'; +import koaPagination from '@/middleware/koa-pagination'; +import { countLogs, findLogs } from '@/queries/log'; + +import { AuthedRouter } from './types'; + +export default function logRoutes(router: T) { + router.get( + '/logs', + koaPagination(), + koaGuard({ + query: object({ + userId: string().optional(), + applicationId: string().optional(), + logType: string().optional(), + }), + }), + async (ctx, next) => { + const { limit, offset } = ctx.pagination; + const { + query: { userId, applicationId, logType }, + } = ctx.guard; + + const [{ count }, logs] = await Promise.all([ + countLogs({ logType, applicationId, userId }), + findLogs(limit, offset, { logType, userId, applicationId }), + ]); + + // Return totalCount to pagination middleware + ctx.pagination.totalCount = count; + ctx.body = logs; + + return next(); + } + ); +}