mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
feat: ability to search events by type with pagination
This commit is contained in:
parent
f588fbdfc4
commit
a402ad8c66
110
src/lib/features/events/event-search-by-type-read-model.test.ts
Normal file
110
src/lib/features/events/event-search-by-type-read-model.test.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import EventStore from './event-store.js';
|
||||||
|
import getLogger from '../../../test/fixtures/no-logger.js';
|
||||||
|
import dbInit, {
|
||||||
|
type ITestDb,
|
||||||
|
} from '../../../test/e2e/helpers/database-init.js';
|
||||||
|
import {
|
||||||
|
USER_CREATED,
|
||||||
|
USER_DELETED,
|
||||||
|
USER_UPDATED,
|
||||||
|
} from '../../events/index.js';
|
||||||
|
import { EventSearchByType } from './event-search-by-type-read-model.js';
|
||||||
|
|
||||||
|
let db: ITestDb;
|
||||||
|
beforeAll(async () => {
|
||||||
|
getLogger.setMuteError(true);
|
||||||
|
db = await dbInit('event-search-by-type', getLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
getLogger.setMuteError(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Can read search events by type', async () => {
|
||||||
|
const store = new EventStore(db.rawDatabase, getLogger);
|
||||||
|
const readModel = new EventSearchByType(db.rawDatabase, getLogger);
|
||||||
|
|
||||||
|
await store.store({
|
||||||
|
type: USER_CREATED,
|
||||||
|
createdBy: 'test',
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
createdByUserId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedEvents = await readModel.search({
|
||||||
|
types: [USER_UPDATED],
|
||||||
|
offset: 0,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
expect(updatedEvents).toBeTruthy();
|
||||||
|
expect(updatedEvents.length).toBe(0);
|
||||||
|
|
||||||
|
const createdEvents = await readModel.search({
|
||||||
|
types: [USER_CREATED],
|
||||||
|
offset: 0,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
expect(createdEvents).toBeTruthy();
|
||||||
|
expect(createdEvents.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Events by type are sorted and can be paginated', async () => {
|
||||||
|
const store = new EventStore(db.rawDatabase, getLogger);
|
||||||
|
const readModel = new EventSearchByType(db.rawDatabase, getLogger);
|
||||||
|
|
||||||
|
await store.store({
|
||||||
|
type: USER_DELETED,
|
||||||
|
data: { id: 1 },
|
||||||
|
createdBy: 'test',
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
createdByUserId: 1,
|
||||||
|
});
|
||||||
|
await store.store({
|
||||||
|
type: USER_UPDATED,
|
||||||
|
data: { id: 2 },
|
||||||
|
createdBy: 'test',
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
createdByUserId: 1,
|
||||||
|
});
|
||||||
|
await store.store({
|
||||||
|
type: USER_DELETED,
|
||||||
|
data: { id: 3 },
|
||||||
|
createdBy: 'test',
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
createdByUserId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allEvents = await readModel.search({
|
||||||
|
types: [USER_UPDATED, USER_DELETED],
|
||||||
|
offset: 0,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
expect(allEvents).toBeTruthy();
|
||||||
|
expect(allEvents.length).toBe(3);
|
||||||
|
|
||||||
|
const firstPage = await readModel.search({
|
||||||
|
types: [USER_UPDATED, USER_DELETED],
|
||||||
|
offset: 0,
|
||||||
|
limit: 2,
|
||||||
|
});
|
||||||
|
expect(firstPage).toBeTruthy();
|
||||||
|
expect(firstPage.length).toBe(2);
|
||||||
|
|
||||||
|
const secondPage = await readModel.search({
|
||||||
|
types: [USER_UPDATED, USER_DELETED],
|
||||||
|
offset: 2,
|
||||||
|
limit: 2,
|
||||||
|
});
|
||||||
|
expect(secondPage).toBeTruthy();
|
||||||
|
expect(secondPage.length).toBe(1);
|
||||||
|
expect(secondPage[0].type).toBe(USER_DELETED);
|
||||||
|
expect(secondPage[0].data.id).toBe(3);
|
||||||
|
|
||||||
|
const nonExistingPage = await readModel.search({
|
||||||
|
types: [USER_UPDATED, USER_DELETED],
|
||||||
|
offset: 4,
|
||||||
|
limit: 2,
|
||||||
|
});
|
||||||
|
expect(nonExistingPage).toBeTruthy();
|
||||||
|
expect(nonExistingPage.length).toBe(0);
|
||||||
|
});
|
72
src/lib/features/events/event-search-by-type-read-model.ts
Normal file
72
src/lib/features/events/event-search-by-type-read-model.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import type { IEvent, IEventType } from '../../events/index.js';
|
||||||
|
import type { Logger, LogProvider } from '../../logger.js';
|
||||||
|
import type { Db } from '../../db/db.js';
|
||||||
|
import type { IEventTable } from './event-store.js';
|
||||||
|
|
||||||
|
const EVENT_COLUMNS = [
|
||||||
|
'id',
|
||||||
|
'type',
|
||||||
|
'created_by',
|
||||||
|
'created_at',
|
||||||
|
'created_by_user_id',
|
||||||
|
'data',
|
||||||
|
'pre_data',
|
||||||
|
'tags',
|
||||||
|
'feature_name',
|
||||||
|
'project',
|
||||||
|
'environment',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const TABLE = 'events';
|
||||||
|
|
||||||
|
export interface EventSearchByTypeQueryParams {
|
||||||
|
types: string[];
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
order?: 'asc' | 'desc'; // asc by default
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventSearchByTypeReadModel {
|
||||||
|
search(params: EventSearchByTypeQueryParams): Promise<IEvent[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventSearchByType implements EventSearchByTypeReadModel {
|
||||||
|
private db: Db;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
// a new DB has to be injected per transaction
|
||||||
|
constructor(db: Db, getLogger: LogProvider) {
|
||||||
|
this.db = db;
|
||||||
|
this.logger = getLogger('event-by-type');
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(params: EventSearchByTypeQueryParams): Promise<IEvent[]> {
|
||||||
|
const query = this.db
|
||||||
|
.select(EVENT_COLUMNS)
|
||||||
|
.from(TABLE)
|
||||||
|
.whereIn('type', params.types)
|
||||||
|
.orderBy('id', params.order || 'asc')
|
||||||
|
.offset(params.offset)
|
||||||
|
.limit(params.limit);
|
||||||
|
|
||||||
|
const rows = await query;
|
||||||
|
return rows.map(this.rowToEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
rowToEvent(row: IEventTable): IEvent {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
type: row.type as IEventType,
|
||||||
|
createdBy: row.created_by,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
createdByUserId: row.created_by_user_id,
|
||||||
|
data: row.data,
|
||||||
|
preData: row.pre_data,
|
||||||
|
tags: row.tags || [],
|
||||||
|
featureName: row.feature_name,
|
||||||
|
project: row.project,
|
||||||
|
environment: row.environment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,6 @@ export interface IEventSearchParams {
|
|||||||
to?: string;
|
to?: string;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
types?: string[];
|
|
||||||
environment?: string;
|
environment?: string;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
Loading…
Reference in New Issue
Block a user