diff --git a/lib/db/event-store.js b/lib/db/event-store.js index eaf64488ad..fadd20850a 100644 --- a/lib/db/event-store.js +++ b/lib/db/event-store.js @@ -3,7 +3,14 @@ const { EventEmitter } = require('events'); const { DROP_FEATURES } = require('../event-type'); -const EVENT_COLUMNS = ['id', 'type', 'created_by', 'created_at', 'data']; +const EVENT_COLUMNS = [ + 'id', + 'type', + 'created_by', + 'created_at', + 'data', + 'tags', +]; class EventStore extends EventEmitter { constructor(db) { @@ -14,8 +21,9 @@ class EventStore extends EventEmitter { async store(event) { await this.db('events').insert({ type: event.type, - created_by: event.createdBy, // eslint-disable-line + created_by: event.createdBy, // eslint-disable-line data: event.data, + tags: event.tags ? JSON.stringify(event.tags) : [], }); this.emit(event.type, event); } @@ -56,6 +64,7 @@ class EventStore extends EventEmitter { createdBy: row.created_by, createdAt: row.created_at, data: row.data, + tags: row.tags, }; } } diff --git a/lib/routes/admin-api/archive.test.js b/lib/routes/admin-api/archive.test.js index 26fa627c90..c49473c01c 100644 --- a/lib/routes/admin-api/archive.test.js +++ b/lib/routes/admin-api/archive.test.js @@ -83,7 +83,7 @@ test('should revive toggle', t => { }); test('should create event when reviving toggle', async t => { - t.plan(4); + t.plan(6); const name = 'name1'; const { request, @@ -99,13 +99,24 @@ test('should create event when reviving toggle', async t => { strategies: [{ name: 'default' }], }); + await featureToggleService.addTag( + name, + { + type: 'simple', + value: 'tag', + }, + 'test@test.com', + ); + await request.post(`${base}/api/admin/archive/revive/${name}`); const events = await eventStore.getEvents(); - t.is(events.length, 1); - t.is(events[0].type, 'feature-revived'); - t.is(events[0].data.name, name); - t.is(events[0].createdBy, 'unknown'); + t.is(events.length, 3); + t.is(events[2].type, 'feature-revived'); + t.is(events[2].data.name, name); + t.is(events[2].createdBy, 'unknown'); + t.is(events[2].tags[0].type, 'simple'); + t.is(events[2].tags[0].value, 'tag'); }); test('should require toggle name when reviving', t => { diff --git a/lib/routes/admin-api/feature.test.js b/lib/routes/admin-api/feature.test.js index 3e75e4d34c..611fafd955 100644 --- a/lib/routes/admin-api/feature.test.js +++ b/lib/routes/admin-api/feature.test.js @@ -8,7 +8,11 @@ const { createServices } = require('../../services'); const permissions = require('../../../test/fixtures/permissions'); const getLogger = require('../../../test/fixtures/no-logger'); const getApp = require('../../app'); -const { UPDATE_FEATURE, CREATE_FEATURE } = require('../../permissions'); +const { + UPDATE_FEATURE, + CREATE_FEATURE, + DELETE_FEATURE, +} = require('../../permissions'); const eventBus = new EventEmitter(); @@ -31,6 +35,7 @@ function getSetup() { base, perms, featureToggleStore: stores.featureToggleStore, + eventStore: stores.eventStore, request: supertest(app), }; } @@ -523,3 +528,55 @@ test('Should be able to filter on project', t => { t.is(res.body.features[1].name, 'a_tag.toggle'); }); }); + +test('Tags should be included in archive events', async t => { + const { request, eventStore, featureToggleStore, base, perms } = getSetup(); + perms.withPermissions(UPDATE_FEATURE, DELETE_FEATURE); + + featureToggleStore.createFeature({ + name: 'a_team.toggle', + enabled: false, + strategies: [{ name: 'default' }], + project: 'projecta', + }); + featureToggleStore.tagFeature('a_team.toggle', { + type: 'simple', + value: 'tag', + }); + await request + .delete(`${base}/api/admin/features/a_team.toggle`) + .expect(200); + const events = await eventStore.getEvents(); + t.is(events[0].type, 'feature-archived'); + t.is(events[0].tags[0].type, 'simple'); + t.is(events[0].tags[0].value, 'tag'); +}); + +test('Tags should be included in updated events', async t => { + const { request, eventStore, featureToggleStore, base, perms } = getSetup(); + perms.withPermissions(UPDATE_FEATURE, DELETE_FEATURE); + + featureToggleStore.createFeature({ + name: 'a_team.toggle', + enabled: false, + strategies: [{ name: 'default' }], + project: 'projecta', + }); + featureToggleStore.tagFeature('a_team.toggle', { + type: 'simple', + value: 'tag', + }); + await request + .put(`${base}/api/admin/features/a_team.toggle`) + .send({ + name: 'a_team.toggle', + enabled: false, + strategies: [{ name: 'default' }], + project: 'projectb', + }) + .expect(200); + const events = await eventStore.getEvents(); + t.is(events[0].type, 'feature-updated'); + t.is(events[0].tags[0].type, 'simple'); + t.is(events[0].tags[0].value, 'tag'); +}); diff --git a/lib/services/feature-toggle-service.js b/lib/services/feature-toggle-service.js index 85a3b5dc18..9bae03b879 100644 --- a/lib/services/feature-toggle-service.js +++ b/lib/services/feature-toggle-service.js @@ -75,29 +75,41 @@ class FeatureToggleService { await this.featureToggleStore.getFeature(updatedFeature.name); const value = await featureSchema.validateAsync(updatedFeature); await this.featureToggleStore.updateFeature(value); + const tags = + (await this.featureToggleStore.getAllTagsForFeature( + updatedFeature.name, + )) || []; await this.eventStore.store({ type: FEATURE_UPDATED, createdBy: userName, data: updatedFeature, + tags, }); } async archiveToggle(name, userName) { await this.featureToggleStore.getFeature(name); await this.featureToggleStore.archiveFeature(name); + const tags = + (await this.featureToggleStore.getAllTagsForFeature(name)) || []; await this.eventStore.store({ type: FEATURE_ARCHIVED, createdBy: userName, data: { name }, + tags, }); } async reviveToggle(name, userName) { await this.featureToggleStore.reviveFeature({ name }); + const tags = + (await this.featureToggleStore.getAllTagsForFeature(name)) || []; + await this.eventStore.store({ type: FEATURE_REVIVED, createdBy: userName, data: { name }, + tags, }); } @@ -181,10 +193,15 @@ class FeatureToggleService { const feature = await this.featureToggleStore.getFeature(featureName); feature[field] = value; await this.featureToggleStore.updateFeature(feature); + const tags = + (await this.featureToggleStore.getAllTagsForFeature(featureName)) || + []; + await this.eventStore.store({ type: FEATURE_UPDATED, createdBy: userName, data: feature, + tags, }); return feature; } diff --git a/migrations/20210127094440-add-tags-column-to-events.js b/migrations/20210127094440-add-tags-column-to-events.js new file mode 100644 index 0000000000..4883a095ef --- /dev/null +++ b/migrations/20210127094440-add-tags-column-to-events.js @@ -0,0 +1,18 @@ +'use strict'; + +exports.up = function(db, cb) { + db.runSql( + ` + ALTER TABLE events ADD COLUMN IF NOT EXISTS tags json DEFAULT '[]' + `, + cb, + ); +}; +exports.down = function(db, cb) { + db.runSql( + ` + ALTER TABLE events DROP COLUMN IF EXISTS tags; + `, + cb, + ); +}; diff --git a/test/fixtures/fake-feature-toggle-store.js b/test/fixtures/fake-feature-toggle-store.js index 5bca38c3c9..facc3afee4 100644 --- a/test/fixtures/fake-feature-toggle-store.js +++ b/test/fixtures/fake-feature-toggle-store.js @@ -31,6 +31,13 @@ module.exports = () => { ); _features.push(updatedFeature); }, + archiveFeature: feature => { + _features.slice( + _features.indexOf(({ name }) => name === feature.name), + 1, + ); + _archive.push(feature); + }, createFeature: feature => _features.push(feature), getArchivedFeatures: () => Promise.resolve(_archive), addArchivedFeature: feature => _archive.push(feature),