mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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