Skip to content

Commit 5721b2d

Browse files
committed
feat(events): add event metrics endpoint
1 parent 1863389 commit 5721b2d

File tree

9 files changed

+128
-4
lines changed

9 files changed

+128
-4
lines changed

src/modules/events/app/event.mapper.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { EventDetailDto } from '../domain/dto/get-event-res-body.dto';
2+
import type { EventMetricDto } from '../domain/dto/get-events-metrics-res-body.dto';
23
import type { EventDto } from '../domain/dto/get-events-res-body.dto';
3-
import type { Event } from '../domain/entity/event.entity';
4+
import type { Event, EventMetric } from '../domain/entity/event.entity';
45

56
export function toEventDto(event: Event): EventDto {
67
return {
@@ -24,3 +25,13 @@ export function toEventDetailDto(event: Event): EventDetailDto {
2425
payload: event.payload ?? null,
2526
};
2627
}
28+
29+
export function toEventMetricDto(event: EventMetric): EventMetricDto {
30+
return {
31+
id: event.id,
32+
source: event.source,
33+
kind: event.kind,
34+
occurredAt: event.occurredAt,
35+
level: event.level,
36+
};
37+
}

src/modules/events/app/event.service.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { inject, injectable } from 'inversify';
22
import { EventDeps } from '../domain/dep/event.dep';
33
import type { GetEventParamsDto } from '../domain/dto/get-event-params.dto';
44
import type { EventDetailDto } from '../domain/dto/get-event-res-body.dto';
5+
import type { GetEventsMetricsQueryDto } from '../domain/dto/get-events-metrics-query.dto';
6+
import type { EventMetricDto } from '../domain/dto/get-events-metrics-res-body.dto';
57
import type { GetEventsQueryDto } from '../domain/dto/get-events-query.dto';
68
import type { EventDto } from '../domain/dto/get-events-res-body.dto';
79
import type { IEventRepository } from '../domain/port/event-repo.interface';
810
import type { IEventService } from '../domain/port/event-service.interface';
9-
import { toEventDetailDto, toEventDto } from './event.mapper';
11+
import { toEventDetailDto, toEventDto, toEventMetricDto } from './event.mapper';
1012

1113
@injectable()
1214
export class EventService implements IEventService {
@@ -34,4 +36,15 @@ export class EventService implements IEventService {
3436

3537
return events.map((event) => toEventDto(event));
3638
}
39+
40+
public async listEventMetrics(dto: GetEventsMetricsQueryDto): Promise<EventMetricDto[]> {
41+
const events = await this.eventRepository.listEventMetrics({
42+
limit: dto.limit,
43+
kind: dto.kind,
44+
source: dto.source,
45+
since: dto.since,
46+
});
47+
48+
return events.map((event) => toEventMetricDto(event));
49+
}
3750
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { z } from 'zod';
2+
3+
export const schemaGetEventsMetricsQuery = z.object({
4+
limit: z.coerce.number().int().positive().max(2000).optional(),
5+
kind: z.coerce.number().int().optional(),
6+
source: z.coerce.number().int().optional(),
7+
since: z.iso.datetime().optional(),
8+
});
9+
10+
export type GetEventsMetricsQueryDto = z.infer<typeof schemaGetEventsMetricsQuery>;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { z } from 'zod';
2+
3+
export const schemaEventMetric = z.object({
4+
id: z.string(),
5+
source: z.number().int(),
6+
kind: z.number().int(),
7+
occurredAt: z.string().nullable(),
8+
level: z.number().int(),
9+
});
10+
11+
export type EventMetricDto = z.infer<typeof schemaEventMetric>;
12+
export const schemaGetEventsMetricsResBody = z.array(schemaEventMetric);

src/modules/events/domain/entity/event.entity.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,11 @@ export type NewEvent = {
3636
level: EventLevels;
3737
payload?: EventPayload | null;
3838
};
39+
40+
export type EventMetric = {
41+
id: string;
42+
source: EventSources;
43+
kind: EventKinds;
44+
occurredAt: string | null;
45+
level: EventLevels;
46+
};

src/modules/events/domain/port/event-repo.interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Event, NewEvent } from '../entity/event.entity';
1+
import type { Event, EventMetric, NewEvent } from '../entity/event.entity';
22
import type { EventKinds, EventSources } from '../event.enums';
33

44
export type ListEventsParams = {
@@ -22,6 +22,7 @@ export interface IEventRepository {
2222
insertEvent(data: NewEvent): Promise<Event>;
2323
getEventById(id: string): Promise<Event | undefined>;
2424
listEvents(params: ListEventsParams): Promise<Event[]>;
25+
listEventMetrics(params: ListEventsParams): Promise<EventMetric[]>;
2526
listEventsAfterId(params: ListEventsAfterIdParams): Promise<Event[]>;
2627
listLatestFetchedAtBySources(sourceIds: EventSources[]): Promise<LatestFetchedAtBySource[]>;
2728
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { GetEventParamsDto } from '../dto/get-event-params.dto';
22
import type { EventDetailDto } from '../dto/get-event-res-body.dto';
3+
import type { GetEventsMetricsQueryDto } from '../dto/get-events-metrics-query.dto';
4+
import type { EventMetricDto } from '../dto/get-events-metrics-res-body.dto';
35
import type { GetEventsQueryDto } from '../dto/get-events-query.dto';
46
import type { EventDto } from '../dto/get-events-res-body.dto';
57

68
export interface IEventService {
79
getEventById(dto: GetEventParamsDto): Promise<EventDetailDto | undefined>;
810
listEvents(dto: GetEventsQueryDto): Promise<EventDto[]>;
11+
listEventMetrics(dto: GetEventsMetricsQueryDto): Promise<EventMetricDto[]>;
912
}

src/modules/events/infra/event.repo.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Kysely } from 'kysely';
33
import { DbDeps } from '@/infra/db/db.dep';
44
import type { DatabaseScheme } from '@/infra/db/db-scheme';
55
import type { EventRow, NewEventRow } from '@/infra/db/events.table';
6-
import type { Event, EventGeo, NewEvent } from '../domain/entity/event.entity';
6+
import type { Event, EventGeo, EventMetric, NewEvent } from '../domain/entity/event.entity';
77
import type { EventSources } from '../domain/event.enums';
88
import type {
99
IEventRepository,
@@ -53,6 +53,31 @@ export class EventRepository implements IEventRepository {
5353
return rows.map((row) => toEvent(row));
5454
}
5555

56+
public async listEventMetrics(params: ListEventsParams): Promise<EventMetric[]> {
57+
const limit = params.limit ?? 50;
58+
59+
let query = this.db
60+
.selectFrom('events')
61+
.select(['id', 'source', 'kind', 'occurred_at', 'level'])
62+
.orderBy('fetched_at', 'desc')
63+
.limit(limit);
64+
65+
if (params.kind !== undefined) {
66+
query = query.where('kind', '=', params.kind);
67+
}
68+
69+
if (params.source) {
70+
query = query.where('source', '=', params.source);
71+
}
72+
73+
if (params.since !== undefined) {
74+
query = query.where('fetched_at', '>=', params.since);
75+
}
76+
77+
const rows = await query.execute();
78+
return rows.map((row) => toEventMetric(row));
79+
}
80+
5681
public async listEventsAfterId(params: ListEventsAfterIdParams): Promise<Event[]> {
5782
const limit = params.limit ?? 500;
5883

@@ -163,6 +188,18 @@ function toEvent(row: EventRow): Event {
163188
};
164189
}
165190

191+
type EventMetricRow = Pick<EventRow, 'id' | 'source' | 'kind' | 'occurred_at' | 'level'>;
192+
193+
function toEventMetric(row: EventMetricRow): EventMetric {
194+
return {
195+
id: row.id,
196+
source: row.source,
197+
kind: row.kind,
198+
occurredAt: normalizeTimestamp(row.occurred_at),
199+
level: row.level,
200+
};
201+
}
202+
166203
function toGeo(lat: number | null, lng: number | null): EventGeo | null {
167204
if (lat === null || lng === null) {
168205
return null;

src/modules/events/view/event.route.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type { EventStreamService } from '../app/event-stream.service';
77
import { EventDeps } from '../domain/dep/event.dep';
88
import { schemaGetEventParams } from '../domain/dto/get-event-params.dto';
99
import { schemaGetEventResBody } from '../domain/dto/get-event-res-body.dto';
10+
import { schemaGetEventsMetricsQuery } from '../domain/dto/get-events-metrics-query.dto';
11+
import { schemaGetEventsMetricsResBody } from '../domain/dto/get-events-metrics-res-body.dto';
1012
import { schemaGetEventsQuery } from '../domain/dto/get-events-query.dto';
1113
import { schemaGetEventsResBody } from '../domain/dto/get-events-res-body.dto';
1214
import { schemaGetEventsStreamQuery } from '../domain/dto/get-events-stream-query.dto';
@@ -46,6 +48,33 @@ export class EventRoute implements IRoute {
4648
},
4749
);
4850

51+
this.app.openapi(
52+
createRoute({
53+
tags: ['Events'],
54+
method: 'get',
55+
path: '/metrics',
56+
summary: 'List events for metrics',
57+
request: {
58+
query: schemaGetEventsMetricsQuery,
59+
},
60+
responses: {
61+
200: {
62+
content: {
63+
'application/json': {
64+
schema: schemaGetEventsMetricsResBody,
65+
},
66+
},
67+
description: 'Event metrics list',
68+
},
69+
},
70+
}),
71+
async (c) => {
72+
const query = c.req.valid('query');
73+
const result = await eventService.listEventMetrics(query);
74+
return c.json(result);
75+
},
76+
);
77+
4978
this.app.openapi(
5079
createRoute({
5180
tags: ['Events'],

0 commit comments

Comments
 (0)