diff --git a/src/lib/db/event-store.ts b/src/lib/db/event-store.ts index 2f70e1d21b..4980e19495 100644 --- a/src/lib/db/event-store.ts +++ b/src/lib/db/event-store.ts @@ -121,7 +121,7 @@ class EventStore extends EventEmitter implements IEventStore { .select(EVENT_COLUMNS) .from(TABLE) .limit(100) - .whereRaw("data ->> 'name' = ?", [name]) + .where('type', name) .andWhere( 'id', '>=', diff --git a/src/lib/services/setting-service.ts b/src/lib/services/setting-service.ts index 282b86b944..0b2cf3c030 100644 --- a/src/lib/services/setting-service.ts +++ b/src/lib/services/setting-service.ts @@ -2,30 +2,67 @@ import { IUnleashConfig } from '../types/option'; import { IUnleashStores } from '../types/stores'; import { Logger } from '../logger'; import { ISettingStore } from '../types/stores/settings-store'; +import { IEventStore } from '../types/stores/event-store'; +import { + SettingCreatedEvent, + SettingDeletedEvent, + SettingUpdatedEvent, +} from '../types/events'; export default class SettingService { private logger: Logger; private settingStore: ISettingStore; + private eventStore: IEventStore; + constructor( - { settingStore }: Pick, + { + settingStore, + eventStore, + }: Pick, { getLogger }: Pick, ) { this.logger = getLogger('services/setting-service.ts'); this.settingStore = settingStore; + this.eventStore = eventStore; } async get(id: string): Promise { return this.settingStore.get(id); } - async insert(id: string, value: object): Promise { - return this.settingStore.insert(id, value); + async insert(id: string, value: object, createdBy: string): Promise { + const exists = await this.settingStore.exists(id); + if (exists) { + await this.settingStore.updateRow(id, value); + await this.eventStore.store( + new SettingUpdatedEvent({ + createdBy, + data: { id }, + }), + ); + } else { + await this.settingStore.insert(id, value); + await this.eventStore.store( + new SettingCreatedEvent({ + createdBy, + data: { id }, + }), + ); + } } - async delete(id: string): Promise { - return this.settingStore.delete(id); + async delete(id: string, createdBy: string): Promise { + await this.settingStore.delete(id); + await this.eventStore.store( + new SettingDeletedEvent({ + createdBy, + data: { + id, + }, + }), + ); } } diff --git a/src/lib/services/user-service.test.ts b/src/lib/services/user-service.test.ts index 126fd478de..0bec13fbb8 100644 --- a/src/lib/services/user-service.test.ts +++ b/src/lib/services/user-service.test.ts @@ -13,6 +13,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 FakeEventStore from '../../test/fixtures/fake-event-store'; const config: IUnleashConfig = createTestConfig(); @@ -31,7 +32,10 @@ test('Should create new user', async () => { const sessionService = new SessionService({ sessionStore }, config); const emailService = new EmailService(config.email, config.getLogger); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); @@ -71,7 +75,10 @@ test('Should create default user', async () => { const sessionStore = new FakeSessionStore(); const sessionService = new SessionService({ sessionStore }, config); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); @@ -103,7 +110,10 @@ test('Should be a valid password', async () => { const sessionStore = new FakeSessionStore(); const sessionService = new SessionService({ sessionStore }, config); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); @@ -133,7 +143,10 @@ test('Password must be at least 10 chars', async () => { const sessionStore = new FakeSessionStore(); const sessionService = new SessionService({ sessionStore }, config); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); @@ -165,7 +178,10 @@ test('The password must contain at least one uppercase letter.', async () => { const sessionStore = new FakeSessionStore(); const sessionService = new SessionService({ sessionStore }, config); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); @@ -199,7 +215,10 @@ test('The password must contain at least one number', async () => { const sessionStore = new FakeSessionStore(); const sessionService = new SessionService({ sessionStore }, config); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); @@ -232,7 +251,10 @@ test('The password must contain at least one special character', async () => { const sessionStore = new FakeSessionStore(); const sessionService = new SessionService({ sessionStore }, config); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); @@ -265,7 +287,10 @@ test('Should be a valid password with special chars', async () => { const sessionStore = new FakeSessionStore(); const sessionService = new SessionService({ sessionStore }, config); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 1ec7cfbcef..1ce88f67da 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -64,6 +64,9 @@ export const ENVIRONMENT_IMPORT = 'environment-import'; export const SEGMENT_CREATED = 'segment-created'; export const SEGMENT_UPDATED = 'segment-updated'; export const SEGMENT_DELETED = 'segment-deleted'; +export const SETTING_CREATED = 'setting-created'; +export const SETTING_UPDATED = 'setting-updated'; +export const SETTING_DELETED = 'setting-deleted'; export const CLIENT_METRICS = 'client-metrics'; @@ -437,3 +440,30 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent { this.preData = preData; } } + +export class SettingCreatedEvent extends BaseEvent { + readonly data: any; + + constructor(eventData: { createdBy: string; data: any }) { + super(SETTING_CREATED, eventData.createdBy); + this.data = eventData.data; + } +} + +export class SettingDeletedEvent extends BaseEvent { + readonly data: any; + + constructor(eventData: { createdBy: string; data: any }) { + super(SETTING_DELETED, eventData.createdBy); + this.data = eventData.data; + } +} + +export class SettingUpdatedEvent extends BaseEvent { + readonly data: any; + + constructor(eventData: { createdBy: string; data: any }) { + super(SETTING_UPDATED, eventData.createdBy); + this.data = eventData.data; + } +} diff --git a/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts b/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts index beab0d675f..e7388ba6fe 100644 --- a/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts +++ b/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts @@ -15,6 +15,7 @@ import SessionService from '../../../../lib/services/session-service'; import { RoleName } from '../../../../lib/types/model'; import SettingService from '../../../../lib/services/setting-service'; import FakeSettingStore from '../../../fixtures/fake-setting-store'; +import FakeEventStore from '../../../fixtures/fake-event-store'; let app; let stores; @@ -56,7 +57,10 @@ beforeAll(async () => { ); const sessionService = new SessionService({ sessionStore }, config); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); userService = new UserService(stores, config, { diff --git a/src/test/e2e/services/reset-token-service.e2e.test.ts b/src/test/e2e/services/reset-token-service.e2e.test.ts index 5631a834e8..cb0eb26c1c 100644 --- a/src/test/e2e/services/reset-token-service.e2e.test.ts +++ b/src/test/e2e/services/reset-token-service.e2e.test.ts @@ -11,6 +11,7 @@ import InvalidTokenError from '../../../lib/error/invalid-token-error'; import { IUser } from '../../../lib/types/user'; import SettingService from '../../../lib/services/setting-service'; import FakeSettingStore from '../../fixtures/fake-setting-store'; +import FakeEventStore from '../../fixtures/fake-event-store'; const config: IUnleashConfig = createTestConfig(); @@ -31,7 +32,10 @@ beforeAll(async () => { sessionService = new SessionService(stores, config); const emailService = new EmailService(undefined, config.getLogger); const settingService = new SettingService( - { settingStore: new FakeSettingStore() }, + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, config, ); diff --git a/src/test/e2e/services/setting-service.test.ts b/src/test/e2e/services/setting-service.test.ts index 4f8042f2b7..ad0e47f860 100644 --- a/src/test/e2e/services/setting-service.test.ts +++ b/src/test/e2e/services/setting-service.test.ts @@ -2,6 +2,11 @@ import SettingService from '../../../lib/services/setting-service'; import { createTestConfig } from '../../config/test-config'; import dbInit from '../helpers/database-init'; import { IUnleashStores } from '../../../lib/types/stores'; +import { + SETTING_CREATED, + SETTING_DELETED, + SETTING_UPDATED, +} from '../../../lib/types/events'; let stores: IUnleashStores; let db; @@ -13,23 +18,51 @@ beforeAll(async () => { stores = db.stores; service = new SettingService(stores, config); }); +beforeEach(async () => { + await stores.eventStore.deleteAll(); +}); afterAll(async () => { await db.destroy(); }); test('Can create new setting', async () => { const someData = { some: 'blob' }; - await service.insert('some-setting', someData); + await service.insert('some-setting', someData, 'test-user'); const actual = await service.get('some-setting'); expect(actual).toStrictEqual(someData); + const { eventStore } = stores; + const createdEvents = await eventStore.getEventsFilterByType( + SETTING_CREATED, + ); + expect(createdEvents).toHaveLength(1); }); test('Can delete setting', async () => { const someData = { some: 'blob' }; - await service.insert('some-setting', someData); - await service.delete('some-setting'); + await service.insert('some-setting', someData, 'test-user'); + await service.delete('some-setting', 'test-user'); const actual = await service.get('some-setting'); expect(actual).toBeUndefined(); + const { eventStore } = stores; + const createdEvents = await eventStore.getEventsFilterByType( + SETTING_DELETED, + ); + expect(createdEvents).toHaveLength(1); +}); + +test('Can update setting', async () => { + const { eventStore } = stores; + const someData = { some: 'blob' }; + await service.insert('updated-setting', someData, 'test-user'); + await service.insert( + 'updated-setting', + { ...someData, test: 'fun' }, + 'test-user', + ); + const updatedEvents = await eventStore.getEventsFilterByType( + SETTING_UPDATED, + ); + expect(updatedEvents).toHaveLength(1); }); diff --git a/src/test/e2e/stores/event-store.e2e.test.ts b/src/test/e2e/stores/event-store.e2e.test.ts index 8cfb4cd859..6fff806f19 100644 --- a/src/test/e2e/stores/event-store.e2e.test.ts +++ b/src/test/e2e/stores/event-store.e2e.test.ts @@ -1,6 +1,9 @@ import { APPLICATION_CREATED, FEATURE_CREATED, + FEATURE_DELETED, + FeatureCreatedEvent, + FeatureDeletedEvent, IEvent, } from '../../../lib/types/events'; @@ -19,6 +22,9 @@ beforeAll(async () => { eventStore = stores.eventStore; }); +beforeEach(async () => { + await eventStore.deleteAll(); +}); afterAll(async () => { if (db) { await db.destroy(); @@ -140,6 +146,7 @@ test('Should delete stored event', async () => { }, }; await eventStore.store(event); + await eventStore.store(event); const events = await eventStore.getAll(); const lastEvent = events[0]; await eventStore.delete(lastEvent.id); @@ -178,3 +185,36 @@ test('Should delete all stored events', async () => { expect(events).toHaveLength(0); }); + +test('Should get all events of type', async () => { + const data = { name: 'someName', project: 'test-project' }; + await Promise.all( + [0, 1, 2, 3, 4, 5].map(async (id) => { + const event = + id % 2 == 0 + ? new FeatureCreatedEvent({ + project: data.project, + featureName: data.name, + createdBy: 'test-user', + data, + tags: [], + }) + : new FeatureDeletedEvent({ + project: data.project, + preData: data, + featureName: data.name, + createdBy: 'test-user', + tags: [], + }); + return eventStore.store(event); + }), + ); + const featureCreatedEvents = await eventStore.getEventsFilterByType( + FEATURE_CREATED, + ); + expect(featureCreatedEvents).toHaveLength(3); + const featureDeletedEvents = await eventStore.getEventsFilterByType( + FEATURE_DELETED, + ); + expect(featureDeletedEvents).toHaveLength(3); +});