1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: new event search (#7708)

This introduces the new event search API, with paging.
This commit is contained in:
Jaanus Sellin 2024-08-02 10:56:42 +03:00 committed by GitHub
parent 5cd657065f
commit bcb7a803d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 494 additions and 106 deletions

View File

@ -3,11 +3,14 @@ import type { IFeatureTagStore, IUnleashStores } from '../../types/stores';
import type { Logger } from '../../logger';
import type { IEventStore } from '../../types/stores/event-store';
import type { IBaseEvent, IEventList } from '../../types/events';
import type { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
import type { DeprecatedSearchEventsSchema } from '../../openapi/spec/deprecated-search-events-schema';
import type EventEmitter from 'events';
import type { IApiUser, ITag, IUser } from '../../types';
import { ApiTokenType } from '../../types/models/api-token';
import { EVENTS_CREATED_BY_PROCESSED } from '../../metric-events';
import type { EventSearchQueryParameters } from '../../openapi/spec/event-search-query-parameters';
import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import { parseSearchOperatorValue } from '../feature-search/search-utils';
export default class EventService {
private logger: Logger;
@ -40,9 +43,38 @@ export default class EventService {
};
}
async searchEvents(search: SearchEventsSchema): Promise<IEventList> {
const totalEvents = await this.eventStore.filteredCount(search);
const events = await this.eventStore.searchEvents(search);
async deprecatedSearchEvents(
search: DeprecatedSearchEventsSchema,
): Promise<IEventList> {
const totalEvents =
await this.eventStore.deprecatedFilteredCount(search);
const events = await this.eventStore.deprecatedSearchEvents(search);
return {
events,
totalEvents,
};
}
async searchEvents(
search: EventSearchQueryParameters,
): Promise<IEventList> {
const queryParams = this.convertToDbParams(search);
const totalEvents = await this.eventStore.searchEventsCount(
{
limit: search.limit,
offset: search.offset,
query: search.query,
},
queryParams,
);
const events = await this.eventStore.searchEvents(
{
limit: search.limit,
offset: search.offset,
query: search.query,
},
queryParams,
);
return {
events,
totalEvents,
@ -110,4 +142,46 @@ export default class EventService {
});
}
}
convertToDbParams = (params: EventSearchQueryParameters): IQueryParam[] => {
const queryParams: IQueryParam[] = [];
if (params.createdAtFrom) {
queryParams.push({
field: 'created_at',
operator: 'IS_ON_OR_AFTER',
values: [params.createdAtFrom],
});
}
if (params.createdAtTo) {
queryParams.push({
field: 'created_at',
operator: 'IS_BEFORE',
values: [params.createdAtTo],
});
}
if (params.createdBy) {
const parsed = parseSearchOperatorValue(
'created_by',
params.createdBy,
);
if (parsed) queryParams.push(parsed);
}
if (params.type) {
const parsed = parseSearchOperatorValue('type', params.type);
if (parsed) queryParams.push(parsed);
}
['feature', 'project'].forEach((field) => {
if (params[field]) {
const parsed = parseSearchOperatorValue(field, params[field]);
if (parsed) queryParams.push(parsed);
}
});
return queryParams;
};
}

View File

@ -27,7 +27,9 @@ test('Trying to get events by name if db fails should yield empty list', async (
client: 'pg',
});
const store = new EventStore(db, getLogger);
const events = await store.searchEvents({ type: 'application-created' });
const events = await store.deprecatedSearchEvents({
type: 'application-created',
});
expect(events).toBeTruthy();
expect(events.length).toBe(0);
await db.destroy();

View File

@ -9,12 +9,14 @@ import {
import type { Logger, LogProvider } from '../../logger';
import type { IEventStore } from '../../types/stores/event-store';
import type { ITag } from '../../types/model';
import type { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
import { sharedEventEmitter } from '../../util/anyEventEmitter';
import type { Db } from '../../db/db';
import type { Knex } from 'knex';
import type EventEmitter from 'events';
import { ADMIN_TOKEN_USER, SYSTEM_USER_ID } from '../../types';
import type { DeprecatedSearchEventsSchema } from '../../openapi';
import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import { applyGenericQueryParams } from '../feature-search/search-utils';
const EVENT_COLUMNS = [
'id',
@ -85,6 +87,12 @@ export interface IEventTable {
tags: ITag[];
}
export interface IEventSearchParams {
query: string | undefined;
offset: string | undefined;
limit: string | undefined;
}
const TABLE = 'events';
class EventStore implements IEventStore {
@ -125,7 +133,9 @@ class EventStore implements IEventStore {
}
}
async filteredCount(eventSearch: SearchEventsSchema): Promise<number> {
async deprecatedFilteredCount(
eventSearch: DeprecatedSearchEventsSchema,
): Promise<number> {
let query = this.db(TABLE);
if (eventSearch.type) {
query = query.andWhere({ type: eventSearch.type });
@ -147,6 +157,22 @@ class EventStore implements IEventStore {
}
}
async searchEventsCount(
params: IEventSearchParams,
queryParams: IQueryParam[],
): Promise<number> {
const query = this.buildSearchQuery(params, queryParams);
const count = await query.count().first();
if (!count) {
return 0;
}
if (typeof count.count === 'string') {
return Number.parseInt(count.count, 10);
} else {
return count.count;
}
}
async batchStore(events: IBaseEvent[]): Promise<void> {
try {
await this.db(TABLE).insert(events.map(this.eventToDbRow));
@ -320,7 +346,46 @@ class EventStore implements IEventStore {
}
}
async searchEvents(search: SearchEventsSchema = {}): Promise<IEvent[]> {
async searchEvents(
params: IEventSearchParams,
queryParams: IQueryParam[],
): Promise<IEvent[]> {
const query = this.buildSearchQuery(params, queryParams)
.select(EVENT_COLUMNS)
.orderBy('created_at', 'desc')
.limit(Number(params.limit) ?? 100)
.offset(Number(params.offset) ?? 0);
try {
return (await query).map(this.rowToEvent);
} catch (err) {
return [];
}
}
private buildSearchQuery(
params: IEventSearchParams,
queryParams: IQueryParam[],
) {
let query = this.db.from<IEventTable>(TABLE);
applyGenericQueryParams(query, queryParams);
if (params.query) {
query = query.where((where) =>
where
.orWhereRaw('data::text ILIKE ?', `%${params.query}%`)
.orWhereRaw('tags::text ILIKE ?', `%${params.query}%`)
.orWhereRaw('pre_data::text ILIKE ?', `%${params.query}%`),
);
}
return query;
}
async deprecatedSearchEvents(
search: DeprecatedSearchEventsSchema = {},
): Promise<IEvent[]> {
let query = this.db
.select(EVENT_COLUMNS)
.from<IEventTable>(TABLE)

View File

@ -104,19 +104,26 @@ export default class FeatureSearchController extends Controller {
state,
status,
favoritesFirst,
sortBy,
} = req.query;
const userId = req.user.id;
const {
normalizedQuery,
normalizedSortBy,
normalizedSortOrder,
normalizedOffset,
normalizedLimit,
} = normalizeQueryParams(req.query, {
limitDefault: 50,
maxLimit: 100,
sortByDefault: 'createdAt',
});
} = normalizeQueryParams(
{
query,
offset: req.query.offset,
limit: req.query.limit,
sortOrder: req.query.sortOrder,
},
{
limitDefault: 50,
maxLimit: 100,
},
);
const normalizedStatus = status
?.map((tag) => tag.split(':'))
@ -136,10 +143,10 @@ export default class FeatureSearchController extends Controller {
state,
createdAt,
createdBy,
sortBy,
status: normalizedStatus,
offset: normalizedOffset,
limit: normalizedLimit,
sortBy: normalizedSortBy,
sortOrder: normalizedSortOrder,
favoritesFirst: normalizedFavoritesFirst,
});

View File

@ -6,9 +6,9 @@ import type {
} from '../../types';
import type {
IFeatureSearchParams,
IQueryOperator,
IQueryParam,
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
import { parseSearchOperatorValue } from './search-utils';
export class FeatureSearchService {
private featureSearchStore: IFeatureSearchStore;
@ -28,6 +28,7 @@ export class FeatureSearchService {
{
...params,
limit: params.limit,
sortBy: params.sortBy || 'createdAt',
},
queryParams,
);
@ -38,27 +39,11 @@ export class FeatureSearchService {
};
}
parseOperatorValue = (field: string, value: string): IQueryParam | null => {
const pattern =
/^(IS|IS_NOT|IS_ANY_OF|IS_NONE_OF|INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL|IS_BEFORE|IS_ON_OR_AFTER):(.+)$/;
const match = value.match(pattern);
if (match) {
return {
field,
operator: match[1] as IQueryOperator,
values: match[2].split(','),
};
}
return null;
};
convertToQueryParams = (params: IFeatureSearchParams): IQueryParam[] => {
const queryParams: IQueryParam[] = [];
if (params.state) {
const parsedState = this.parseOperatorValue('stale', params.state);
const parsedState = parseSearchOperatorValue('stale', params.state);
if (parsedState) {
parsedState.values = parsedState.values.map((value) =>
value === 'active' ? 'false' : 'true',
@ -68,7 +53,7 @@ export class FeatureSearchService {
}
if (params.createdAt) {
const parsed = this.parseOperatorValue(
const parsed = parseSearchOperatorValue(
'features.created_at',
params.createdAt,
);
@ -76,7 +61,7 @@ export class FeatureSearchService {
}
if (params.createdBy) {
const parsed = this.parseOperatorValue(
const parsed = parseSearchOperatorValue(
'users.id',
params.createdBy,
);
@ -84,7 +69,7 @@ export class FeatureSearchService {
}
if (params.type) {
const parsed = this.parseOperatorValue(
const parsed = parseSearchOperatorValue(
'features.type',
params.type,
);
@ -93,7 +78,7 @@ export class FeatureSearchService {
['tag', 'segment', 'project'].forEach((field) => {
if (params[field]) {
const parsed = this.parseOperatorValue(field, params[field]);
const parsed = parseSearchOperatorValue(field, params[field]);
if (parsed) queryParams.push(parsed);
}
});

View File

@ -475,7 +475,7 @@ test('should sort features', async () => {
const { body: defaultCreatedAt } = await sortFeatures({
sortBy: '',
sortOrder: '',
sortOrder: 'asc',
});
expect(defaultCreatedAt).toMatchObject({

View File

@ -1,13 +1,29 @@
import type { Knex } from 'knex';
import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import type {
IQueryOperator,
IQueryParam,
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
export interface NormalizeParamsDefaults {
limitDefault: number;
maxLimit?: number; // Optional because you might not always want to enforce a max limit
sortByDefault: string;
typeDefault?: string; // Optional field for type, not required for every call
}
export type SearchParams = {
query?: string;
offset?: string | number;
limit?: string | number;
sortOrder?: 'asc' | 'desc';
};
export type NormalizedSearchParams = {
normalizedQuery?: string[];
normalizedLimit: number;
normalizedOffset: number;
normalizedSortOrder: 'asc' | 'desc';
};
export const applySearchFilters = (
qb: Knex.QueryBuilder,
searchParams: string[] | undefined,
@ -54,16 +70,10 @@ export const applyGenericQueryParams = (
};
export const normalizeQueryParams = (
params,
params: SearchParams,
defaults: NormalizeParamsDefaults,
) => {
const {
query,
offset,
limit = defaults.limitDefault,
sortOrder,
sortBy = defaults.sortByDefault,
} = params;
): NormalizedSearchParams => {
const { query, offset, limit = defaults.limitDefault, sortOrder } = params;
const normalizedQuery = query
?.split(',')
@ -78,7 +88,6 @@ export const normalizeQueryParams = (
const normalizedOffset = Number(offset) > 0 ? Number(offset) : 0;
const normalizedSortBy = sortBy;
const normalizedSortOrder =
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
@ -86,7 +95,25 @@ export const normalizeQueryParams = (
normalizedQuery,
normalizedLimit,
normalizedOffset,
normalizedSortBy,
normalizedSortOrder,
};
};
export const parseSearchOperatorValue = (
field: string,
value: string,
): IQueryParam | null => {
const pattern =
/^(IS|IS_NOT|IS_ANY_OF|IS_NONE_OF|INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL|IS_BEFORE|IS_ON_OR_AFTER):(.+)$/;
const match = value.match(pattern);
if (match) {
return {
field,
operator: match[1] as IQueryOperator,
values: match[2].split(','),
};
}
return null;
};

View File

@ -34,7 +34,7 @@ export interface IFeatureSearchParams {
offset: number;
favoritesFirst?: boolean;
limit: number;
sortBy: string;
sortBy?: string;
sortOrder: 'asc' | 'desc';
}

View File

@ -269,8 +269,12 @@ export class InstanceStatsService {
this.hasSAML(),
this.hasOIDC(),
this.appCount ? this.appCount : this.refreshAppCountSnapshot(),
this.eventStore.filteredCount({ type: FEATURES_EXPORTED }),
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
this.eventStore.deprecatedFilteredCount({
type: FEATURES_EXPORTED,
}),
this.eventStore.deprecatedFilteredCount({
type: FEATURES_IMPORTED,
}),
this.getProductionChanges(),
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
]);

View File

@ -177,8 +177,9 @@ export default class ClientInstanceService {
query: IClientApplicationsSearchParams,
userId: number,
): Promise<IClientApplications> {
const applications =
await this.clientApplicationsStore.getApplications(query);
const applications = await this.clientApplicationsStore.getApplications(
{ ...query, sortBy: query.sortBy || 'appName' },
);
const accessibleProjects =
await this.privateProjectChecker.getUserAccessibleProjects(userId);
if (accessibleProjects.mode === 'all') {

View File

@ -325,14 +325,12 @@ export default class ProjectController extends Controller {
const {
normalizedQuery,
normalizedSortBy,
normalizedSortOrder,
normalizedOffset,
normalizedLimit,
} = normalizeQueryParams(req.query, {
limitDefault: 50,
maxLimit: 100,
sortByDefault: 'appName',
});
const applications = await this.projectService.getApplications({
@ -340,7 +338,7 @@ export default class ProjectController extends Controller {
project: projectId,
offset: normalizedOffset,
limit: normalizedLimit,
sortBy: normalizedSortBy,
sortBy: req.query.sortBy,
sortOrder: normalizedSortOrder,
});

View File

@ -1117,8 +1117,10 @@ export default class ProjectService {
async getApplications(
searchParams: IProjectApplicationsSearchParams,
): Promise<IProjectApplications> {
const applications =
await this.projectStore.getApplicationsByProject(searchParams);
const applications = await this.projectStore.getApplicationsByProject({
...searchParams,
sortBy: searchParams.sortBy || 'appName',
});
return applications;
}

View File

@ -1,8 +1,8 @@
import type { FromSchema } from 'json-schema-to-ts';
import { IEventTypes } from '../../types';
export const searchEventsSchema = {
$id: '#/components/schemas/searchEventsSchema',
export const deprecatedSearchEventsSchema = {
$id: '#/components/schemas/deprecatedSearchEventsSchema',
type: 'object',
description: `
Search for events by type, project, feature, free-text query,
@ -50,4 +50,6 @@ export const searchEventsSchema = {
components: {},
} as const;
export type SearchEventsSchema = FromSchema<typeof searchEventsSchema>;
export type DeprecatedSearchEventsSchema = FromSchema<
typeof deprecatedSearchEventsSchema
>;

View File

@ -0,0 +1,105 @@
import type { FromQueryParams } from '../util/from-query-params';
export const eventSearchQueryParameters = [
{
name: 'query',
schema: {
type: 'string',
example: 'admin@example.com',
},
description:
'Find events by a free-text search query. The query will be matched against the event type and the event data payload (if any).',
in: 'query',
},
{
name: 'feature',
schema: {
type: 'string',
example: 'IS:myfeature',
pattern: '^(IS|IS_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$',
},
description:
'Filter by feature name using supported operators: IS, IS_ANY_OF',
in: 'query',
},
{
name: 'project',
schema: {
type: 'string',
example: 'IS:default',
pattern: '^(IS|IS_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$',
},
description:
'Filter by projects ID using supported operators: IS, IS_ANY_OF.',
in: 'query',
},
{
name: 'type',
schema: {
type: 'string',
example: 'IS:change-added',
pattern: '^(IS|IS_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$',
},
description:
'Filter by event type using supported operators: IS, IS_ANY_OF.',
in: 'query',
},
{
name: 'createdBy',
schema: {
type: 'string',
example: 'IS:2',
pattern: '^(IS|IS_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$',
},
description:
'The ID of event creator to filter by. The creators can be specified with an operator. The supported operators are IS, IS_ANY_OF.',
in: 'query',
},
{
name: 'createdAtFrom',
schema: {
type: 'string',
example: 'IS:2024-01-01',
pattern: '^(IS):\\d{4}-\\d{2}-\\d{2}$',
},
description:
'The starting date of the creation date range in IS:yyyy-MM-dd format',
in: 'query',
},
{
name: 'createdAtTo',
schema: {
type: 'string',
example: 'IS:2024-01-31',
pattern: '^(IS):\\d{4}-\\d{2}-\\d{2}$',
},
description:
'The ending date of the creation date range in IS:yyyy-MM-dd format',
in: 'query',
},
{
name: 'offset',
schema: {
type: 'string',
example: '50',
},
description:
'The number of features to skip when returning a page. By default it is set to 0.',
in: 'query',
},
{
name: 'limit',
schema: {
type: 'string',
example: '50',
},
description:
'The number of feature environments to return in a page. By default it is set to 50.',
in: 'query',
},
] as const;
export type EventSearchQueryParameters = Partial<
FromQueryParams<typeof eventSearchQueryParameters>
>;

View File

@ -0,0 +1,34 @@
import type { FromSchema } from 'json-schema-to-ts';
import { eventSchema } from './event-schema';
import { tagSchema } from './tag-schema';
export const eventSearchResponseSchema = {
$id: '#/components/schemas/eventsSearchResponseSchema',
type: 'object',
additionalProperties: false,
required: ['events', 'total'],
description: 'A list of events that have been registered by the system',
properties: {
events: {
description: 'The list of events',
type: 'array',
items: { $ref: eventSchema.$id },
},
total: {
type: 'integer',
description: 'The total count of events',
minimum: 0,
example: 842,
},
},
components: {
schemas: {
eventSchema,
tagSchema,
},
},
} as const;
export type EventSearchResponseSchema = FromSchema<
typeof eventSearchResponseSchema
>;

View File

@ -129,6 +129,7 @@ export const featureSearchQueryParameters = [
name: 'sortOrder',
schema: {
type: 'string',
enum: ['asc', 'desc'] as any,
example: 'desc',
},
description:

View File

@ -61,6 +61,7 @@ export * from './date-schema';
export * from './dependencies-exist-schema';
export * from './dependent-feature-schema';
export * from './deprecated-project-overview-schema';
export * from './deprecated-search-events-schema';
export * from './dora-features-schema';
export * from './edge-token-schema';
export * from './email-schema';
@ -69,6 +70,7 @@ export * from './environment-schema';
export * from './environments-project-schema';
export * from './environments-schema';
export * from './event-schema';
export * from './event-search-response-schema';
export * from './events-schema';
export * from './export-query-schema';
export * from './export-result-schema';
@ -161,7 +163,6 @@ export * from './role-schema';
export * from './roles-schema';
export * from './sdk-context-schema';
export * from './sdk-flat-context-schema';
export * from './search-events-schema';
export * from './search-features-schema';
export * from './segment-schema';
export * from './segment-strategies-schema';

View File

@ -19,8 +19,18 @@ import {
} from '../../../lib/openapi/spec/feature-events-schema';
import { getStandardResponses } from '../../../lib/openapi/util/standard-responses';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
import type { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
import type { DeprecatedSearchEventsSchema } from '../../openapi/spec/deprecated-search-events-schema';
import type { IFlagResolver } from '../../types/experimental';
import {
type EventSearchQueryParameters,
eventSearchQueryParameters,
} from '../../openapi/spec/event-search-query-parameters';
import type { IAuthRequest } from '../unleash-types';
import {
type EventSearchResponseSchema,
eventSearchResponseSchema,
} from '../../openapi';
import { normalizeQueryParams } from '../../features/feature-search/search-utils';
const ANON_KEYS = ['email', 'username', 'createdBy'];
const version = 1 as const;
@ -98,6 +108,27 @@ export default class EventController extends Controller {
this.route({
method: 'post',
path: '/search',
handler: this.deprecatedSearchEvents,
permission: NONE,
middleware: [
openApiService.validPath({
operationId: 'deprecatedSearchEvents',
tags: ['Events'],
deprecated: true,
summary: 'Search for events (deprecated)',
description:
'Allows searching for events matching the search criteria in the request body',
requestBody: createRequestSchema(
'deprecatedSearchEventsSchema',
),
responses: { 200: createResponseSchema('eventsSchema') },
}),
],
});
this.route({
method: 'get',
path: '/search',
handler: this.searchEvents,
permission: NONE,
middleware: [
@ -106,9 +137,11 @@ export default class EventController extends Controller {
tags: ['Events'],
summary: 'Search for events',
description:
'Allows searching for events matching the search criteria in the request body',
requestBody: createRequestSchema('searchEventsSchema'),
responses: { 200: createResponseSchema('eventsSchema') },
'Allows searching for events matching the search criteria in the request body. This operation is deprecated. You should perform a GET request to the same endpoint with your query encoded as query parameters instead.',
parameters: [...eventSearchQueryParameters],
responses: {
200: createResponseSchema('eventSearchResponseSchema'),
},
}),
],
});
@ -128,7 +161,9 @@ export default class EventController extends Controller {
const { project } = req.query;
let eventList: IEventList;
if (project) {
eventList = await this.eventService.searchEvents({ project });
eventList = await this.eventService.deprecatedSearchEvents({
project,
});
} else {
eventList = await this.eventService.getEvents();
}
@ -152,7 +187,9 @@ export default class EventController extends Controller {
res: Response<FeatureEventsSchema>,
): Promise<void> {
const feature = req.params.featureName;
const eventList = await this.eventService.searchEvents({ feature });
const eventList = await this.eventService.deprecatedSearchEvents({
feature,
});
const response = {
version,
@ -169,11 +206,13 @@ export default class EventController extends Controller {
);
}
async searchEvents(
req: Request<unknown, unknown, SearchEventsSchema>,
async deprecatedSearchEvents(
req: Request<unknown, unknown, DeprecatedSearchEventsSchema>,
res: Response<EventsSchema>,
): Promise<void> {
const eventList = await this.eventService.searchEvents(req.body);
const eventList = await this.eventService.deprecatedSearchEvents(
req.body,
);
const response = {
version,
@ -188,4 +227,33 @@ export default class EventController extends Controller {
response,
);
}
async searchEvents(
req: IAuthRequest<any, any, any, EventSearchQueryParameters>,
res: Response<EventSearchResponseSchema>,
): Promise<void> {
const { normalizedLimit, normalizedOffset } = normalizeQueryParams(
req.query,
{
limitDefault: 50,
maxLimit: 1000,
},
);
const { events, totalEvents } = await this.eventService.searchEvents({
...req.body,
offset: normalizedOffset,
limit: normalizedLimit,
});
this.openApiService.respondWithValidation(
200,
res,
eventSearchResponseSchema.$id,
serializeDates({
events: serializeDates(this.maybeAnonymiseEvents(events)),
total: totalEvents,
}),
);
}
}

View File

@ -246,14 +246,12 @@ class MetricsController extends Controller {
const { user } = req;
const {
normalizedQuery,
normalizedSortBy,
normalizedSortOrder,
normalizedOffset,
normalizedLimit,
} = normalizeQueryParams(req.query, {
limitDefault: 1000,
maxLimit: 1000,
sortByDefault: 'appName',
});
const applications = await this.clientInstanceService.getApplications(
@ -261,7 +259,7 @@ class MetricsController extends Controller {
searchParams: normalizedQuery,
offset: normalizedOffset,
limit: normalizedLimit,
sortBy: normalizedSortBy,
sortBy: req.query.sortBy,
sortOrder: normalizedSortOrder,
},
extractUserIdFromUser(user),

View File

@ -280,8 +280,12 @@ export default class VersionService {
this.strategyStore.count(),
this.hasSAML(),
this.hasOIDC(),
this.eventStore.filteredCount({ type: FEATURES_EXPORTED }),
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
this.eventStore.deprecatedFilteredCount({
type: FEATURES_EXPORTED,
}),
this.eventStore.deprecatedFilteredCount({
type: FEATURES_IMPORTED,
}),
this.userStats(),
this.productionChanges(),
this.postgresVersion(),

View File

@ -1,8 +1,12 @@
import type { IBaseEvent, IEvent } from '../events';
import type { Store } from './store';
import type { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
import type { DeprecatedSearchEventsSchema } from '../../openapi';
import type EventEmitter from 'events';
import type { IQueryOperations } from '../../features/events/event-store';
import type {
IEventSearchParams,
IQueryOperations,
} from '../../features/events/event-store';
import type { IQueryParam } from '../../features/feature-toggle/types/feature-toggle-strategies-store-type';
export interface IEventStore
extends Store<IEvent, number>,
@ -12,8 +16,20 @@ export interface IEventStore
batchStore(events: IBaseEvent[]): Promise<void>;
getEvents(): Promise<IEvent[]>;
count(): Promise<number>;
filteredCount(search: SearchEventsSchema): Promise<number>;
searchEvents(search: SearchEventsSchema): Promise<IEvent[]>;
deprecatedFilteredCount(
search: DeprecatedSearchEventsSchema,
): Promise<number>;
searchEventsCount(
params: IEventSearchParams,
queryParams: IQueryParam[],
): Promise<number>;
deprecatedSearchEvents(
search: DeprecatedSearchEventsSchema,
): Promise<IEvent[]>;
searchEvents(
params: IEventSearchParams,
queryParams: IQueryParam[],
): Promise<IEvent[]>;
getMaxRevisionId(currentMax?: number): Promise<number>;
query(operations: IQueryOperations[]): Promise<IEvent[]>;
queryCount(operations: IQueryOperations[]): Promise<number>;

View File

@ -36,7 +36,7 @@ test('Can create new setting', async () => {
expect(actual).toStrictEqual(someData);
const { eventStore } = stores;
const createdEvents = await eventStore.searchEvents({
const createdEvents = await eventStore.deprecatedSearchEvents({
type: SETTING_CREATED,
});
expect(createdEvents).toHaveLength(1);
@ -51,7 +51,7 @@ test('Can delete setting', async () => {
const actual = await service.get('some-setting');
expect(actual).toBeUndefined();
const { eventStore } = stores;
const createdEvents = await eventStore.searchEvents({
const createdEvents = await eventStore.deprecatedSearchEvents({
type: SETTING_DELETED,
});
expect(createdEvents).toHaveLength(1);
@ -66,7 +66,7 @@ test('Sentitive SSO settings are redacted in event log', async () => {
const actual = await service.get(property);
const { eventStore } = stores;
const updatedEvents = await eventStore.searchEvents({
const updatedEvents = await eventStore.deprecatedSearchEvents({
type: SETTING_UPDATED,
});
expect(updatedEvents[0].preData).toEqual({ hideEventDetails: true });
@ -83,7 +83,7 @@ test('Can update setting', async () => {
TEST_AUDIT_USER,
false,
);
const updatedEvents = await eventStore.searchEvents({
const updatedEvents = await eventStore.deprecatedSearchEvents({
type: SETTING_UPDATED,
});
expect(updatedEvents).toHaveLength(1);

View File

@ -234,11 +234,11 @@ test('Should get all events of type', async () => {
return eventStore.store(event);
}),
);
const featureCreatedEvents = await eventStore.searchEvents({
const featureCreatedEvents = await eventStore.deprecatedSearchEvents({
type: FEATURE_CREATED,
});
expect(featureCreatedEvents).toHaveLength(3);
const featureDeletedEvents = await eventStore.searchEvents({
const featureDeletedEvents = await eventStore.deprecatedSearchEvents({
type: FEATURE_DELETED,
});
expect(featureDeletedEvents).toHaveLength(3);

View File

@ -2,7 +2,7 @@ import type { IEventStore } from '../../lib/types/stores/event-store';
import type { IBaseEvent, IEvent } from '../../lib/types/events';
import { sharedEventEmitter } from '../../lib/util/anyEventEmitter';
import type { IQueryOperations } from '../../lib/features/events/event-store';
import type { SearchEventsSchema } from '../../lib/openapi';
import type { DeprecatedSearchEventsSchema } from '../../lib/openapi';
import type EventEmitter from 'events';
class FakeEventStore implements IEventStore {
@ -61,7 +61,13 @@ class FakeEventStore implements IEventStore {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
filteredCount(search: SearchEventsSchema): Promise<number> {
searchEventsCount(): Promise<number> {
return Promise.resolve(0);
}
deprecatedFilteredCount(
search: DeprecatedSearchEventsSchema,
): Promise<number> {
return Promise.resolve(0);
}
@ -79,23 +85,11 @@ class FakeEventStore implements IEventStore {
return this.events;
}
async searchEvents(): Promise<IEvent[]> {
async deprecatedSearchEvents(): Promise<IEvent[]> {
throw new Error('Method not implemented.');
}
async getForFeatures(
features: string[],
environments: string[],
query: { type: string; projectId: string },
): Promise<IEvent[]> {
return this.events.filter((event) => {
return (
event.type === query.type &&
event.project === query.projectId &&
features.includes(event.data.featureName) &&
environments.includes(event.data.environment)
);
});
async searchEvents(): Promise<IEvent[]> {
throw new Error('Method not implemented.');
}
async query(operations: IQueryOperations[]): Promise<IEvent[]> {