unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
179 lines • 7.17 kB
JavaScript
import { ApiTokenType } from '../../types/model.js';
import { EVENTS_CREATED_BY_PROCESSED } from '../../metric-events.js';
import { parseSearchOperatorValue } from '../feature-search/search-utils.js';
import { addDays, formatISO } from 'date-fns';
import lodash from 'lodash';
const { isEqual } = lodash;
export default class EventService {
constructor({ eventStore, featureTagStore, }, { getLogger, eventBus, isEnterprise, }, privateProjectChecker, accessReadModel) {
this.convertToDbParams = (params) => {
const queryParams = [];
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 !== 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;
};
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() {
const totalEvents = await this.eventStore.count();
const events = await this.eventStore.getEvents();
return {
events,
totalEvents,
};
}
async searchEvents(search, userId) {
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, listener) {
return this.eventStore.on(eventName, listener);
}
off(eventName, listener) {
return this.eventStore.off(eventName, listener);
}
async enhanceEventsWithTags(events) {
const featureNamesSet = new Set();
for (const event of events) {
if (event.featureName && !event.tags) {
featureNamesSet.add(event.featureName);
}
}
const featureTagsMap = 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) {
return user?.type === ApiTokenType.ADMIN;
}
async storeEvent(event) {
return this.storeEvents([event]);
}
async storeEvents(events) {
// 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() {
const updated = await this.eventStore.setCreatedByUserId(100);
if (updated !== undefined) {
this.eventBus.emit(EVENTS_CREATED_BY_PROCESSED, {
updated,
});
}
}
async getEventCreators() {
return this.eventStore.getEventCreators();
}
async getProjectFilterForNonAdmins(userId) {
const isRootAdmin = await this.accessReadModel.isRootAdmin(userId);
if (!isRootAdmin) {
return [{ field: 'project', operator: 'IS_NOT', values: [null] }];
}
return [];
}
}
export const filterAccessibleProjects = (projectParam, projectAccess) => {
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;
};
//# sourceMappingURL=event-service.js.map