mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
feat: new event search (#7708)
This introduces the new event search API, with paging.
This commit is contained in:
parent
5cd657065f
commit
bcb7a803d0
@ -3,11 +3,14 @@ import type { IFeatureTagStore, IUnleashStores } from '../../types/stores';
|
|||||||
import type { Logger } from '../../logger';
|
import type { Logger } from '../../logger';
|
||||||
import type { IEventStore } from '../../types/stores/event-store';
|
import type { IEventStore } from '../../types/stores/event-store';
|
||||||
import type { IBaseEvent, IEventList } from '../../types/events';
|
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 EventEmitter from 'events';
|
||||||
import type { IApiUser, ITag, IUser } from '../../types';
|
import type { IApiUser, ITag, IUser } from '../../types';
|
||||||
import { ApiTokenType } from '../../types/models/api-token';
|
import { ApiTokenType } from '../../types/models/api-token';
|
||||||
import { EVENTS_CREATED_BY_PROCESSED } from '../../metric-events';
|
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 {
|
export default class EventService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -40,9 +43,38 @@ export default class EventService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchEvents(search: SearchEventsSchema): Promise<IEventList> {
|
async deprecatedSearchEvents(
|
||||||
const totalEvents = await this.eventStore.filteredCount(search);
|
search: DeprecatedSearchEventsSchema,
|
||||||
const events = await this.eventStore.searchEvents(search);
|
): 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 {
|
return {
|
||||||
events,
|
events,
|
||||||
totalEvents,
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,9 @@ test('Trying to get events by name if db fails should yield empty list', async (
|
|||||||
client: 'pg',
|
client: 'pg',
|
||||||
});
|
});
|
||||||
const store = new EventStore(db, getLogger);
|
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).toBeTruthy();
|
||||||
expect(events.length).toBe(0);
|
expect(events.length).toBe(0);
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
@ -9,12 +9,14 @@ import {
|
|||||||
import type { Logger, LogProvider } from '../../logger';
|
import type { Logger, LogProvider } from '../../logger';
|
||||||
import type { IEventStore } from '../../types/stores/event-store';
|
import type { IEventStore } from '../../types/stores/event-store';
|
||||||
import type { ITag } from '../../types/model';
|
import type { ITag } from '../../types/model';
|
||||||
import type { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
|
|
||||||
import { sharedEventEmitter } from '../../util/anyEventEmitter';
|
import { sharedEventEmitter } from '../../util/anyEventEmitter';
|
||||||
import type { Db } from '../../db/db';
|
import type { Db } from '../../db/db';
|
||||||
import type { Knex } from 'knex';
|
import type { Knex } from 'knex';
|
||||||
import type EventEmitter from 'events';
|
import type EventEmitter from 'events';
|
||||||
import { ADMIN_TOKEN_USER, SYSTEM_USER_ID } from '../../types';
|
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 = [
|
const EVENT_COLUMNS = [
|
||||||
'id',
|
'id',
|
||||||
@ -85,6 +87,12 @@ export interface IEventTable {
|
|||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IEventSearchParams {
|
||||||
|
query: string | undefined;
|
||||||
|
offset: string | undefined;
|
||||||
|
limit: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const TABLE = 'events';
|
const TABLE = 'events';
|
||||||
|
|
||||||
class EventStore implements IEventStore {
|
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);
|
let query = this.db(TABLE);
|
||||||
if (eventSearch.type) {
|
if (eventSearch.type) {
|
||||||
query = query.andWhere({ type: 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> {
|
async batchStore(events: IBaseEvent[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.db(TABLE).insert(events.map(this.eventToDbRow));
|
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
|
let query = this.db
|
||||||
.select(EVENT_COLUMNS)
|
.select(EVENT_COLUMNS)
|
||||||
.from<IEventTable>(TABLE)
|
.from<IEventTable>(TABLE)
|
||||||
|
@ -104,19 +104,26 @@ export default class FeatureSearchController extends Controller {
|
|||||||
state,
|
state,
|
||||||
status,
|
status,
|
||||||
favoritesFirst,
|
favoritesFirst,
|
||||||
|
sortBy,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const {
|
const {
|
||||||
normalizedQuery,
|
normalizedQuery,
|
||||||
normalizedSortBy,
|
|
||||||
normalizedSortOrder,
|
normalizedSortOrder,
|
||||||
normalizedOffset,
|
normalizedOffset,
|
||||||
normalizedLimit,
|
normalizedLimit,
|
||||||
} = normalizeQueryParams(req.query, {
|
} = normalizeQueryParams(
|
||||||
limitDefault: 50,
|
{
|
||||||
maxLimit: 100,
|
query,
|
||||||
sortByDefault: 'createdAt',
|
offset: req.query.offset,
|
||||||
});
|
limit: req.query.limit,
|
||||||
|
sortOrder: req.query.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
limitDefault: 50,
|
||||||
|
maxLimit: 100,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const normalizedStatus = status
|
const normalizedStatus = status
|
||||||
?.map((tag) => tag.split(':'))
|
?.map((tag) => tag.split(':'))
|
||||||
@ -136,10 +143,10 @@ export default class FeatureSearchController extends Controller {
|
|||||||
state,
|
state,
|
||||||
createdAt,
|
createdAt,
|
||||||
createdBy,
|
createdBy,
|
||||||
|
sortBy,
|
||||||
status: normalizedStatus,
|
status: normalizedStatus,
|
||||||
offset: normalizedOffset,
|
offset: normalizedOffset,
|
||||||
limit: normalizedLimit,
|
limit: normalizedLimit,
|
||||||
sortBy: normalizedSortBy,
|
|
||||||
sortOrder: normalizedSortOrder,
|
sortOrder: normalizedSortOrder,
|
||||||
favoritesFirst: normalizedFavoritesFirst,
|
favoritesFirst: normalizedFavoritesFirst,
|
||||||
});
|
});
|
||||||
|
@ -6,9 +6,9 @@ import type {
|
|||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type {
|
import type {
|
||||||
IFeatureSearchParams,
|
IFeatureSearchParams,
|
||||||
IQueryOperator,
|
|
||||||
IQueryParam,
|
IQueryParam,
|
||||||
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
|
import { parseSearchOperatorValue } from './search-utils';
|
||||||
|
|
||||||
export class FeatureSearchService {
|
export class FeatureSearchService {
|
||||||
private featureSearchStore: IFeatureSearchStore;
|
private featureSearchStore: IFeatureSearchStore;
|
||||||
@ -28,6 +28,7 @@ export class FeatureSearchService {
|
|||||||
{
|
{
|
||||||
...params,
|
...params,
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
|
sortBy: params.sortBy || 'createdAt',
|
||||||
},
|
},
|
||||||
queryParams,
|
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[] => {
|
convertToQueryParams = (params: IFeatureSearchParams): IQueryParam[] => {
|
||||||
const queryParams: IQueryParam[] = [];
|
const queryParams: IQueryParam[] = [];
|
||||||
|
|
||||||
if (params.state) {
|
if (params.state) {
|
||||||
const parsedState = this.parseOperatorValue('stale', params.state);
|
const parsedState = parseSearchOperatorValue('stale', params.state);
|
||||||
if (parsedState) {
|
if (parsedState) {
|
||||||
parsedState.values = parsedState.values.map((value) =>
|
parsedState.values = parsedState.values.map((value) =>
|
||||||
value === 'active' ? 'false' : 'true',
|
value === 'active' ? 'false' : 'true',
|
||||||
@ -68,7 +53,7 @@ export class FeatureSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.createdAt) {
|
if (params.createdAt) {
|
||||||
const parsed = this.parseOperatorValue(
|
const parsed = parseSearchOperatorValue(
|
||||||
'features.created_at',
|
'features.created_at',
|
||||||
params.createdAt,
|
params.createdAt,
|
||||||
);
|
);
|
||||||
@ -76,7 +61,7 @@ export class FeatureSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.createdBy) {
|
if (params.createdBy) {
|
||||||
const parsed = this.parseOperatorValue(
|
const parsed = parseSearchOperatorValue(
|
||||||
'users.id',
|
'users.id',
|
||||||
params.createdBy,
|
params.createdBy,
|
||||||
);
|
);
|
||||||
@ -84,7 +69,7 @@ export class FeatureSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.type) {
|
if (params.type) {
|
||||||
const parsed = this.parseOperatorValue(
|
const parsed = parseSearchOperatorValue(
|
||||||
'features.type',
|
'features.type',
|
||||||
params.type,
|
params.type,
|
||||||
);
|
);
|
||||||
@ -93,7 +78,7 @@ export class FeatureSearchService {
|
|||||||
|
|
||||||
['tag', 'segment', 'project'].forEach((field) => {
|
['tag', 'segment', 'project'].forEach((field) => {
|
||||||
if (params[field]) {
|
if (params[field]) {
|
||||||
const parsed = this.parseOperatorValue(field, params[field]);
|
const parsed = parseSearchOperatorValue(field, params[field]);
|
||||||
if (parsed) queryParams.push(parsed);
|
if (parsed) queryParams.push(parsed);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -475,7 +475,7 @@ test('should sort features', async () => {
|
|||||||
|
|
||||||
const { body: defaultCreatedAt } = await sortFeatures({
|
const { body: defaultCreatedAt } = await sortFeatures({
|
||||||
sortBy: '',
|
sortBy: '',
|
||||||
sortOrder: '',
|
sortOrder: 'asc',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(defaultCreatedAt).toMatchObject({
|
expect(defaultCreatedAt).toMatchObject({
|
||||||
|
@ -1,13 +1,29 @@
|
|||||||
import type { Knex } from 'knex';
|
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 {
|
export interface NormalizeParamsDefaults {
|
||||||
limitDefault: number;
|
limitDefault: number;
|
||||||
maxLimit?: number; // Optional because you might not always want to enforce a max limit
|
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
|
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 = (
|
export const applySearchFilters = (
|
||||||
qb: Knex.QueryBuilder,
|
qb: Knex.QueryBuilder,
|
||||||
searchParams: string[] | undefined,
|
searchParams: string[] | undefined,
|
||||||
@ -54,16 +70,10 @@ export const applyGenericQueryParams = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeQueryParams = (
|
export const normalizeQueryParams = (
|
||||||
params,
|
params: SearchParams,
|
||||||
defaults: NormalizeParamsDefaults,
|
defaults: NormalizeParamsDefaults,
|
||||||
) => {
|
): NormalizedSearchParams => {
|
||||||
const {
|
const { query, offset, limit = defaults.limitDefault, sortOrder } = params;
|
||||||
query,
|
|
||||||
offset,
|
|
||||||
limit = defaults.limitDefault,
|
|
||||||
sortOrder,
|
|
||||||
sortBy = defaults.sortByDefault,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
const normalizedQuery = query
|
const normalizedQuery = query
|
||||||
?.split(',')
|
?.split(',')
|
||||||
@ -78,7 +88,6 @@ export const normalizeQueryParams = (
|
|||||||
|
|
||||||
const normalizedOffset = Number(offset) > 0 ? Number(offset) : 0;
|
const normalizedOffset = Number(offset) > 0 ? Number(offset) : 0;
|
||||||
|
|
||||||
const normalizedSortBy = sortBy;
|
|
||||||
const normalizedSortOrder =
|
const normalizedSortOrder =
|
||||||
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
||||||
|
|
||||||
@ -86,7 +95,25 @@ export const normalizeQueryParams = (
|
|||||||
normalizedQuery,
|
normalizedQuery,
|
||||||
normalizedLimit,
|
normalizedLimit,
|
||||||
normalizedOffset,
|
normalizedOffset,
|
||||||
normalizedSortBy,
|
|
||||||
normalizedSortOrder,
|
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;
|
||||||
|
};
|
||||||
|
@ -34,7 +34,7 @@ export interface IFeatureSearchParams {
|
|||||||
offset: number;
|
offset: number;
|
||||||
favoritesFirst?: boolean;
|
favoritesFirst?: boolean;
|
||||||
limit: number;
|
limit: number;
|
||||||
sortBy: string;
|
sortBy?: string;
|
||||||
sortOrder: 'asc' | 'desc';
|
sortOrder: 'asc' | 'desc';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,8 +269,12 @@ export class InstanceStatsService {
|
|||||||
this.hasSAML(),
|
this.hasSAML(),
|
||||||
this.hasOIDC(),
|
this.hasOIDC(),
|
||||||
this.appCount ? this.appCount : this.refreshAppCountSnapshot(),
|
this.appCount ? this.appCount : this.refreshAppCountSnapshot(),
|
||||||
this.eventStore.filteredCount({ type: FEATURES_EXPORTED }),
|
this.eventStore.deprecatedFilteredCount({
|
||||||
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
|
type: FEATURES_EXPORTED,
|
||||||
|
}),
|
||||||
|
this.eventStore.deprecatedFilteredCount({
|
||||||
|
type: FEATURES_IMPORTED,
|
||||||
|
}),
|
||||||
this.getProductionChanges(),
|
this.getProductionChanges(),
|
||||||
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
|
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
|
||||||
]);
|
]);
|
||||||
|
@ -177,8 +177,9 @@ export default class ClientInstanceService {
|
|||||||
query: IClientApplicationsSearchParams,
|
query: IClientApplicationsSearchParams,
|
||||||
userId: number,
|
userId: number,
|
||||||
): Promise<IClientApplications> {
|
): Promise<IClientApplications> {
|
||||||
const applications =
|
const applications = await this.clientApplicationsStore.getApplications(
|
||||||
await this.clientApplicationsStore.getApplications(query);
|
{ ...query, sortBy: query.sortBy || 'appName' },
|
||||||
|
);
|
||||||
const accessibleProjects =
|
const accessibleProjects =
|
||||||
await this.privateProjectChecker.getUserAccessibleProjects(userId);
|
await this.privateProjectChecker.getUserAccessibleProjects(userId);
|
||||||
if (accessibleProjects.mode === 'all') {
|
if (accessibleProjects.mode === 'all') {
|
||||||
|
@ -325,14 +325,12 @@ export default class ProjectController extends Controller {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
normalizedQuery,
|
normalizedQuery,
|
||||||
normalizedSortBy,
|
|
||||||
normalizedSortOrder,
|
normalizedSortOrder,
|
||||||
normalizedOffset,
|
normalizedOffset,
|
||||||
normalizedLimit,
|
normalizedLimit,
|
||||||
} = normalizeQueryParams(req.query, {
|
} = normalizeQueryParams(req.query, {
|
||||||
limitDefault: 50,
|
limitDefault: 50,
|
||||||
maxLimit: 100,
|
maxLimit: 100,
|
||||||
sortByDefault: 'appName',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const applications = await this.projectService.getApplications({
|
const applications = await this.projectService.getApplications({
|
||||||
@ -340,7 +338,7 @@ export default class ProjectController extends Controller {
|
|||||||
project: projectId,
|
project: projectId,
|
||||||
offset: normalizedOffset,
|
offset: normalizedOffset,
|
||||||
limit: normalizedLimit,
|
limit: normalizedLimit,
|
||||||
sortBy: normalizedSortBy,
|
sortBy: req.query.sortBy,
|
||||||
sortOrder: normalizedSortOrder,
|
sortOrder: normalizedSortOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1117,8 +1117,10 @@ export default class ProjectService {
|
|||||||
async getApplications(
|
async getApplications(
|
||||||
searchParams: IProjectApplicationsSearchParams,
|
searchParams: IProjectApplicationsSearchParams,
|
||||||
): Promise<IProjectApplications> {
|
): Promise<IProjectApplications> {
|
||||||
const applications =
|
const applications = await this.projectStore.getApplicationsByProject({
|
||||||
await this.projectStore.getApplicationsByProject(searchParams);
|
...searchParams,
|
||||||
|
sortBy: searchParams.sortBy || 'appName',
|
||||||
|
});
|
||||||
return applications;
|
return applications;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { FromSchema } from 'json-schema-to-ts';
|
import type { FromSchema } from 'json-schema-to-ts';
|
||||||
import { IEventTypes } from '../../types';
|
import { IEventTypes } from '../../types';
|
||||||
|
|
||||||
export const searchEventsSchema = {
|
export const deprecatedSearchEventsSchema = {
|
||||||
$id: '#/components/schemas/searchEventsSchema',
|
$id: '#/components/schemas/deprecatedSearchEventsSchema',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: `
|
description: `
|
||||||
Search for events by type, project, feature, free-text query,
|
Search for events by type, project, feature, free-text query,
|
||||||
@ -50,4 +50,6 @@ export const searchEventsSchema = {
|
|||||||
components: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type SearchEventsSchema = FromSchema<typeof searchEventsSchema>;
|
export type DeprecatedSearchEventsSchema = FromSchema<
|
||||||
|
typeof deprecatedSearchEventsSchema
|
||||||
|
>;
|
105
src/lib/openapi/spec/event-search-query-parameters.ts
Normal file
105
src/lib/openapi/spec/event-search-query-parameters.ts
Normal 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>
|
||||||
|
>;
|
34
src/lib/openapi/spec/event-search-response-schema.ts
Normal file
34
src/lib/openapi/spec/event-search-response-schema.ts
Normal 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
|
||||||
|
>;
|
@ -129,6 +129,7 @@ export const featureSearchQueryParameters = [
|
|||||||
name: 'sortOrder',
|
name: 'sortOrder',
|
||||||
schema: {
|
schema: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
enum: ['asc', 'desc'] as any,
|
||||||
example: 'desc',
|
example: 'desc',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
|
@ -61,6 +61,7 @@ export * from './date-schema';
|
|||||||
export * from './dependencies-exist-schema';
|
export * from './dependencies-exist-schema';
|
||||||
export * from './dependent-feature-schema';
|
export * from './dependent-feature-schema';
|
||||||
export * from './deprecated-project-overview-schema';
|
export * from './deprecated-project-overview-schema';
|
||||||
|
export * from './deprecated-search-events-schema';
|
||||||
export * from './dora-features-schema';
|
export * from './dora-features-schema';
|
||||||
export * from './edge-token-schema';
|
export * from './edge-token-schema';
|
||||||
export * from './email-schema';
|
export * from './email-schema';
|
||||||
@ -69,6 +70,7 @@ export * from './environment-schema';
|
|||||||
export * from './environments-project-schema';
|
export * from './environments-project-schema';
|
||||||
export * from './environments-schema';
|
export * from './environments-schema';
|
||||||
export * from './event-schema';
|
export * from './event-schema';
|
||||||
|
export * from './event-search-response-schema';
|
||||||
export * from './events-schema';
|
export * from './events-schema';
|
||||||
export * from './export-query-schema';
|
export * from './export-query-schema';
|
||||||
export * from './export-result-schema';
|
export * from './export-result-schema';
|
||||||
@ -161,7 +163,6 @@ export * from './role-schema';
|
|||||||
export * from './roles-schema';
|
export * from './roles-schema';
|
||||||
export * from './sdk-context-schema';
|
export * from './sdk-context-schema';
|
||||||
export * from './sdk-flat-context-schema';
|
export * from './sdk-flat-context-schema';
|
||||||
export * from './search-events-schema';
|
|
||||||
export * from './search-features-schema';
|
export * from './search-features-schema';
|
||||||
export * from './segment-schema';
|
export * from './segment-schema';
|
||||||
export * from './segment-strategies-schema';
|
export * from './segment-strategies-schema';
|
||||||
|
@ -19,8 +19,18 @@ import {
|
|||||||
} from '../../../lib/openapi/spec/feature-events-schema';
|
} from '../../../lib/openapi/spec/feature-events-schema';
|
||||||
import { getStandardResponses } from '../../../lib/openapi/util/standard-responses';
|
import { getStandardResponses } from '../../../lib/openapi/util/standard-responses';
|
||||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
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 { 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 ANON_KEYS = ['email', 'username', 'createdBy'];
|
||||||
const version = 1 as const;
|
const version = 1 as const;
|
||||||
@ -98,6 +108,27 @@ export default class EventController extends Controller {
|
|||||||
this.route({
|
this.route({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/search',
|
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,
|
handler: this.searchEvents,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
@ -106,9 +137,11 @@ export default class EventController extends Controller {
|
|||||||
tags: ['Events'],
|
tags: ['Events'],
|
||||||
summary: 'Search for events',
|
summary: 'Search for events',
|
||||||
description:
|
description:
|
||||||
'Allows searching for events matching the search criteria in the request body',
|
'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.',
|
||||||
requestBody: createRequestSchema('searchEventsSchema'),
|
parameters: [...eventSearchQueryParameters],
|
||||||
responses: { 200: createResponseSchema('eventsSchema') },
|
responses: {
|
||||||
|
200: createResponseSchema('eventSearchResponseSchema'),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -128,7 +161,9 @@ export default class EventController extends Controller {
|
|||||||
const { project } = req.query;
|
const { project } = req.query;
|
||||||
let eventList: IEventList;
|
let eventList: IEventList;
|
||||||
if (project) {
|
if (project) {
|
||||||
eventList = await this.eventService.searchEvents({ project });
|
eventList = await this.eventService.deprecatedSearchEvents({
|
||||||
|
project,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
eventList = await this.eventService.getEvents();
|
eventList = await this.eventService.getEvents();
|
||||||
}
|
}
|
||||||
@ -152,7 +187,9 @@ export default class EventController extends Controller {
|
|||||||
res: Response<FeatureEventsSchema>,
|
res: Response<FeatureEventsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const feature = req.params.featureName;
|
const feature = req.params.featureName;
|
||||||
const eventList = await this.eventService.searchEvents({ feature });
|
const eventList = await this.eventService.deprecatedSearchEvents({
|
||||||
|
feature,
|
||||||
|
});
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
version,
|
version,
|
||||||
@ -169,11 +206,13 @@ export default class EventController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchEvents(
|
async deprecatedSearchEvents(
|
||||||
req: Request<unknown, unknown, SearchEventsSchema>,
|
req: Request<unknown, unknown, DeprecatedSearchEventsSchema>,
|
||||||
res: Response<EventsSchema>,
|
res: Response<EventsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const eventList = await this.eventService.searchEvents(req.body);
|
const eventList = await this.eventService.deprecatedSearchEvents(
|
||||||
|
req.body,
|
||||||
|
);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
version,
|
version,
|
||||||
@ -188,4 +227,33 @@ export default class EventController extends Controller {
|
|||||||
response,
|
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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,14 +246,12 @@ class MetricsController extends Controller {
|
|||||||
const { user } = req;
|
const { user } = req;
|
||||||
const {
|
const {
|
||||||
normalizedQuery,
|
normalizedQuery,
|
||||||
normalizedSortBy,
|
|
||||||
normalizedSortOrder,
|
normalizedSortOrder,
|
||||||
normalizedOffset,
|
normalizedOffset,
|
||||||
normalizedLimit,
|
normalizedLimit,
|
||||||
} = normalizeQueryParams(req.query, {
|
} = normalizeQueryParams(req.query, {
|
||||||
limitDefault: 1000,
|
limitDefault: 1000,
|
||||||
maxLimit: 1000,
|
maxLimit: 1000,
|
||||||
sortByDefault: 'appName',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const applications = await this.clientInstanceService.getApplications(
|
const applications = await this.clientInstanceService.getApplications(
|
||||||
@ -261,7 +259,7 @@ class MetricsController extends Controller {
|
|||||||
searchParams: normalizedQuery,
|
searchParams: normalizedQuery,
|
||||||
offset: normalizedOffset,
|
offset: normalizedOffset,
|
||||||
limit: normalizedLimit,
|
limit: normalizedLimit,
|
||||||
sortBy: normalizedSortBy,
|
sortBy: req.query.sortBy,
|
||||||
sortOrder: normalizedSortOrder,
|
sortOrder: normalizedSortOrder,
|
||||||
},
|
},
|
||||||
extractUserIdFromUser(user),
|
extractUserIdFromUser(user),
|
||||||
|
@ -280,8 +280,12 @@ export default class VersionService {
|
|||||||
this.strategyStore.count(),
|
this.strategyStore.count(),
|
||||||
this.hasSAML(),
|
this.hasSAML(),
|
||||||
this.hasOIDC(),
|
this.hasOIDC(),
|
||||||
this.eventStore.filteredCount({ type: FEATURES_EXPORTED }),
|
this.eventStore.deprecatedFilteredCount({
|
||||||
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
|
type: FEATURES_EXPORTED,
|
||||||
|
}),
|
||||||
|
this.eventStore.deprecatedFilteredCount({
|
||||||
|
type: FEATURES_IMPORTED,
|
||||||
|
}),
|
||||||
this.userStats(),
|
this.userStats(),
|
||||||
this.productionChanges(),
|
this.productionChanges(),
|
||||||
this.postgresVersion(),
|
this.postgresVersion(),
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import type { IBaseEvent, IEvent } from '../events';
|
import type { IBaseEvent, IEvent } from '../events';
|
||||||
import type { Store } from './store';
|
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 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
|
export interface IEventStore
|
||||||
extends Store<IEvent, number>,
|
extends Store<IEvent, number>,
|
||||||
@ -12,8 +16,20 @@ export interface IEventStore
|
|||||||
batchStore(events: IBaseEvent[]): Promise<void>;
|
batchStore(events: IBaseEvent[]): Promise<void>;
|
||||||
getEvents(): Promise<IEvent[]>;
|
getEvents(): Promise<IEvent[]>;
|
||||||
count(): Promise<number>;
|
count(): Promise<number>;
|
||||||
filteredCount(search: SearchEventsSchema): Promise<number>;
|
deprecatedFilteredCount(
|
||||||
searchEvents(search: SearchEventsSchema): Promise<IEvent[]>;
|
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>;
|
getMaxRevisionId(currentMax?: number): Promise<number>;
|
||||||
query(operations: IQueryOperations[]): Promise<IEvent[]>;
|
query(operations: IQueryOperations[]): Promise<IEvent[]>;
|
||||||
queryCount(operations: IQueryOperations[]): Promise<number>;
|
queryCount(operations: IQueryOperations[]): Promise<number>;
|
||||||
|
@ -36,7 +36,7 @@ test('Can create new setting', async () => {
|
|||||||
|
|
||||||
expect(actual).toStrictEqual(someData);
|
expect(actual).toStrictEqual(someData);
|
||||||
const { eventStore } = stores;
|
const { eventStore } = stores;
|
||||||
const createdEvents = await eventStore.searchEvents({
|
const createdEvents = await eventStore.deprecatedSearchEvents({
|
||||||
type: SETTING_CREATED,
|
type: SETTING_CREATED,
|
||||||
});
|
});
|
||||||
expect(createdEvents).toHaveLength(1);
|
expect(createdEvents).toHaveLength(1);
|
||||||
@ -51,7 +51,7 @@ test('Can delete setting', async () => {
|
|||||||
const actual = await service.get('some-setting');
|
const actual = await service.get('some-setting');
|
||||||
expect(actual).toBeUndefined();
|
expect(actual).toBeUndefined();
|
||||||
const { eventStore } = stores;
|
const { eventStore } = stores;
|
||||||
const createdEvents = await eventStore.searchEvents({
|
const createdEvents = await eventStore.deprecatedSearchEvents({
|
||||||
type: SETTING_DELETED,
|
type: SETTING_DELETED,
|
||||||
});
|
});
|
||||||
expect(createdEvents).toHaveLength(1);
|
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 actual = await service.get(property);
|
||||||
const { eventStore } = stores;
|
const { eventStore } = stores;
|
||||||
|
|
||||||
const updatedEvents = await eventStore.searchEvents({
|
const updatedEvents = await eventStore.deprecatedSearchEvents({
|
||||||
type: SETTING_UPDATED,
|
type: SETTING_UPDATED,
|
||||||
});
|
});
|
||||||
expect(updatedEvents[0].preData).toEqual({ hideEventDetails: true });
|
expect(updatedEvents[0].preData).toEqual({ hideEventDetails: true });
|
||||||
@ -83,7 +83,7 @@ test('Can update setting', async () => {
|
|||||||
TEST_AUDIT_USER,
|
TEST_AUDIT_USER,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const updatedEvents = await eventStore.searchEvents({
|
const updatedEvents = await eventStore.deprecatedSearchEvents({
|
||||||
type: SETTING_UPDATED,
|
type: SETTING_UPDATED,
|
||||||
});
|
});
|
||||||
expect(updatedEvents).toHaveLength(1);
|
expect(updatedEvents).toHaveLength(1);
|
||||||
|
@ -234,11 +234,11 @@ test('Should get all events of type', async () => {
|
|||||||
return eventStore.store(event);
|
return eventStore.store(event);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const featureCreatedEvents = await eventStore.searchEvents({
|
const featureCreatedEvents = await eventStore.deprecatedSearchEvents({
|
||||||
type: FEATURE_CREATED,
|
type: FEATURE_CREATED,
|
||||||
});
|
});
|
||||||
expect(featureCreatedEvents).toHaveLength(3);
|
expect(featureCreatedEvents).toHaveLength(3);
|
||||||
const featureDeletedEvents = await eventStore.searchEvents({
|
const featureDeletedEvents = await eventStore.deprecatedSearchEvents({
|
||||||
type: FEATURE_DELETED,
|
type: FEATURE_DELETED,
|
||||||
});
|
});
|
||||||
expect(featureDeletedEvents).toHaveLength(3);
|
expect(featureDeletedEvents).toHaveLength(3);
|
||||||
|
28
src/test/fixtures/fake-event-store.ts
vendored
28
src/test/fixtures/fake-event-store.ts
vendored
@ -2,7 +2,7 @@ import type { IEventStore } from '../../lib/types/stores/event-store';
|
|||||||
import type { IBaseEvent, IEvent } from '../../lib/types/events';
|
import type { IBaseEvent, IEvent } from '../../lib/types/events';
|
||||||
import { sharedEventEmitter } from '../../lib/util/anyEventEmitter';
|
import { sharedEventEmitter } from '../../lib/util/anyEventEmitter';
|
||||||
import type { IQueryOperations } from '../../lib/features/events/event-store';
|
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';
|
import type EventEmitter from 'events';
|
||||||
|
|
||||||
class FakeEventStore implements IEventStore {
|
class FakeEventStore implements IEventStore {
|
||||||
@ -61,7 +61,13 @@ class FakeEventStore implements IEventStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// 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);
|
return Promise.resolve(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,23 +85,11 @@ class FakeEventStore implements IEventStore {
|
|||||||
return this.events;
|
return this.events;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchEvents(): Promise<IEvent[]> {
|
async deprecatedSearchEvents(): Promise<IEvent[]> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
async searchEvents(): Promise<IEvent[]> {
|
||||||
async getForFeatures(
|
throw new Error('Method not implemented.');
|
||||||
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 query(operations: IQueryOperations[]): Promise<IEvent[]> {
|
async query(operations: IQueryOperations[]): Promise<IEvent[]> {
|
||||||
|
Loading…
Reference in New Issue
Block a user