diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index 999523931c..68451704e6 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -172,15 +172,11 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { projectId, featureName } = req.params; - const featureToggle = await this.featureService.getFeatureMetadata( - featureName, - ); - const { newDocument } = applyPatch(featureToggle, req.body); - const userName = extractUsername(req); - const updated = await this.featureService.updateFeatureToggle( + const updated = await this.featureService.patchFeature( projectId, - newDocument, - userName, + featureName, + extractUsername(req), + req.body, ); res.status(200).json(updated); } diff --git a/src/lib/services/feature-toggle-service-v2.ts b/src/lib/services/feature-toggle-service-v2.ts index ac434e3dc6..cd8f5eef8d 100644 --- a/src/lib/services/feature-toggle-service-v2.ts +++ b/src/lib/services/feature-toggle-service-v2.ts @@ -45,6 +45,7 @@ import { import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store'; import { DEFAULT_ENV } from '../util/constants'; +import { applyPatch, deepClone, Operation } from 'fast-json-patch'; class FeatureToggleServiceV2 { private logger: Logger; @@ -94,6 +95,34 @@ class FeatureToggleServiceV2 { this.featureEnvironmentStore = featureEnvironmentStore; } + async patchFeature( + projectId: string, + featureName: string, + userName: string, + operations: Operation[], + ): Promise { + const featureToggle = await this.getFeatureMetadata(featureName); + + const { newDocument } = applyPatch( + deepClone(featureToggle), + operations, + ); + const updated = await this.updateFeatureToggle( + projectId, + newDocument, + userName, + ); + if (featureToggle.stale !== newDocument.stale) { + await this.eventStore.store({ + type: newDocument.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, + data: updated, + project: projectId, + createdBy: userName, + }); + } + return updated; + } + async createStrategy( strategyConfig: Omit, projectId: string, diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index 01cff4f5c0..3302fcbf83 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -6,6 +6,8 @@ import { FEATURE_ENVIRONMENT_DISABLED, FEATURE_ENVIRONMENT_ENABLED, FEATURE_METADATA_UPDATED, + FEATURE_STALE_OFF, + FEATURE_STALE_ON, FEATURE_STRATEGY_REMOVE, } from '../../../../../lib/types/events'; @@ -531,6 +533,56 @@ test('Should patch feature toggle', async () => { expect(updateForOurToggle.data.type).toBe('kill-switch'); }); +test('Patching feature toggles to stale should trigger FEATURE_STALE_ON event', async () => { + const url = '/api/admin/projects/default/features'; + const name = 'toggle.stale.on.patch'; + await app.request + .post(url) + .send({ name, description: 'some', type: 'release', stale: false }) + .expect(201); + await app.request + .patch(`${url}/${name}`) + .send([{ op: 'replace', path: '/stale', value: true }]) + .expect(200); + + const { body: toggle } = await app.request.get(`${url}/${name}`); + + expect(toggle.name).toBe(name); + expect(toggle.archived).toBeFalsy(); + expect(toggle.stale).toBeTruthy(); + const events = await db.stores.eventStore.getAll({ + type: FEATURE_STALE_ON, + }); + const updateForOurToggle = events.find((e) => e.data.name === name); + expect(updateForOurToggle).toBeTruthy(); + expect(updateForOurToggle.data.stale).toBe(true); +}); + +test('Patching feature toggles to active (turning stale to false) should trigger FEATURE_STALE_OFF event', async () => { + const url = '/api/admin/projects/default/features'; + const name = 'toggle.stale.off.patch'; + await app.request + .post(url) + .send({ name, description: 'some', type: 'release', stale: true }) + .expect(201); + await app.request + .patch(`${url}/${name}`) + .send([{ op: 'replace', path: '/stale', value: false }]) + .expect(200); + + const { body: toggle } = await app.request.get(`${url}/${name}`); + + expect(toggle.name).toBe(name); + expect(toggle.archived).toBeFalsy(); + expect(toggle.stale).toBe(false); + const events = await db.stores.eventStore.getAll({ + type: FEATURE_STALE_OFF, + }); + const updateForOurToggle = events.find((e) => e.data.name === name); + expect(updateForOurToggle).toBeTruthy(); + expect(updateForOurToggle.data.stale).toBe(false); +}); + test('Should archive feature toggle', async () => { const url = '/api/admin/projects/default/features'; const name = 'new.toggle.archive';