mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core,schemas): support get hook recent logs (#3859)
This commit is contained in:
parent
f1730db70b
commit
b92508db3a
6 changed files with 147 additions and 5 deletions
|
@ -13,10 +13,12 @@ export type LogCondition = {
|
|||
logKey?: string;
|
||||
applicationId?: string;
|
||||
userId?: string;
|
||||
hookId?: string;
|
||||
startTimeExclusive?: number;
|
||||
};
|
||||
|
||||
const buildLogConditionSql = (logCondition: LogCondition) =>
|
||||
conditionalSql(logCondition, ({ logKey, applicationId, userId }) => {
|
||||
conditionalSql(logCondition, ({ logKey, applicationId, userId, hookId, startTimeExclusive }) => {
|
||||
const subConditions = [
|
||||
conditionalSql(logKey, (logKey) => sql`${fields.key}=${logKey}`),
|
||||
conditionalSql(userId, (userId) => sql`${fields.payload}->>'userId'=${userId}`),
|
||||
|
@ -24,6 +26,12 @@ const buildLogConditionSql = (logCondition: LogCondition) =>
|
|||
applicationId,
|
||||
(applicationId) => sql`${fields.payload}->>'applicationId'=${applicationId}`
|
||||
),
|
||||
conditionalSql(hookId, (hookId) => sql`${fields.payload}->>'hookId'=${hookId}`),
|
||||
conditionalSql(
|
||||
startTimeExclusive,
|
||||
(startTimeExclusive) =>
|
||||
sql`${fields.createdAt} > to_timestamp(${startTimeExclusive}::double precision / 1000)`
|
||||
),
|
||||
].filter(({ sql }) => sql);
|
||||
|
||||
return subConditions.length > 0 ? sql`where ${sql.join(subConditions, sql` and `)}` : sql``;
|
||||
|
|
|
@ -4,8 +4,11 @@ import {
|
|||
type HookEvents,
|
||||
type HookConfig,
|
||||
type CreateHook,
|
||||
LogResult,
|
||||
type Log,
|
||||
} from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
import {
|
||||
mockCreatedAtForHook,
|
||||
|
@ -42,13 +45,34 @@ const hooks = {
|
|||
deleteHookById: jest.fn(),
|
||||
};
|
||||
|
||||
const tenantContext = new MockTenant(undefined, { hooks });
|
||||
const mockLog: Log = {
|
||||
tenantId: 'fake_tenant',
|
||||
id: '1',
|
||||
key: 'a',
|
||||
payload: { key: 'a', result: LogResult.Success },
|
||||
createdAt: 123,
|
||||
};
|
||||
|
||||
const logs = {
|
||||
countLogs: jest.fn().mockResolvedValue({
|
||||
count: 1,
|
||||
}),
|
||||
findLogs: jest.fn().mockResolvedValue([mockLog]),
|
||||
};
|
||||
|
||||
const { countLogs, findLogs } = logs;
|
||||
|
||||
const tenantContext = new MockTenant(undefined, { hooks, logs });
|
||||
|
||||
const hookRoutes = await pickDefault(import('./hook.js'));
|
||||
|
||||
describe('hook routes', () => {
|
||||
const hookRequest = createRequester({ authedRoutes: hookRoutes, tenantContext });
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('GET /hooks', async () => {
|
||||
const response = await hookRequest.get('/hooks');
|
||||
expect(response.status).toEqual(200);
|
||||
|
@ -63,6 +87,25 @@ describe('hook routes', () => {
|
|||
expect(response.body.id).toBe(hookIdInMockList);
|
||||
});
|
||||
|
||||
it('GET /hooks/:id/recent-logs should call countLogs and findLogs with correct parameters', async () => {
|
||||
jest.useFakeTimers().setSystemTime(100_000);
|
||||
|
||||
const hookId = 'foo';
|
||||
const logKey = 'TriggerHook.PostSignIn';
|
||||
const page = 1;
|
||||
const pageSize = 5;
|
||||
|
||||
const startTimeExclusive = subDays(new Date(100_000), 1).getTime();
|
||||
|
||||
await hookRequest.get(
|
||||
`/hooks/${hookId}/recent-logs?logKey=${logKey}&page=${page}&page_size=${pageSize}`
|
||||
);
|
||||
expect(countLogs).toHaveBeenCalledWith({ hookId, logKey, startTimeExclusive });
|
||||
expect(findLogs).toHaveBeenCalledWith(5, 0, { hookId, logKey, startTimeExclusive });
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('POST /hooks', async () => {
|
||||
const name = 'fooName';
|
||||
const events: HookEvents = [HookEvent.PostRegister];
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Hooks, hookEventsGuard } from '@logto/schemas';
|
||||
import { Hooks, Logs, hookEventsGuard } from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { conditional, deduplicate } from '@silverhand/essentials';
|
||||
import { subDays } from 'date-fns';
|
||||
import { 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 assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
|
@ -16,7 +18,10 @@ const nonemptyUniqueHookEventsGuard = hookEventsGuard
|
|||
export default function hookRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const { findAllHooks, findHookById, insertHook, updateHookById, deleteHookById } = queries.hooks;
|
||||
const {
|
||||
hooks: { findAllHooks, findHookById, insertHook, updateHookById, deleteHookById },
|
||||
logs: { countLogs, findLogs },
|
||||
} = queries;
|
||||
|
||||
router.get(
|
||||
'/hooks',
|
||||
|
@ -46,6 +51,37 @@ export default function hookRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/hooks/:id/recent-logs',
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string() }),
|
||||
query: z.object({ logKey: z.string().optional() }),
|
||||
response: Logs.guard.omit({ tenantId: true }).array(),
|
||||
status: 200,
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { limit, offset } = ctx.pagination;
|
||||
|
||||
const {
|
||||
params: { id },
|
||||
query: { logKey },
|
||||
} = ctx.guard;
|
||||
|
||||
const startTimeExclusive = subDays(new Date(), 1).getTime();
|
||||
|
||||
const [{ count }, logs] = await Promise.all([
|
||||
countLogs({ logKey, hookId: id, startTimeExclusive }),
|
||||
findLogs(limit, offset, { logKey, hookId: id, startTimeExclusive }),
|
||||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = logs;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/hooks',
|
||||
koaGuard({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Hook, HookConfig, LogKey } from '@logto/schemas';
|
||||
import type { Hook, HookConfig, Log, LogKey } from '@logto/schemas';
|
||||
import { HookEvent, SignInIdentifier, LogResult, InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import { authedAdminApi, deleteUser, getLogs, putInteraction } from '#src/api/index.js';
|
||||
|
@ -225,4 +225,38 @@ describe('hooks', () => {
|
|||
]);
|
||||
await deleteUser(id);
|
||||
});
|
||||
|
||||
it('should get recent hook logs correctly', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', { json: createPayload(HookEvent.PostRegister, 'http://localhost:9999') })
|
||||
.json<Hook>();
|
||||
|
||||
// Init session and submit
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
await client.send(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const id = await processSession(client, redirectTo);
|
||||
await waitFor(500); // Wait for hooks execution
|
||||
|
||||
const logs = await authedAdminApi
|
||||
.get(`hooks/${createdHook.id}/recent-logs?page_size=100`)
|
||||
.json<Log[]>();
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result } }) =>
|
||||
hookId === createdHook.id && result === LogResult.Success
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
|
||||
await deleteUser(id);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { sql } from 'slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
await pool.query(sql`
|
||||
create index logs__hook_id on logs (tenant_id, (payload->>'hookId'));
|
||||
`);
|
||||
},
|
||||
down: async (pool) => {
|
||||
await pool.query(sql`
|
||||
drop index logs__hook_id;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -19,3 +19,6 @@ create index logs__user_id
|
|||
|
||||
create index logs__application_id
|
||||
on logs (tenant_id, (payload->>'applicationId'));
|
||||
|
||||
create index logs__hook_id
|
||||
on logs (tenant_id, (payload->>'hookId'));
|
||||
|
|
Loading…
Reference in a new issue