1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-11-10 01:19:53 +01:00
unleash.unleash/src/lib/features/events/event-service.ts

291 lines
9.2 KiB
TypeScript

import type { IUnleashConfig } from '../../types/option.js';
import type { IFeatureTagStore, IUnleashStores } from '../../types/stores.js';
import type { Logger } from '../../logger.js';
import type {
IEventSearchParams,
IEventStore,
} from '../../types/stores/event-store.js';
import type { IApiUser, IUser } from '../../types/index.js';
import type EventEmitter from 'node:events';
import { ApiTokenType } from '../../types/model.js';
import { EVENTS_CREATED_BY_PROCESSED } from '../../metric-events.js';
import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type.js';
import { parseSearchOperatorValue } from '../feature-search/search-utils.js';
import { addDays, formatISO } from 'date-fns';
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType.js';
import type { ProjectAccess } from '../private-project/privateProjectStore.js';
import type { IAccessReadModel } from '../access/access-read-model-type.js';
import lodash from 'lodash';
import type { IEventList, IBaseEvent } from '../../events/index.js';
import type { ITag } from '../../tags/index.js';
const { isEqual } = lodash;
export default class EventService {
private logger: Logger;
private eventStore: IEventStore;
private featureTagStore: IFeatureTagStore;
private accessReadModel: IAccessReadModel;
private privateProjectChecker: IPrivateProjectChecker;
private eventBus: EventEmitter;
private isEnterprise: boolean;
constructor(
{
eventStore,
featureTagStore,
}: Pick<IUnleashStores, 'eventStore' | 'featureTagStore'>,
{
getLogger,
eventBus,
isEnterprise,
}: Pick<IUnleashConfig, 'getLogger' | 'eventBus' | 'isEnterprise'>,
privateProjectChecker: IPrivateProjectChecker,
accessReadModel: IAccessReadModel,
) {
this.logger = getLogger('services/event-service.ts');
this.eventStore = eventStore;
this.privateProjectChecker = privateProjectChecker;
this.featureTagStore = featureTagStore;
this.eventBus = eventBus;
this.isEnterprise = isEnterprise;
this.accessReadModel = accessReadModel;
}
async getEvents(): Promise<IEventList> {
const totalEvents = await this.eventStore.count();
const events = await this.eventStore.getEvents();
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(
queryParams,
search.query,
);
const events = await this.eventStore.searchEvents(
{
limit: search.limit,
offset: search.offset,
query: search.query,
},
queryParams,
{
withIp: this.isEnterprise,
},
);
return {
events,
totalEvents,
};
}
onEvent(
eventName: string | symbol,
listener: (...args: any[]) => void,
): EventEmitter {
return this.eventStore.on(eventName, listener);
}
off(
eventName: string | symbol,
listener: (...args: any[]) => void,
): EventEmitter {
return this.eventStore.off(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> {
// if the event comes with both preData and data, we need to check if they are different before storing, otherwise we discard the event
let enhancedEvents = events.filter(
(event) =>
!event.preData ||
!event.data ||
!isEqual(event.preData, event.data),
);
if (enhancedEvents.length === 0) {
return;
}
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);
}
if (params.groupId) {
const parsed = parseSearchOperatorValue('group_id', params.groupId);
if (parsed) queryParams.push(parsed);
}
['project', 'type', 'environment', 'id'].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;
};