1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-27 00:19:39 +01:00
unleash.unleash/src/lib/features/events/event-service.ts
Thomas Heartman b276d5a89c
fix: correctly filter for to dates (#8567)
This change fixes a bug in the event filter's `to` query parameter.

The problem was that in an attempt to make it inclusive, we also
stripped it of the `IS:` prefix, which meant it had no effect. This
change fixes that by first splitting the value, fixing only the
date (because we want it to include the entire day), and then joining
it back together.
2024-10-29 09:42:15 +01:00

275 lines
8.7 KiB
TypeScript

import type { IUnleashConfig } from '../../types/option';
import type { IFeatureTagStore, IUnleashStores } from '../../types/stores';
import type { Logger } from '../../logger';
import type {
IEventSearchParams,
IEventStore,
} from '../../types/stores/event-store';
import type { IBaseEvent, IEventList } from '../../types/events';
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 { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import { parseSearchOperatorValue } from '../feature-search/search-utils';
import { addDays, formatISO } from 'date-fns';
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
import type { ProjectAccess } from '../private-project/privateProjectStore';
import type { IAccessReadModel } from '../access/access-read-model-type';
export default class EventService {
private logger: Logger;
private eventStore: IEventStore;
private featureTagStore: IFeatureTagStore;
private accessReadModel: IAccessReadModel;
private privateProjectChecker: IPrivateProjectChecker;
private eventBus: EventEmitter;
constructor(
{
eventStore,
featureTagStore,
}: Pick<IUnleashStores, 'eventStore' | 'featureTagStore'>,
{ getLogger, eventBus }: Pick<IUnleashConfig, 'getLogger' | 'eventBus'>,
privateProjectChecker: IPrivateProjectChecker,
accessReadModel: IAccessReadModel,
) {
this.logger = getLogger('services/event-service.ts');
this.eventStore = eventStore;
this.privateProjectChecker = privateProjectChecker;
this.featureTagStore = featureTagStore;
this.eventBus = eventBus;
this.accessReadModel = accessReadModel;
}
async getEvents(): Promise<IEventList> {
const totalEvents = await this.eventStore.count();
const events = await this.eventStore.getEvents();
return {
events,
totalEvents,
};
}
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: IEventSearchParams,
userId: number,
): Promise<IEventList> {
const projectAccess =
await this.privateProjectChecker.getUserAccessibleProjects(userId);
search.project = filterAccessibleProjects(
search.project,
projectAccess,
);
const queryParams = this.convertToDbParams(search);
const projectFilter = await this.getProjectFilterForNonAdmins(userId);
queryParams.push(...projectFilter);
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,
};
}
async onEvent(
eventName: string | symbol,
listener: (...args: any[]) => void,
): Promise<EventEmitter> {
return this.eventStore.on(eventName, listener);
}
private async enhanceEventsWithTags(
events: IBaseEvent[],
): Promise<IBaseEvent[]> {
const featureNamesSet = new Set<string>();
for (const event of events) {
if (event.featureName && !event.tags) {
featureNamesSet.add(event.featureName);
}
}
const featureTagsMap: Map<string, ITag[]> = new Map();
const allTagsInFeatures = await this.featureTagStore.getAllByFeatures(
Array.from(featureNamesSet),
);
for (const tag of allTagsInFeatures) {
const featureTags = featureTagsMap.get(tag.featureName) || [];
featureTags.push({ value: tag.tagValue, type: tag.tagType });
featureTagsMap.set(tag.featureName, featureTags);
}
for (const event of events) {
if (event.featureName && !event.tags) {
event.tags = featureTagsMap.get(event.featureName);
}
}
return events;
}
isAdminToken(user: IUser | IApiUser): boolean {
return (user as IApiUser)?.type === ApiTokenType.ADMIN;
}
async storeEvent(event: IBaseEvent): Promise<void> {
return this.storeEvents([event]);
}
async storeEvents(events: IBaseEvent[]): Promise<void> {
let enhancedEvents = events;
for (const enhancer of [this.enhanceEventsWithTags.bind(this)]) {
enhancedEvents = await enhancer(enhancedEvents);
}
return this.eventStore.batchStore(enhancedEvents);
}
async setEventCreatedByUserId(): Promise<void> {
const updated = await this.eventStore.setCreatedByUserId(100);
if (updated !== undefined) {
this.eventBus.emit(EVENTS_CREATED_BY_PROCESSED, {
updated,
});
}
}
convertToDbParams = (params: IEventSearchParams): IQueryParam[] => {
const queryParams: IQueryParam[] = [];
if (params.from) {
const parsed = parseSearchOperatorValue('created_at', params.from);
if (parsed) {
queryParams.push({
field: parsed.field,
operator: 'IS_ON_OR_AFTER',
values: parsed.values,
});
}
}
if (params.to) {
const parsed = parseSearchOperatorValue('created_at', params.to);
if (parsed) {
const values = parsed.values
.filter((v): v is string => v !== null)
.map((date) =>
formatISO(addDays(new Date(date), 1), {
representation: 'date',
}),
);
queryParams.push({
field: parsed.field,
operator: 'IS_BEFORE',
values,
});
}
}
if (params.createdBy) {
const parsed = parseSearchOperatorValue(
'created_by_user_id',
params.createdBy,
);
if (parsed) queryParams.push(parsed);
}
if (params.feature) {
const parsed = parseSearchOperatorValue(
'feature_name',
params.feature,
);
if (parsed) queryParams.push(parsed);
}
['project', 'type'].forEach((field) => {
if (params[field]) {
const parsed = parseSearchOperatorValue(field, params[field]);
if (parsed) queryParams.push(parsed);
}
});
return queryParams;
};
async getEventCreators() {
return this.eventStore.getEventCreators();
}
async getProjectFilterForNonAdmins(userId: number): Promise<IQueryParam[]> {
const isRootAdmin = await this.accessReadModel.isRootAdmin(userId);
if (!isRootAdmin) {
return [{ field: 'project', operator: 'IS_NOT', values: [null] }];
}
return [];
}
}
export const filterAccessibleProjects = (
projectParam: string | undefined,
projectAccess: ProjectAccess,
): string | undefined => {
if (projectAccess.mode !== 'all') {
const allowedProjects = projectAccess.projects;
if (!projectParam) {
return `IS_ANY_OF:${allowedProjects.join(',')}`;
} else {
const searchProjectList = projectParam.split(',');
const filteredProjects = searchProjectList
.filter((proj) =>
allowedProjects.includes(
proj.replace(/^(IS|IS_ANY_OF):/, ''),
),
)
.join(',');
if (!filteredProjects) {
throw new Error(
'No accessible projects in the search parameters',
);
}
return filteredProjects;
}
}
return projectParam;
};