mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
chore: re use extract user methods (#5947)
## About the changes 1. Re-use existing methods in extract-user.ts:70f6a07f2c/src/lib/features/events/event-service.ts (L93-L101)
2. Move event-service and event-store to features/event 3. Add export default in previous paths for backward compatibility:70f6a07f2c/src/lib/services/event-service.ts (L1-L4)
and70f6a07f2c/src/lib/db/event-store.ts (L1-L4)
This commit is contained in:
parent
605125fbb5
commit
b91df61994
@ -1,433 +1,4 @@
|
||||
import {
|
||||
IEvent,
|
||||
IBaseEvent,
|
||||
SEGMENT_UPDATED,
|
||||
FEATURE_IMPORT,
|
||||
FEATURES_IMPORTED,
|
||||
IEventType,
|
||||
} from '../types/events';
|
||||
import { LogProvider, Logger } from '../logger';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { ITag } from '../types/model';
|
||||
import { SearchEventsSchema } from '../openapi/spec/search-events-schema';
|
||||
import { sharedEventEmitter } from '../util/anyEventEmitter';
|
||||
import { Db } from './db';
|
||||
import { Knex } from 'knex';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
const EVENT_COLUMNS = [
|
||||
'id',
|
||||
'type',
|
||||
'created_by',
|
||||
'created_at',
|
||||
'created_by_user_id',
|
||||
'data',
|
||||
'pre_data',
|
||||
'tags',
|
||||
'feature_name',
|
||||
'project',
|
||||
'environment',
|
||||
] as const;
|
||||
|
||||
export type IQueryOperations =
|
||||
| IWhereOperation
|
||||
| IBeforeDateOperation
|
||||
| IBetweenDatesOperation
|
||||
| IForFeaturesOperation;
|
||||
|
||||
interface IWhereOperation {
|
||||
op: 'where';
|
||||
parameters: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IBeforeDateOperation {
|
||||
op: 'beforeDate';
|
||||
parameters: {
|
||||
dateAccessor: string;
|
||||
date: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IBetweenDatesOperation {
|
||||
op: 'betweenDate';
|
||||
parameters: {
|
||||
dateAccessor: string;
|
||||
range: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IForFeaturesOperation {
|
||||
op: 'forFeatures';
|
||||
parameters: IForFeaturesParams;
|
||||
}
|
||||
|
||||
interface IForFeaturesParams {
|
||||
type: string;
|
||||
projectId: string;
|
||||
environments: string[];
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export interface IEventTable {
|
||||
id: number;
|
||||
type: string;
|
||||
created_by: string;
|
||||
created_at: Date;
|
||||
created_by_user_id: number;
|
||||
data?: any;
|
||||
pre_data?: any;
|
||||
feature_name?: string;
|
||||
project?: string;
|
||||
environment?: string;
|
||||
tags: ITag[];
|
||||
}
|
||||
|
||||
const TABLE = 'events';
|
||||
|
||||
class EventStore implements IEventStore {
|
||||
private db: Db;
|
||||
|
||||
// only one shared event emitter should exist across all event store instances
|
||||
private eventEmitter: EventEmitter = sharedEventEmitter;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
// a new DB has to be injected per transaction
|
||||
constructor(db: Db, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('lib/db/event-store.ts');
|
||||
}
|
||||
|
||||
async store(event: IBaseEvent): Promise<void> {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.insert(this.eventToDbRow(event))
|
||||
.returning(EVENT_COLUMNS);
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn(`Failed to store "${event.type}" event: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
const count = await this.db(TABLE)
|
||||
.count<Record<string, number>>()
|
||||
.first();
|
||||
if (!count) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof count.count === 'string') {
|
||||
return parseInt(count.count, 10);
|
||||
} else {
|
||||
return count.count;
|
||||
}
|
||||
}
|
||||
|
||||
async filteredCount(eventSearch: SearchEventsSchema): Promise<number> {
|
||||
let query = this.db(TABLE);
|
||||
if (eventSearch.type) {
|
||||
query = query.andWhere({ type: eventSearch.type });
|
||||
}
|
||||
if (eventSearch.project) {
|
||||
query = query.andWhere({ project: eventSearch.project });
|
||||
}
|
||||
if (eventSearch.feature) {
|
||||
query = query.andWhere({ feature_name: eventSearch.feature });
|
||||
}
|
||||
const count = await query.count().first();
|
||||
if (!count) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof count.count === 'string') {
|
||||
return parseInt(count.count, 10);
|
||||
} else {
|
||||
return count.count;
|
||||
}
|
||||
}
|
||||
|
||||
async batchStore(events: IBaseEvent[]): Promise<void> {
|
||||
try {
|
||||
await this.db(TABLE).insert(events.map(this.eventToDbRow));
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn(`Failed to store events: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getMaxRevisionId(largerThan: number = 0): Promise<number> {
|
||||
const row = await this.db(TABLE)
|
||||
.max('id')
|
||||
.where((builder) =>
|
||||
builder
|
||||
.whereNotNull('feature_name')
|
||||
.orWhereIn('type', [
|
||||
SEGMENT_UPDATED,
|
||||
FEATURE_IMPORT,
|
||||
FEATURES_IMPORTED,
|
||||
]),
|
||||
)
|
||||
.andWhere('id', '>=', largerThan)
|
||||
.first();
|
||||
return row?.max ?? 0;
|
||||
}
|
||||
|
||||
async delete(key: number): Promise<void> {
|
||||
await this.db(TABLE).where({ id: key }).del();
|
||||
}
|
||||
|
||||
async deleteAll(): Promise<void> {
|
||||
await this.db(TABLE).del();
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
async exists(key: number): Promise<boolean> {
|
||||
const result = await this.db.raw(
|
||||
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`,
|
||||
[key],
|
||||
);
|
||||
const { present } = result.rows[0];
|
||||
return present;
|
||||
}
|
||||
|
||||
async query(operations: IQueryOperations[]): Promise<IEvent[]> {
|
||||
try {
|
||||
let query: Knex.QueryBuilder = this.select();
|
||||
|
||||
operations.forEach((operation) => {
|
||||
if (operation.op === 'where') {
|
||||
query = this.where(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'forFeatures') {
|
||||
query = this.forFeatures(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'beforeDate') {
|
||||
query = this.beforeDate(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'betweenDate') {
|
||||
query = this.betweenDate(query, operation.parameters);
|
||||
}
|
||||
});
|
||||
|
||||
const rows = await query;
|
||||
return rows.map(this.rowToEvent);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async queryCount(operations: IQueryOperations[]): Promise<number> {
|
||||
try {
|
||||
let query: Knex.QueryBuilder = this.db.count().from(TABLE);
|
||||
|
||||
operations.forEach((operation) => {
|
||||
if (operation.op === 'where') {
|
||||
query = this.where(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'forFeatures') {
|
||||
query = this.forFeatures(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'beforeDate') {
|
||||
query = this.beforeDate(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'betweenDate') {
|
||||
query = this.betweenDate(query, operation.parameters);
|
||||
}
|
||||
});
|
||||
|
||||
const queryResult = await query.first();
|
||||
return parseInt(queryResult.count || 0);
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
where(
|
||||
query: Knex.QueryBuilder,
|
||||
parameters: { [key: string]: string },
|
||||
): Knex.QueryBuilder {
|
||||
return query.where(parameters);
|
||||
}
|
||||
|
||||
beforeDate(
|
||||
query: Knex.QueryBuilder,
|
||||
parameters: { dateAccessor: string; date: string },
|
||||
): Knex.QueryBuilder {
|
||||
return query.andWhere(parameters.dateAccessor, '>=', parameters.date);
|
||||
}
|
||||
|
||||
betweenDate(
|
||||
query: Knex.QueryBuilder,
|
||||
parameters: { dateAccessor: string; range: string[] },
|
||||
): Knex.QueryBuilder {
|
||||
if (parameters.range && parameters.range.length === 2) {
|
||||
return query.andWhereBetween(parameters.dateAccessor, [
|
||||
parameters.range[0],
|
||||
parameters.range[1],
|
||||
]);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
select(): Knex.QueryBuilder {
|
||||
return this.db.select(EVENT_COLUMNS).from(TABLE);
|
||||
}
|
||||
|
||||
forFeatures(
|
||||
query: Knex.QueryBuilder,
|
||||
parameters: IForFeaturesParams,
|
||||
): Knex.QueryBuilder {
|
||||
return query
|
||||
.where({ type: parameters.type, project: parameters.projectId })
|
||||
.whereIn('feature_name', parameters.features)
|
||||
.whereIn('environment', parameters.environments);
|
||||
}
|
||||
|
||||
async get(key: number): Promise<IEvent> {
|
||||
const row = await this.db(TABLE).where({ id: key }).first();
|
||||
return this.rowToEvent(row);
|
||||
}
|
||||
|
||||
async getAll(query?: Object): Promise<IEvent[]> {
|
||||
return this.getEvents(query);
|
||||
}
|
||||
|
||||
async getEvents(query?: Object): Promise<IEvent[]> {
|
||||
try {
|
||||
let qB = this.db
|
||||
.select(EVENT_COLUMNS)
|
||||
.from(TABLE)
|
||||
.limit(100)
|
||||
.orderBy('created_at', 'desc');
|
||||
if (query) {
|
||||
qB = qB.where(query);
|
||||
}
|
||||
const rows = await qB;
|
||||
return rows.map(this.rowToEvent);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async searchEvents(search: SearchEventsSchema = {}): Promise<IEvent[]> {
|
||||
let query = this.db
|
||||
.select(EVENT_COLUMNS)
|
||||
.from<IEventTable>(TABLE)
|
||||
.limit(search.limit ?? 100)
|
||||
.offset(search.offset ?? 0)
|
||||
.orderBy('created_at', 'desc');
|
||||
|
||||
if (search.type) {
|
||||
query = query.andWhere({
|
||||
type: search.type,
|
||||
});
|
||||
}
|
||||
|
||||
if (search.project) {
|
||||
query = query.andWhere({
|
||||
project: search.project,
|
||||
});
|
||||
}
|
||||
|
||||
if (search.feature) {
|
||||
query = query.andWhere({
|
||||
feature_name: search.feature,
|
||||
});
|
||||
}
|
||||
|
||||
if (search.query) {
|
||||
query = query.where((where) =>
|
||||
where
|
||||
.orWhereRaw('type::text ILIKE ?', `%${search.query}%`)
|
||||
.orWhereRaw('created_by::text ILIKE ?', `%${search.query}%`)
|
||||
.orWhereRaw('data::text ILIKE ?', `%${search.query}%`)
|
||||
.orWhereRaw('tags::text ILIKE ?', `%${search.query}%`)
|
||||
.orWhereRaw('pre_data::text ILIKE ?', `%${search.query}%`),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return (await query).map(this.rowToEvent);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
rowToEvent(row: IEventTable): IEvent {
|
||||
return {
|
||||
id: row.id,
|
||||
type: row.type as IEventType,
|
||||
createdBy: row.created_by,
|
||||
createdAt: row.created_at,
|
||||
createdByUserId: row.created_by_user_id,
|
||||
data: row.data,
|
||||
preData: row.pre_data,
|
||||
tags: row.tags || [],
|
||||
featureName: row.feature_name,
|
||||
project: row.project,
|
||||
environment: row.environment,
|
||||
};
|
||||
}
|
||||
|
||||
eventToDbRow(e: IBaseEvent): Omit<IEventTable, 'id' | 'created_at'> {
|
||||
return {
|
||||
type: e.type,
|
||||
created_by: e.createdBy ?? 'admin',
|
||||
created_by_user_id: e.createdByUserId,
|
||||
data: Array.isArray(e.data) ? JSON.stringify(e.data) : e.data,
|
||||
pre_data: Array.isArray(e.preData)
|
||||
? JSON.stringify(e.preData)
|
||||
: e.preData,
|
||||
// @ts-expect-error workaround for json-array
|
||||
tags: JSON.stringify(e.tags),
|
||||
feature_name: e.featureName,
|
||||
project: e.project,
|
||||
environment: e.environment,
|
||||
};
|
||||
}
|
||||
|
||||
setMaxListeners(number: number): EventEmitter {
|
||||
return this.eventEmitter.setMaxListeners(number);
|
||||
}
|
||||
|
||||
on(
|
||||
eventName: string | symbol,
|
||||
listener: (...args: any[]) => void,
|
||||
): EventEmitter {
|
||||
return this.eventEmitter.on(eventName, listener);
|
||||
}
|
||||
|
||||
emit(eventName: string | symbol, ...args: any[]): boolean {
|
||||
return this.eventEmitter.emit(eventName, ...args);
|
||||
}
|
||||
|
||||
off(
|
||||
eventName: string | symbol,
|
||||
listener: (...args: any[]) => void,
|
||||
): EventEmitter {
|
||||
return this.eventEmitter.off(eventName, listener);
|
||||
}
|
||||
|
||||
async setUnannouncedToAnnounced(): Promise<IEvent[]> {
|
||||
const rows = await this.db(TABLE)
|
||||
.update({ announced: true })
|
||||
.where('announced', false)
|
||||
.returning(EVENT_COLUMNS);
|
||||
return rows.map(this.rowToEvent);
|
||||
}
|
||||
|
||||
async publishUnannouncedEvents(): Promise<void> {
|
||||
const events = await this.setUnannouncedToAnnounced();
|
||||
|
||||
events.forEach((e) => this.eventEmitter.emit(e.type, e));
|
||||
}
|
||||
}
|
||||
|
||||
import EventStore from '../features/events/event-store';
|
||||
// For backward compatibility
|
||||
export * from '../features/events/event-store';
|
||||
export default EventStore;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IUnleashConfig, IUnleashStores } from '../types';
|
||||
|
||||
import EventStore from './event-store';
|
||||
import EventStore from '../features/events/event-store';
|
||||
import FeatureToggleStore from '../features/feature-toggle/feature-toggle-store';
|
||||
import FeatureTypeStore from './feature-type-store';
|
||||
import StrategyStore from './strategy-store';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import FakeEventStore from '../../../test/fixtures/fake-event-store';
|
||||
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
|
||||
import { Db } from '../../db/db';
|
||||
import EventStore from '../../db/event-store';
|
||||
import EventStore from './event-store';
|
||||
import FeatureTagStore from '../../db/feature-tag-store';
|
||||
import { EventService } from '../../services';
|
||||
import { IUnleashConfig } from '../../types';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ADMIN_TOKEN_USER, IApiUser } from '../types';
|
||||
import { createTestConfig } from '../../test/config/test-config';
|
||||
import { createFakeEventsService } from '../../lib/features';
|
||||
import { ApiTokenType } from '../../lib/types/models/api-token';
|
||||
import { ADMIN_TOKEN_USER, IApiUser } from '../../types';
|
||||
import { createTestConfig } from '../../../test/config/test-config';
|
||||
import { createFakeEventsService } from '..';
|
||||
import { ApiTokenType } from '../../types/models/api-token';
|
||||
|
||||
test('when using an admin token should get the username of the token and the id from internalAdminTokenUserId', async () => {
|
||||
const adminToken: IApiUser = {
|
137
src/lib/features/events/event-service.ts
Normal file
137
src/lib/features/events/event-service.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IFeatureTagStore, IUnleashStores } from '../../types/stores';
|
||||
import { Logger } from '../../logger';
|
||||
import { IEventStore } from '../../types/stores/event-store';
|
||||
import { IBaseEvent, IEventList, IUserEvent } from '../../types/events';
|
||||
import { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
|
||||
import EventEmitter from 'events';
|
||||
import { IApiUser, ITag, IUser } from '../../types';
|
||||
import { ApiTokenType } from '../../types/models/api-token';
|
||||
import {
|
||||
extractUserIdFromUser,
|
||||
extractUsernameFromUser,
|
||||
} from '../../util/extract-user';
|
||||
|
||||
export default class EventService {
|
||||
private logger: Logger;
|
||||
|
||||
private eventStore: IEventStore;
|
||||
|
||||
private featureTagStore: IFeatureTagStore;
|
||||
|
||||
constructor(
|
||||
{
|
||||
eventStore,
|
||||
featureTagStore,
|
||||
}: Pick<IUnleashStores, 'eventStore' | 'featureTagStore'>,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||
) {
|
||||
this.logger = getLogger('services/event-service.ts');
|
||||
this.eventStore = eventStore;
|
||||
this.featureTagStore = featureTagStore;
|
||||
}
|
||||
|
||||
async getEvents(): Promise<IEventList> {
|
||||
const totalEvents = await this.eventStore.count();
|
||||
const events = await this.eventStore.getEvents();
|
||||
return {
|
||||
events,
|
||||
totalEvents,
|
||||
};
|
||||
}
|
||||
|
||||
async searchEvents(search: SearchEventsSchema): Promise<IEventList> {
|
||||
const totalEvents = await this.eventStore.filteredCount(search);
|
||||
const events = await this.eventStore.searchEvents(search);
|
||||
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;
|
||||
}
|
||||
|
||||
getUserDetails(user: IUser | IApiUser): {
|
||||
createdBy: string;
|
||||
createdByUserId: number;
|
||||
} {
|
||||
return {
|
||||
createdBy: extractUsernameFromUser(user),
|
||||
createdByUserId: extractUserIdFromUser(user),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use storeUserEvent instead
|
||||
*/
|
||||
async storeEvent(event: IBaseEvent): Promise<void> {
|
||||
return this.storeEvents([event]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use storeUserEvents instead
|
||||
*/
|
||||
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 storeUserEvent(event: IUserEvent): Promise<void> {
|
||||
return this.storeUserEvents([event]);
|
||||
}
|
||||
|
||||
async storeUserEvents(events: IUserEvent[]): Promise<void> {
|
||||
let enhancedEvents = events.map(({ byUser, ...event }) => {
|
||||
return {
|
||||
...event,
|
||||
...this.getUserDetails(byUser),
|
||||
};
|
||||
});
|
||||
for (const enhancer of [this.enhanceEventsWithTags.bind(this)]) {
|
||||
enhancedEvents = await enhancer(enhancedEvents);
|
||||
}
|
||||
return this.eventStore.batchStore(enhancedEvents);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import knex from 'knex';
|
||||
import EventStore from './event-store';
|
||||
import getLogger from '../../test/fixtures/no-logger';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import { subHours, formatRFC3339 } from 'date-fns';
|
||||
import dbInit from '../../test/e2e/helpers/database-init';
|
||||
import dbInit from '../../../test/e2e/helpers/database-init';
|
||||
|
||||
beforeAll(() => {
|
||||
getLogger.setMuteError(true);
|
433
src/lib/features/events/event-store.ts
Normal file
433
src/lib/features/events/event-store.ts
Normal file
@ -0,0 +1,433 @@
|
||||
import {
|
||||
IEvent,
|
||||
IBaseEvent,
|
||||
SEGMENT_UPDATED,
|
||||
FEATURE_IMPORT,
|
||||
FEATURES_IMPORTED,
|
||||
IEventType,
|
||||
} from '../../types/events';
|
||||
import { LogProvider, Logger } from '../../logger';
|
||||
import { IEventStore } from '../../types/stores/event-store';
|
||||
import { ITag } from '../../types/model';
|
||||
import { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
|
||||
import { sharedEventEmitter } from '../../util/anyEventEmitter';
|
||||
import { Db } from '../../db/db';
|
||||
import { Knex } from 'knex';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
const EVENT_COLUMNS = [
|
||||
'id',
|
||||
'type',
|
||||
'created_by',
|
||||
'created_at',
|
||||
'created_by_user_id',
|
||||
'data',
|
||||
'pre_data',
|
||||
'tags',
|
||||
'feature_name',
|
||||
'project',
|
||||
'environment',
|
||||
] as const;
|
||||
|
||||
export type IQueryOperations =
|
||||
| IWhereOperation
|
||||
| IBeforeDateOperation
|
||||
| IBetweenDatesOperation
|
||||
| IForFeaturesOperation;
|
||||
|
||||
interface IWhereOperation {
|
||||
op: 'where';
|
||||
parameters: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IBeforeDateOperation {
|
||||
op: 'beforeDate';
|
||||
parameters: {
|
||||
dateAccessor: string;
|
||||
date: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IBetweenDatesOperation {
|
||||
op: 'betweenDate';
|
||||
parameters: {
|
||||
dateAccessor: string;
|
||||
range: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IForFeaturesOperation {
|
||||
op: 'forFeatures';
|
||||
parameters: IForFeaturesParams;
|
||||
}
|
||||
|
||||
interface IForFeaturesParams {
|
||||
type: string;
|
||||
projectId: string;
|
||||
environments: string[];
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export interface IEventTable {
|
||||
id: number;
|
||||
type: string;
|
||||
created_by: string;
|
||||
created_at: Date;
|
||||
created_by_user_id: number;
|
||||
data?: any;
|
||||
pre_data?: any;
|
||||
feature_name?: string;
|
||||
project?: string;
|
||||
environment?: string;
|
||||
tags: ITag[];
|
||||
}
|
||||
|
||||
const TABLE = 'events';
|
||||
|
||||
class EventStore implements IEventStore {
|
||||
private db: Db;
|
||||
|
||||
// only one shared event emitter should exist across all event store instances
|
||||
private eventEmitter: EventEmitter = sharedEventEmitter;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
// a new DB has to be injected per transaction
|
||||
constructor(db: Db, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('event-store');
|
||||
}
|
||||
|
||||
async store(event: IBaseEvent): Promise<void> {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.insert(this.eventToDbRow(event))
|
||||
.returning(EVENT_COLUMNS);
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn(`Failed to store "${event.type}" event: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
const count = await this.db(TABLE)
|
||||
.count<Record<string, number>>()
|
||||
.first();
|
||||
if (!count) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof count.count === 'string') {
|
||||
return parseInt(count.count, 10);
|
||||
} else {
|
||||
return count.count;
|
||||
}
|
||||
}
|
||||
|
||||
async filteredCount(eventSearch: SearchEventsSchema): Promise<number> {
|
||||
let query = this.db(TABLE);
|
||||
if (eventSearch.type) {
|
||||
query = query.andWhere({ type: eventSearch.type });
|
||||
}
|
||||
if (eventSearch.project) {
|
||||
query = query.andWhere({ project: eventSearch.project });
|
||||
}
|
||||
if (eventSearch.feature) {
|
||||
query = query.andWhere({ feature_name: eventSearch.feature });
|
||||
}
|
||||
const count = await query.count().first();
|
||||
if (!count) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof count.count === 'string') {
|
||||
return parseInt(count.count, 10);
|
||||
} else {
|
||||
return count.count;
|
||||
}
|
||||
}
|
||||
|
||||
async batchStore(events: IBaseEvent[]): Promise<void> {
|
||||
try {
|
||||
await this.db(TABLE).insert(events.map(this.eventToDbRow));
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn(`Failed to store events: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getMaxRevisionId(largerThan: number = 0): Promise<number> {
|
||||
const row = await this.db(TABLE)
|
||||
.max('id')
|
||||
.where((builder) =>
|
||||
builder
|
||||
.whereNotNull('feature_name')
|
||||
.orWhereIn('type', [
|
||||
SEGMENT_UPDATED,
|
||||
FEATURE_IMPORT,
|
||||
FEATURES_IMPORTED,
|
||||
]),
|
||||
)
|
||||
.andWhere('id', '>=', largerThan)
|
||||
.first();
|
||||
return row?.max ?? 0;
|
||||
}
|
||||
|
||||
async delete(key: number): Promise<void> {
|
||||
await this.db(TABLE).where({ id: key }).del();
|
||||
}
|
||||
|
||||
async deleteAll(): Promise<void> {
|
||||
await this.db(TABLE).del();
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
async exists(key: number): Promise<boolean> {
|
||||
const result = await this.db.raw(
|
||||
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`,
|
||||
[key],
|
||||
);
|
||||
const { present } = result.rows[0];
|
||||
return present;
|
||||
}
|
||||
|
||||
async query(operations: IQueryOperations[]): Promise<IEvent[]> {
|
||||
try {
|
||||
let query: Knex.QueryBuilder = this.select();
|
||||
|
||||
operations.forEach((operation) => {
|
||||
if (operation.op === 'where') {
|
||||
query = this.where(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'forFeatures') {
|
||||
query = this.forFeatures(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'beforeDate') {
|
||||
query = this.beforeDate(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'betweenDate') {
|
||||
query = this.betweenDate(query, operation.parameters);
|
||||
}
|
||||
});
|
||||
|
||||
const rows = await query;
|
||||
return rows.map(this.rowToEvent);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async queryCount(operations: IQueryOperations[]): Promise<number> {
|
||||
try {
|
||||
let query: Knex.QueryBuilder = this.db.count().from(TABLE);
|
||||
|
||||
operations.forEach((operation) => {
|
||||
if (operation.op === 'where') {
|
||||
query = this.where(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'forFeatures') {
|
||||
query = this.forFeatures(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'beforeDate') {
|
||||
query = this.beforeDate(query, operation.parameters);
|
||||
}
|
||||
|
||||
if (operation.op === 'betweenDate') {
|
||||
query = this.betweenDate(query, operation.parameters);
|
||||
}
|
||||
});
|
||||
|
||||
const queryResult = await query.first();
|
||||
return parseInt(queryResult.count || 0);
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
where(
|
||||
query: Knex.QueryBuilder,
|
||||
parameters: { [key: string]: string },
|
||||
): Knex.QueryBuilder {
|
||||
return query.where(parameters);
|
||||
}
|
||||
|
||||
beforeDate(
|
||||
query: Knex.QueryBuilder,
|
||||
parameters: { dateAccessor: string; date: string },
|
||||
): Knex.QueryBuilder {
|
||||
return query.andWhere(parameters.dateAccessor, '>=', parameters.date);
|
||||
}
|
||||
|
||||
betweenDate(
|
||||
query: Knex.QueryBuilder,
|
||||
parameters: { dateAccessor: string; range: string[] },
|
||||
): Knex.QueryBuilder {
|
||||
if (parameters.range && parameters.range.length === 2) {
|
||||
return query.andWhereBetween(parameters.dateAccessor, [
|
||||
parameters.range[0],
|
||||
parameters.range[1],
|
||||
]);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
select(): Knex.QueryBuilder {
|
||||
return this.db.select(EVENT_COLUMNS).from(TABLE);
|
||||
}
|
||||
|
||||
forFeatures(
|
||||
query: Knex.QueryBuilder,
|
||||
parameters: IForFeaturesParams,
|
||||
): Knex.QueryBuilder {
|
||||
return query
|
||||
.where({ type: parameters.type, project: parameters.projectId })
|
||||
.whereIn('feature_name', parameters.features)
|
||||
.whereIn('environment', parameters.environments);
|
||||
}
|
||||
|
||||
async get(key: number): Promise<IEvent> {
|
||||
const row = await this.db(TABLE).where({ id: key }).first();
|
||||
return this.rowToEvent(row);
|
||||
}
|
||||
|
||||
async getAll(query?: Object): Promise<IEvent[]> {
|
||||
return this.getEvents(query);
|
||||
}
|
||||
|
||||
async getEvents(query?: Object): Promise<IEvent[]> {
|
||||
try {
|
||||
let qB = this.db
|
||||
.select(EVENT_COLUMNS)
|
||||
.from(TABLE)
|
||||
.limit(100)
|
||||
.orderBy('created_at', 'desc');
|
||||
if (query) {
|
||||
qB = qB.where(query);
|
||||
}
|
||||
const rows = await qB;
|
||||
return rows.map(this.rowToEvent);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async searchEvents(search: SearchEventsSchema = {}): Promise<IEvent[]> {
|
||||
let query = this.db
|
||||
.select(EVENT_COLUMNS)
|
||||
.from<IEventTable>(TABLE)
|
||||
.limit(search.limit ?? 100)
|
||||
.offset(search.offset ?? 0)
|
||||
.orderBy('created_at', 'desc');
|
||||
|
||||
if (search.type) {
|
||||
query = query.andWhere({
|
||||
type: search.type,
|
||||
});
|
||||
}
|
||||
|
||||
if (search.project) {
|
||||
query = query.andWhere({
|
||||
project: search.project,
|
||||
});
|
||||
}
|
||||
|
||||
if (search.feature) {
|
||||
query = query.andWhere({
|
||||
feature_name: search.feature,
|
||||
});
|
||||
}
|
||||
|
||||
if (search.query) {
|
||||
query = query.where((where) =>
|
||||
where
|
||||
.orWhereRaw('type::text ILIKE ?', `%${search.query}%`)
|
||||
.orWhereRaw('created_by::text ILIKE ?', `%${search.query}%`)
|
||||
.orWhereRaw('data::text ILIKE ?', `%${search.query}%`)
|
||||
.orWhereRaw('tags::text ILIKE ?', `%${search.query}%`)
|
||||
.orWhereRaw('pre_data::text ILIKE ?', `%${search.query}%`),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return (await query).map(this.rowToEvent);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
rowToEvent(row: IEventTable): IEvent {
|
||||
return {
|
||||
id: row.id,
|
||||
type: row.type as IEventType,
|
||||
createdBy: row.created_by,
|
||||
createdAt: row.created_at,
|
||||
createdByUserId: row.created_by_user_id,
|
||||
data: row.data,
|
||||
preData: row.pre_data,
|
||||
tags: row.tags || [],
|
||||
featureName: row.feature_name,
|
||||
project: row.project,
|
||||
environment: row.environment,
|
||||
};
|
||||
}
|
||||
|
||||
eventToDbRow(e: IBaseEvent): Omit<IEventTable, 'id' | 'created_at'> {
|
||||
return {
|
||||
type: e.type,
|
||||
created_by: e.createdBy ?? 'admin',
|
||||
created_by_user_id: e.createdByUserId,
|
||||
data: Array.isArray(e.data) ? JSON.stringify(e.data) : e.data,
|
||||
pre_data: Array.isArray(e.preData)
|
||||
? JSON.stringify(e.preData)
|
||||
: e.preData,
|
||||
// @ts-expect-error workaround for json-array
|
||||
tags: JSON.stringify(e.tags),
|
||||
feature_name: e.featureName,
|
||||
project: e.project,
|
||||
environment: e.environment,
|
||||
};
|
||||
}
|
||||
|
||||
setMaxListeners(number: number): EventEmitter {
|
||||
return this.eventEmitter.setMaxListeners(number);
|
||||
}
|
||||
|
||||
on(
|
||||
eventName: string | symbol,
|
||||
listener: (...args: any[]) => void,
|
||||
): EventEmitter {
|
||||
return this.eventEmitter.on(eventName, listener);
|
||||
}
|
||||
|
||||
emit(eventName: string | symbol, ...args: any[]): boolean {
|
||||
return this.eventEmitter.emit(eventName, ...args);
|
||||
}
|
||||
|
||||
off(
|
||||
eventName: string | symbol,
|
||||
listener: (...args: any[]) => void,
|
||||
): EventEmitter {
|
||||
return this.eventEmitter.off(eventName, listener);
|
||||
}
|
||||
|
||||
async setUnannouncedToAnnounced(): Promise<IEvent[]> {
|
||||
const rows = await this.db(TABLE)
|
||||
.update({ announced: true })
|
||||
.where('announced', false)
|
||||
.returning(EVENT_COLUMNS);
|
||||
return rows.map(this.rowToEvent);
|
||||
}
|
||||
|
||||
async publishUnannouncedEvents(): Promise<void> {
|
||||
const events = await this.setUnannouncedToAnnounced();
|
||||
|
||||
events.forEach((e) => this.eventEmitter.emit(e.type, e));
|
||||
}
|
||||
}
|
||||
|
||||
export default EventStore;
|
@ -38,7 +38,7 @@ import FakeEventStore from '../../../test/fixtures/fake-event-store';
|
||||
import FakeFeatureStrategiesStore from '../feature-toggle/fakes/fake-feature-strategies-store';
|
||||
import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store';
|
||||
import FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store';
|
||||
import EventStore from '../../db/event-store';
|
||||
import EventStore from '../events/event-store';
|
||||
import {
|
||||
createFakePrivateProjectChecker,
|
||||
createPrivateProjectChecker,
|
||||
|
@ -101,7 +101,7 @@ import { IChangeRequestAccessReadModel } from '../change-request-access-service/
|
||||
import { checkFeatureFlagNamesAgainstPattern } from '../feature-naming-pattern/feature-naming-validation';
|
||||
import { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
|
||||
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
|
||||
import EventService from '../../services/event-service';
|
||||
import EventService from '../events/event-service';
|
||||
import { DependentFeaturesService } from '../dependent-features/dependent-features-service';
|
||||
import { FeatureToggleInsert } from './feature-toggle-store';
|
||||
|
||||
|
@ -3,7 +3,7 @@ import MaintenanceService from './maintenance-service';
|
||||
import SettingService from '../../services/setting-service';
|
||||
import { createTestConfig } from '../../../test/config/test-config';
|
||||
import FakeSettingStore from '../../../test/fixtures/fake-setting-store';
|
||||
import EventService from '../../services/event-service';
|
||||
import EventService from '../events/event-service';
|
||||
|
||||
test('Scheduler should run scheduled functions if maintenance mode is off', async () => {
|
||||
const config = createTestConfig();
|
||||
|
@ -21,7 +21,7 @@ import { IProjectStore } from '../../types/stores/project-store';
|
||||
import MinimumOneEnvironmentError from '../../error/minimum-one-environment-error';
|
||||
import { IFlagResolver } from '../../types/experimental';
|
||||
import { CreateFeatureStrategySchema } from '../../openapi';
|
||||
import EventService from '../../services/event-service';
|
||||
import EventService from '../events/event-service';
|
||||
|
||||
export default class EnvironmentService {
|
||||
private logger: Logger;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Db, IUnleashConfig } from '../../server-impl';
|
||||
import EventStore from '../../db/event-store';
|
||||
import EventStore from '../events/event-store';
|
||||
import GroupStore from '../../db/group-store';
|
||||
import { AccountStore } from '../../db/account-store';
|
||||
import EnvironmentStore from '../project-environments/environment-store';
|
||||
|
@ -4,7 +4,7 @@ import MaintenanceService from '../maintenance/maintenance-service';
|
||||
import { createTestConfig } from '../../../test/config/test-config';
|
||||
import SettingService from '../../services/setting-service';
|
||||
import FakeSettingStore from '../../../test/fixtures/fake-setting-store';
|
||||
import EventService from '../../services/event-service';
|
||||
import EventService from '../events/event-service';
|
||||
import { SCHEDULER_JOB_TIME } from '../../metric-events';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
import { Logger } from '../../logger';
|
||||
import { ITagType, ITagTypeStore } from './tag-type-store-type';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import EventService from '../../services/event-service';
|
||||
import EventService from '../events/event-service';
|
||||
import { SYSTEM_USER } from '../../types';
|
||||
|
||||
export default class TagTypeService {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import EventService from '../../services/event-service';
|
||||
import EventService from '../../features/events/event-service';
|
||||
import { ADMIN, NONE } from '../../types/permissions';
|
||||
import { IEvent, IEventList } from '../../types/events';
|
||||
import Controller from '../controller';
|
||||
|
@ -48,7 +48,7 @@ import {
|
||||
ROLE_UPDATED,
|
||||
SYSTEM_USER,
|
||||
} from '../types';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
const { ADMIN } = permissions;
|
||||
|
||||
|
@ -14,7 +14,7 @@ import AddonService from './addon-service';
|
||||
import { IAddonDto } from '../types/stores/addon-store';
|
||||
import SimpleAddon from './addon-service-test-simple-addon';
|
||||
import { IAddonProviders } from '../addons';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import { SYSTEM_USER } from '../types';
|
||||
|
||||
const MASKED_VALUE = '*****';
|
||||
|
@ -11,7 +11,7 @@ import { IAddon, IAddonDto, IAddonStore } from '../types/stores/addon-store';
|
||||
import { IUnleashStores, IUnleashConfig, SYSTEM_USER } from '../types';
|
||||
import { IAddonDefinition } from '../types/model';
|
||||
import { minutesToMilliseconds } from 'date-fns';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import { omitKeys } from '../util';
|
||||
|
||||
const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]);
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
API_TOKEN_UPDATED,
|
||||
} from '../types';
|
||||
import { addDays } from 'date-fns';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
|
||||
import { createFakeEventsService } from '../../lib/features';
|
||||
|
||||
|
@ -33,7 +33,7 @@ import {
|
||||
extractUsernameFromUser,
|
||||
omitKeys,
|
||||
} from '../util';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
const resolveTokenPermissions = (tokenType: string) => {
|
||||
if (tokenType === ApiTokenType.ADMIN) {
|
||||
|
@ -10,7 +10,7 @@ import { IUnleashConfig } from '../types/option';
|
||||
import { ContextFieldStrategiesSchema } from '../openapi/spec/context-field-strategies-schema';
|
||||
import { IFeatureStrategy, IFlagResolver } from '../types';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
const { contextSchema, nameSchema } = require('./context-schema');
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
|
@ -1,141 +1,4 @@
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import { IFeatureTagStore, IUnleashStores } from '../types/stores';
|
||||
import { Logger } from '../logger';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { IBaseEvent, IEventList, IUserEvent } from '../types/events';
|
||||
import { SearchEventsSchema } from '../openapi/spec/search-events-schema';
|
||||
import EventEmitter from 'events';
|
||||
import { ADMIN_TOKEN_USER, IApiUser, ITag, IUser, SYSTEM_USER } from '../types';
|
||||
import { ApiTokenType } from '../../lib/types/models/api-token';
|
||||
|
||||
export default class EventService {
|
||||
private logger: Logger;
|
||||
|
||||
private eventStore: IEventStore;
|
||||
|
||||
private featureTagStore: IFeatureTagStore;
|
||||
|
||||
constructor(
|
||||
{
|
||||
eventStore,
|
||||
featureTagStore,
|
||||
}: Pick<IUnleashStores, 'eventStore' | 'featureTagStore'>,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||
) {
|
||||
this.logger = getLogger('services/event-service.ts');
|
||||
this.eventStore = eventStore;
|
||||
this.featureTagStore = featureTagStore;
|
||||
}
|
||||
|
||||
async getEvents(): Promise<IEventList> {
|
||||
const totalEvents = await this.eventStore.count();
|
||||
const events = await this.eventStore.getEvents();
|
||||
return {
|
||||
events,
|
||||
totalEvents,
|
||||
};
|
||||
}
|
||||
|
||||
async searchEvents(search: SearchEventsSchema): Promise<IEventList> {
|
||||
const totalEvents = await this.eventStore.filteredCount(search);
|
||||
const events = await this.eventStore.searchEvents(search);
|
||||
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;
|
||||
}
|
||||
|
||||
getUserDetails(user: IUser | IApiUser): {
|
||||
createdBy: string;
|
||||
createdByUserId: number;
|
||||
} {
|
||||
return {
|
||||
createdBy:
|
||||
(user as IUser)?.email ||
|
||||
user?.username ||
|
||||
(this.isAdminToken(user)
|
||||
? ADMIN_TOKEN_USER.username
|
||||
: SYSTEM_USER.username),
|
||||
createdByUserId:
|
||||
(user as IUser)?.id ||
|
||||
(user as IApiUser)?.internalAdminTokenUserId ||
|
||||
SYSTEM_USER.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use storeUserEvent instead
|
||||
*/
|
||||
async storeEvent(event: IBaseEvent): Promise<void> {
|
||||
return this.storeEvents([event]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use storeUserEvents instead
|
||||
*/
|
||||
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 storeUserEvent(event: IUserEvent): Promise<void> {
|
||||
return this.storeUserEvents([event]);
|
||||
}
|
||||
|
||||
async storeUserEvents(events: IUserEvent[]): Promise<void> {
|
||||
let enhancedEvents = events.map(({ byUser, ...event }) => {
|
||||
return {
|
||||
...event,
|
||||
...this.getUserDetails(byUser),
|
||||
};
|
||||
});
|
||||
for (const enhancer of [this.enhanceEventsWithTags.bind(this)]) {
|
||||
enhancedEvents = await enhancer(enhancedEvents);
|
||||
}
|
||||
return this.eventStore.batchStore(enhancedEvents);
|
||||
}
|
||||
}
|
||||
import EventService from '../features/events/event-service';
|
||||
// For backward compatibility
|
||||
export * from '../features/events/event-service';
|
||||
export default EventService;
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
import { IUser } from '../types/user';
|
||||
import { extractUsernameFromUser } from '../util';
|
||||
import { IFavoriteProjectKey } from '../types/stores/favorite-projects';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
export interface IFavoriteFeatureProps {
|
||||
feature: string;
|
||||
|
@ -11,7 +11,7 @@ import { IChangeRequestAccessReadModel } from '../features/change-request-access
|
||||
import { ISegmentService } from '../segments/segment-service-interface';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import { IDependentFeaturesReadModel } from '../features/dependent-features/dependent-features-read-model-type';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
|
||||
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
import { ITagStore } from '../types/stores/tag-store';
|
||||
import { ITag } from '../types/model';
|
||||
import { BadDataError, FOREIGN_KEY_VIOLATION } from '../../lib/error';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
class FeatureTagService {
|
||||
private tagStore: ITagStore;
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
IFeatureTypeStore,
|
||||
} from '../types/stores/feature-type-store';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import { FEATURE_TYPE_UPDATED, IUser } from '../types';
|
||||
import { extractUsernameFromUser } from '../util';
|
||||
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import { IAccountStore } from '../types/stores/account-store';
|
||||
import { IUser } from '../types/user';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
export class GroupService {
|
||||
private groupStore: IGroupStore;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IUnleashConfig, IUnleashStores, IUnleashServices } from '../types';
|
||||
import FeatureTypeService from './feature-type-service';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import HealthService from './health-service';
|
||||
|
||||
import ProjectService from './project-service';
|
||||
|
@ -9,7 +9,7 @@ import BadDataError from '../error/bad-data-error';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import { OperationDeniedError } from '../error/operation-denied-error';
|
||||
import { PAT_LIMIT } from '../util/constants';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
export default class PatService {
|
||||
private config: IUnleashConfig;
|
||||
|
@ -66,7 +66,7 @@ import { BadDataError, PermissionError } from '../error';
|
||||
import { ProjectDoraMetricsSchema } from '../openapi';
|
||||
import { checkFeatureNamingData } from '../features/feature-naming-pattern/feature-naming-validation';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||
|
||||
|
@ -17,7 +17,7 @@ import UserService from './user-service';
|
||||
import { IUser } from '../types/user';
|
||||
import { URL } from 'url';
|
||||
import { add } from 'date-fns';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
export class PublicSignupTokenService {
|
||||
private store: IPublicSignupTokenStore;
|
||||
|
@ -3,7 +3,7 @@ import { SchedulerService } from '../features/scheduler/scheduler-service';
|
||||
import { createTestConfig } from '../../test/config/test-config';
|
||||
import FakeSettingStore from '../../test/fixtures/fake-setting-store';
|
||||
import SettingService from './setting-service';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import MaintenanceService from '../features/maintenance/maintenance-service';
|
||||
|
||||
function ms(timeMs: number) {
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
import { PermissionError } from '../error';
|
||||
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import { IChangeRequestSegmentUsageReadModel } from '../features/change-request-segment-usage-service/change-request-segment-usage-read-model';
|
||||
|
||||
export class SegmentService implements ISegmentService {
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
SettingDeletedEvent,
|
||||
SettingUpdatedEvent,
|
||||
} from '../types/events';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
export default class SettingService {
|
||||
private config: IUnleashConfig;
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
} from '../types/events';
|
||||
import { GLOBAL_ENV } from '../types/environment';
|
||||
import variantsExportV3 from '../../test/examples/variantsexport_v3.json';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import { SYSTEM_USER_ID } from '../types';
|
||||
const oldExportExample = require('./state-service-export-v1.json');
|
||||
const TESTUSERID = 3333;
|
||||
|
@ -52,7 +52,7 @@ import { DEFAULT_ENV } from '../util/constants';
|
||||
import { GLOBAL_ENV } from '../types/environment';
|
||||
import { ISegmentStore } from '../types/stores/segment-store';
|
||||
import { PartialSome } from '../types/partial';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
export interface IBackupOption {
|
||||
includeFeatureToggles: boolean;
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
IStrategyStore,
|
||||
} from '../types/stores/strategy-store';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
const strategySchema = require('./strategy-schema');
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
|
@ -6,7 +6,7 @@ import { IUnleashStores } from '../types/stores';
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import { ITagStore } from '../types/stores/tag-store';
|
||||
import { ITag } from '../types/model';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
export default class TagService {
|
||||
private tagStore: ITagStore;
|
||||
|
@ -14,7 +14,7 @@ import User from '../types/user';
|
||||
import FakeResetTokenStore from '../../test/fixtures/fake-reset-token-store';
|
||||
import SettingService from './setting-service';
|
||||
import FakeSettingStore from '../../test/fixtures/fake-setting-store';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
|
||||
|
||||
const config: IUnleashConfig = createTestConfig();
|
||||
|
@ -31,7 +31,7 @@ import BadDataError from '../error/bad-data-error';
|
||||
import { isDefined } from '../util/isDefined';
|
||||
import { TokenUserSchema } from '../openapi/spec/token-user-schema';
|
||||
import PasswordMismatch from '../error/password-mismatch';
|
||||
import EventService from './event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
|
||||
const systemUser = new User({ id: -1, username: 'system' });
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { EmailService } from '../services/email-service';
|
||||
import UserService from '../services/user-service';
|
||||
import ResetTokenService from '../services/reset-token-service';
|
||||
import FeatureTypeService from '../services/feature-type-service';
|
||||
import EventService from '../services/event-service';
|
||||
import EventService from '../features/events/event-service';
|
||||
import HealthService from '../services/health-service';
|
||||
import SettingService from '../services/setting-service';
|
||||
import SessionService from '../services/session-service';
|
||||
|
@ -2,7 +2,7 @@ import { IBaseEvent, IEvent } from '../events';
|
||||
import { Store } from './store';
|
||||
import { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
|
||||
import EventEmitter from 'events';
|
||||
import { IQueryOperations } from '../../db/event-store';
|
||||
import { IQueryOperations } from '../../features/events/event-store';
|
||||
|
||||
export interface IEventStore
|
||||
extends Store<IEvent, number>,
|
||||
|
2
src/test/fixtures/fake-event-store.ts
vendored
2
src/test/fixtures/fake-event-store.ts
vendored
@ -1,7 +1,7 @@
|
||||
import { IEventStore } from '../../lib/types/stores/event-store';
|
||||
import { IEvent } from '../../lib/types/events';
|
||||
import { sharedEventEmitter } from '../../lib/util/anyEventEmitter';
|
||||
import { IQueryOperations } from '../../lib/db/event-store';
|
||||
import { IQueryOperations } from '../../lib/features/events/event-store';
|
||||
import { SearchEventsSchema } from '../../lib/openapi';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user