From 68d4ac025292d2d5cde76556641a392f79f61682 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 15 Sep 2021 15:00:14 +0200 Subject: [PATCH] feat: add project and environment columns to events --- src/lib/db/event-store.ts | 21 ++++++++++ src/lib/routes/admin-api/event.ts | 9 ++++- src/lib/services/event-service.ts | 4 ++ src/lib/services/feature-toggle-service-v2.ts | 9 ++++- src/lib/services/project-service.ts | 5 ++- src/lib/types/model.ts | 2 + src/lib/types/stores/event-store.ts | 1 + ...oject-and-environment-columns-to-events.js | 29 ++++++++++++++ src/test/e2e/api/admin/event.e2e.test.ts | 38 +++++++++++++++++-- src/test/fixtures/fake-event-store.ts | 4 ++ 10 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 src/migrations/20210915122001-add-project-and-environment-columns-to-events.js diff --git a/src/lib/db/event-store.ts b/src/lib/db/event-store.ts index 8ef3c8800f..918c6f87a7 100644 --- a/src/lib/db/event-store.ts +++ b/src/lib/db/event-store.ts @@ -12,6 +12,8 @@ const EVENT_COLUMNS = [ 'created_at', 'data', 'tags', + 'project', + 'environment', ]; export interface IEventTable { @@ -20,6 +22,8 @@ export interface IEventTable { created_by: string; created_at: Date; data: any; + project?: string; + environment?: string; tags: []; } @@ -126,6 +130,19 @@ class EventStore extends EventEmitter implements IEventStore { } } + async getEventsFilterByProject(project: string): Promise { + try { + const rows = await this.db + .select(EVENT_COLUMNS) + .from(TABLE) + .where({ project }) + .orderBy('created_at', 'desc'); + return rows.map(this.rowToEvent); + } catch (err) { + return []; + } + } + rowToEvent(row: IEventTable): IEvent { return { id: row.id, @@ -134,6 +151,8 @@ class EventStore extends EventEmitter implements IEventStore { createdAt: row.created_at, data: row.data, tags: row.tags || [], + project: row.project, + environment: row.environment, }; } @@ -143,6 +162,8 @@ class EventStore extends EventEmitter implements IEventStore { created_by: e.createdBy, data: e.data, tags: JSON.stringify(e.tags), + project: e.project, + environment: e.environment, }; } } diff --git a/src/lib/routes/admin-api/event.ts b/src/lib/routes/admin-api/event.ts index e2baf5bc78..247ad788d5 100644 --- a/src/lib/routes/admin-api/event.ts +++ b/src/lib/routes/admin-api/event.ts @@ -24,7 +24,14 @@ export default class EventController extends Controller { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async getEvents(req, res): Promise { - const events = await this.eventService.getEvents(); + let events; + if (req.query?.project) { + events = await this.eventService.getEventsForProject( + req.query.project, + ); + } else { + events = await this.eventService.getEvents(); + } eventDiffer.addDiffs(events); res.json({ version, events }); } diff --git a/src/lib/services/event-service.ts b/src/lib/services/event-service.ts index 8bf3e75308..7ebbb8e184 100644 --- a/src/lib/services/event-service.ts +++ b/src/lib/services/event-service.ts @@ -28,6 +28,10 @@ export default class EventService { (e: IEvent) => e.type !== FEATURE_METADATA_UPDATED, ); } + + async getEventsForProject(project: string): Promise { + return this.eventStore.getEventsFilterByProject(project); + } } module.exports = EventService; diff --git a/src/lib/services/feature-toggle-service-v2.ts b/src/lib/services/feature-toggle-service-v2.ts index a7d7d816d4..c38ff518a7 100644 --- a/src/lib/services/feature-toggle-service-v2.ts +++ b/src/lib/services/feature-toggle-service-v2.ts @@ -309,6 +309,7 @@ class FeatureToggleServiceV2 { await this.eventStore.store({ type: FEATURE_CREATED, createdBy: userName, + project: projectId, data, }); @@ -341,6 +342,7 @@ class FeatureToggleServiceV2 { type: FEATURE_METADATA_UPDATED, createdBy: userName, data: featureToggle, + project: projectId, tags, }); return featureToggle; @@ -455,12 +457,13 @@ class FeatureToggleServiceV2 { createdBy: userName, data, tags, + project: feature.project, }); return feature; } async archiveToggle(name: string, userName: string): Promise { - await this.featureToggleStore.get(name); + const feature = await this.featureToggleStore.get(name); await this.featureToggleStore.archive(name); const tags = (await this.featureTagStore.getAllTagsForFeature(name)) || []; @@ -468,6 +471,7 @@ class FeatureToggleServiceV2 { type: FEATURE_ARCHIVED, createdBy: userName, data: { name }, + project: feature.project, tags, }); } @@ -514,6 +518,7 @@ class FeatureToggleServiceV2 { createdBy: userName, data, tags, + project: projectId, }); return feature; } @@ -583,6 +588,7 @@ class FeatureToggleServiceV2 { type: event || FEATURE_UPDATED, createdBy: userName, data, + project: data.project, tags, }); return feature; @@ -612,6 +618,7 @@ class FeatureToggleServiceV2 { type: FEATURE_REVIVED, createdBy: userName, data, + project: data.project, tags, }); } diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 59fd21b7f5..1ec6c8777d 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -131,6 +131,7 @@ export default class ProjectService { type: PROJECT_CREATED, createdBy: getCreatedBy(user), data, + project: newProject.id, }); return data; @@ -146,6 +147,7 @@ export default class ProjectService { type: PROJECT_UPDATED, createdBy: getCreatedBy(user), data: project, + project: project.id, }); } @@ -211,10 +213,11 @@ export default class ProjectService { await this.eventStore.store({ type: PROJECT_DELETED, createdBy: getCreatedBy(user), + project: id, data: { id }, }); - this.accessService.removeDefaultProjectRoles(user, id); + await this.accessService.removeDefaultProjectRoles(user, id); } async validateId(id: string): Promise { diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 67d0d6c38b..25a218509e 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -197,6 +197,8 @@ export interface IAddonConfig { export interface ICreateEvent { type: string; createdBy: string; + project?: string; + environment?: string; data?: any; tags?: ITag[]; } diff --git a/src/lib/types/stores/event-store.ts b/src/lib/types/stores/event-store.ts index 3f3965ac31..e537c59bc1 100644 --- a/src/lib/types/stores/event-store.ts +++ b/src/lib/types/stores/event-store.ts @@ -7,4 +7,5 @@ export interface IEventStore extends Store, EventEmitter { batchStore(events: ICreateEvent[]): Promise; getEvents(): Promise; getEventsFilterByType(name: string): Promise; + getEventsFilterByProject(project: string): Promise; } diff --git a/src/migrations/20210915122001-add-project-and-environment-columns-to-events.js b/src/migrations/20210915122001-add-project-and-environment-columns-to-events.js new file mode 100644 index 0000000000..abe5713530 --- /dev/null +++ b/src/migrations/20210915122001-add-project-and-environment-columns-to-events.js @@ -0,0 +1,29 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + ALTER TABLE events + ADD COLUMN project TEXT; + ALTER TABLE events + ADD COLUMN environment TEXT; + CREATE INDEX events_project_idx ON events(project); + CREATE INDEX events_environment_idx ON events(environment); + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + DROP INDEX events_environment_idx; + DROP INDEX events_project_idx; + ALTER TABLE events + DROP COLUMN environment; + ALTER TABLE events + DROP COLUMN project; + `, + cb, + ); +}; diff --git a/src/test/e2e/api/admin/event.e2e.test.ts b/src/test/e2e/api/admin/event.e2e.test.ts index ad50354761..bbee66737a 100644 --- a/src/test/e2e/api/admin/event.e2e.test.ts +++ b/src/test/e2e/api/admin/event.e2e.test.ts @@ -1,13 +1,17 @@ -import { setupApp } from '../../helpers/test-helper'; -import dbInit from '../../helpers/database-init'; +import { IUnleashTest, setupApp } from '../../helpers/test-helper'; +import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; +import { FEATURE_CREATED } from '../../../../lib/types/events'; +import { IEventStore } from '../../../../lib/types/stores/event-store'; -let app; -let db; +let app: IUnleashTest; +let db: ITestDb; +let eventStore: IEventStore; beforeAll(async () => { db = await dbInit('event_api_serial', getLogger); app = await setupApp(db.stores); + eventStore = db.stores.eventStore; }); afterAll(async () => { @@ -30,3 +34,29 @@ test('returns events given a name', async () => { .expect('Content-Type', /json/) .expect(200); }); + +test('Can filter by project', async () => { + await eventStore.store({ + type: FEATURE_CREATED, + project: 'something-else', + data: { id: 'some-other-feature' }, + tags: [], + createdBy: 'test-user', + environment: 'test', + }); + await eventStore.store({ + type: FEATURE_CREATED, + project: 'default', + data: { id: 'feature' }, + tags: [], + createdBy: 'test-user', + environment: 'test', + }); + await app.request + .get('/api/admin/events?project=default') + .expect(200) + .expect((res) => { + expect(res.body.events).toHaveLength(1); + expect(res.body.events[0].data.id).toEqual('feature'); + }); +}); diff --git a/src/test/fixtures/fake-event-store.ts b/src/test/fixtures/fake-event-store.ts index e55b47fec9..050233cb2e 100644 --- a/src/test/fixtures/fake-event-store.ts +++ b/src/test/fixtures/fake-event-store.ts @@ -57,6 +57,10 @@ class FakeEventStore extends EventEmitter implements IEventStore { async getEventsFilterByType(type: string): Promise { return this.events.filter((e) => e.type === type); } + + async getEventsFilterByProject(project: string): Promise { + return this.events.filter((e) => e.project === project); + } } module.exports = FakeEventStore;