diff --git a/src/lib/db/addon-store.ts b/src/lib/db/addon-store.ts index 419a17903a..1b7b672f60 100644 --- a/src/lib/db/addon-store.ts +++ b/src/lib/db/addon-store.ts @@ -115,6 +115,8 @@ export default class AddonStore implements IAddonStore { description: row.description, parameters: row.parameters, events: row.events, + projects: row.projects || [], + environments: row.environments || [], createdAt: row.created_at, }; } @@ -127,6 +129,8 @@ export default class AddonStore implements IAddonStore { description: addon.description, parameters: JSON.stringify(addon.parameters), events: JSON.stringify(addon.events), + projects: JSON.stringify(addon.projects || []), + environments: JSON.stringify(addon.environments || []), }; } } diff --git a/src/lib/openapi/spec/addon-schema.ts b/src/lib/openapi/spec/addon-schema.ts index 88194e175b..b7c92321b9 100644 --- a/src/lib/openapi/spec/addon-schema.ts +++ b/src/lib/openapi/spec/addon-schema.ts @@ -32,6 +32,18 @@ export const addonSchema = { type: 'string', }, }, + projects: { + type: 'array', + items: { + type: 'string', + }, + }, + environments: { + type: 'array', + items: { + type: 'string', + }, + }, }, components: {}, } as const; diff --git a/src/lib/services/addon-schema.ts b/src/lib/services/addon-schema.ts index d653970092..723160c423 100644 --- a/src/lib/services/addon-schema.ts +++ b/src/lib/services/addon-schema.ts @@ -12,5 +12,7 @@ export const addonSchema = joi .pattern(joi.string(), [joi.string(), joi.number(), joi.boolean()]) .optional(), events: joi.array().optional().items(joi.string()), + projects: joi.array().optional().items(joi.string()), + environments: joi.array().optional().items(joi.string()), }) .options({ allowUnknown: false, stripUnknown: true }); diff --git a/src/lib/services/addon-service.test.ts b/src/lib/services/addon-service.test.ts index 4e7fe2ed43..3d3dcdf361 100644 --- a/src/lib/services/addon-service.test.ts +++ b/src/lib/services/addon-service.test.ts @@ -13,15 +13,17 @@ import createStores from '../../test/fixtures/store'; import AddonService from './addon-service'; import { IAddonDto } from '../types/stores/addon-store'; import SimpleAddon from './addon-service-test-simple-addon'; +import { IAddonProviders } from '../addons'; const MASKED_VALUE = '*****'; -const addonProvider = { simple: new SimpleAddon() }; +let addonProvider: IAddonProviders; function getSetup() { const stores = createStores(); const tagTypeService = new TagTypeService(stores, { getLogger }); + addonProvider = { simple: new SimpleAddon() }; return { addonService: new AddonService( stores, @@ -49,7 +51,7 @@ test('should load addon configurations', async () => { test('should load provider definitions', async () => { const { addonService } = getSetup(); - const providerDefinitions = await addonService.getProviderDefinitions(); + const providerDefinitions = addonService.getProviderDefinitions(); const simple = providerDefinitions.find((p) => p.name === 'simple'); @@ -110,6 +112,276 @@ test('should trigger simple-addon eventHandler', async () => { expect(events[0].event.data.name).toBe('some-toggle'); }); +test('should not trigger event handler if project of event is different from addon', async () => { + const { addonService, stores } = getSetup(); + const config = { + provider: 'simple', + enabled: true, + events: [FEATURE_CREATED], + projects: ['someproject'], + description: '', + parameters: { + url: 'http://localhost:wh', + }, + }; + + await addonService.createAddon(config, 'me@mail.com'); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: 'someotherproject', + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + const simpleProvider = addonService.addonProviders.simple; + // @ts-expect-error + const events = simpleProvider.getEvents(); + + expect(events.length).toBe(0); +}); + +test('should trigger event handler if project for event is one of the desired projects for addon', async () => { + const { addonService, stores } = getSetup(); + const desiredProject = 'desired'; + const otherProject = 'other'; + const config = { + provider: 'simple', + enabled: true, + events: [FEATURE_CREATED], + projects: [desiredProject], + description: '', + parameters: { + url: 'http://localhost:wh', + }, + }; + + await addonService.createAddon(config, 'me@mail.com'); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: desiredProject, + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: otherProject, + data: { + name: 'other-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + const simpleProvider = addonService.addonProviders.simple; + // @ts-expect-error + const events = simpleProvider.getEvents(); + + expect(events.length).toBe(1); + expect(events[0].event.type).toBe(FEATURE_CREATED); + expect(events[0].event.data.name).toBe('some-toggle'); +}); + +test('should trigger events for multiple projects if addon is setup to filter multiple projects', async () => { + const { addonService, stores } = getSetup(); + const desiredProjects = ['desired', 'desired2']; + const otherProject = 'other'; + const config = { + provider: 'simple', + enabled: true, + events: [FEATURE_CREATED], + projects: desiredProjects, + description: '', + parameters: { + url: 'http://localhost:wh', + }, + }; + + await addonService.createAddon(config, 'me@mail.com'); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: desiredProjects[0], + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: otherProject, + data: { + name: 'other-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: desiredProjects[1], + data: { + name: 'third-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + const simpleProvider = addonService.addonProviders.simple; + // @ts-expect-error + const events = simpleProvider.getEvents(); + + expect(events.length).toBe(2); + expect(events[0].event.type).toBe(FEATURE_CREATED); + expect(events[0].event.data.name).toBe('some-toggle'); + expect(events[1].event.type).toBe(FEATURE_CREATED); + expect(events[1].event.data.name).toBe('third-toggle'); +}); + +test('should filter events on environment if addon is setup to filter for it', async () => { + const { addonService, stores } = getSetup(); + const desiredEnvironment = 'desired'; + const otherEnvironment = 'other'; + const config = { + provider: 'simple', + enabled: true, + events: [FEATURE_CREATED], + projects: [], + environments: [desiredEnvironment], + description: '', + parameters: { + url: 'http://localhost:wh', + }, + }; + + await addonService.createAddon(config, 'me@mail.com'); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: desiredEnvironment, + environment: desiredEnvironment, + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + environment: otherEnvironment, + data: { + name: 'other-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + const simpleProvider = addonService.addonProviders.simple; + // @ts-expect-error + const events = simpleProvider.getEvents(); + + expect(events.length).toBe(1); + expect(events[0].event.type).toBe(FEATURE_CREATED); + expect(events[0].event.data.name).toBe('some-toggle'); +}); + +test('Should support filtering by both project and environment', async () => { + const { addonService, stores } = getSetup(); + const desiredProjects = ['desired1', 'desired2', 'desired3']; + const desiredEnvironments = ['env1', 'env2', 'env3']; + const config = { + provider: 'simple', + enabled: true, + events: [FEATURE_CREATED], + projects: desiredProjects, + environments: desiredEnvironments, + description: '', + parameters: { + url: 'http://localhost:wh', + }, + }; + const expectedFeatureNames = [ + 'desired-toggle1', + 'desired-toggle2', + 'desired-toggle3', + ]; + await addonService.createAddon(config, 'me@mail.com'); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: desiredProjects[0], + environment: desiredEnvironments[0], + data: { + name: expectedFeatureNames[0], + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: desiredProjects[0], + environment: 'wrongenvironment', + data: { + name: 'other-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: desiredProjects[2], + environment: desiredEnvironments[1], + data: { + name: expectedFeatureNames[1], + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: desiredProjects[2], + environment: desiredEnvironments[2], + data: { + name: expectedFeatureNames[2], + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + await stores.eventStore.store({ + type: FEATURE_CREATED, + createdBy: 'some@user.com', + project: 'wrongproject', + environment: desiredEnvironments[0], + data: { + name: 'not-expected', + enabled: false, + strategies: [{ name: 'default' }], + }, + }); + + const simpleProvider = addonService.addonProviders.simple; + // @ts-expect-error + const events = simpleProvider.getEvents(); + + expect(events.length).toBe(3); + expect(events[0].event.type).toBe(FEATURE_CREATED); + expect(events[0].event.data.name).toBe(expectedFeatureNames[0]); + expect(events[1].event.type).toBe(FEATURE_CREATED); + expect(events[1].event.data.name).toBe(expectedFeatureNames[1]); + expect(events[2].event.type).toBe(FEATURE_CREATED); + expect(events[2].event.data.name).toBe(expectedFeatureNames[2]); +}); + test('should create simple-addon config', async () => { const { addonService } = getSetup(); diff --git a/src/lib/services/addon-service.ts b/src/lib/services/addon-service.ts index 74f5f9fab0..4b17e49af9 100644 --- a/src/lib/services/addon-service.ts +++ b/src/lib/services/addon-service.ts @@ -105,6 +105,18 @@ export default class AddonService { this.fetchAddonConfigs().then((addonInstances) => { addonInstances .filter((addon) => addon.events.includes(eventName)) + .filter( + (addon) => + !addon.projects || + addon.projects.length == 0 || + addon.projects.includes(event.project), + ) + .filter( + (addon) => + !addon.environments || + addon.environments.length == 0 || + addon.environments.includes(event.environment), + ) .filter((addon) => addonProviders[addon.provider]) .forEach((addon) => addonProviders[addon.provider].handleEvent( diff --git a/src/lib/types/stores/addon-store.ts b/src/lib/types/stores/addon-store.ts index c0864b23b5..3d6e8a7869 100644 --- a/src/lib/types/stores/addon-store.ts +++ b/src/lib/types/stores/addon-store.ts @@ -5,6 +5,8 @@ export interface IAddonDto { description: string; enabled: boolean; parameters: Record; + projects?: string[]; + environments?: string[]; events: string[]; } diff --git a/src/migrations/20220711084613-add-projects-and-environments-for-addons.js b/src/migrations/20220711084613-add-projects-and-environments-for-addons.js new file mode 100644 index 0000000000..aff4c8164f --- /dev/null +++ b/src/migrations/20220711084613-add-projects-and-environments-for-addons.js @@ -0,0 +1,21 @@ +exports.up = function (db, cb) { + db.runSql( + `ALTER TABLE addons + ADD COLUMN + projects jsonb DEFAULT '[]'::jsonb; + ALTER TABLE addons + ADD COLUMN environments jsonb DEFAULT '[]'::jsonb; + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + ALTER TABLE addons DROP COLUMN projects; + ALTER TABLE addons DROP COLUMN environments; +`, + cb, + ); +}; diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index a6fdf4fde6..b85be57f34 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -97,6 +97,12 @@ Object { "enabled": Object { "type": "boolean", }, + "environments": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, "events": Object { "items": Object { "type": "string", @@ -110,6 +116,12 @@ Object { "additionalProperties": true, "type": "object", }, + "projects": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, "provider": Object { "type": "string", },