From 6b59b30846ab65d36a6869f548e068f1f80082b4 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 15 May 2024 12:47:01 +0200 Subject: [PATCH] feat: backfill current stage on startup (#7057) --- .../fake-feature-lifecycle-store.ts | 2 ++ .../feature-lifecycle-service.ts | 1 + .../feature-lifecycle-store-type.ts | 1 + .../feature-lifecycle-store.ts | 10 ++++++++++ .../feature-lifecycle.e2e.test.ts | 20 +++++++++++++++++++ 5 files changed, 34 insertions(+) diff --git a/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts b/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts index d15b1f9f4923..bf545e899f99 100644 --- a/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts +++ b/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts @@ -16,6 +16,8 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore { ); } + async backfill() {} + private async insertOne( featureLifecycleStage: FeatureLifecycleStage, ): Promise { diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts index ff976d6e8de9..164690307dc1 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts @@ -87,6 +87,7 @@ export class FeatureLifecycleService extends EventEmitter { } listen() { + void this.checkEnabled(() => this.featureLifecycleStore.backfill()); this.eventStore.on(FEATURE_CREATED, async (event) => { await this.checkEnabled(() => this.featureInitialized(event.featureName), diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts index a0659ffa8a43..d8ff0b88599c 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts @@ -21,4 +21,5 @@ export interface IFeatureLifecycleStore { stageExists(stage: FeatureLifecycleStage): Promise; delete(feature: string): Promise; deleteStage(stage: FeatureLifecycleStage): Promise; + backfill(): Promise; } diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts index 89076e7f3088..6e3586a72f1e 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts @@ -26,6 +26,16 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore { this.db = db; } + async backfill(): Promise { + await this.db.raw(` + INSERT INTO feature_lifecycles (feature, stage, created_at) + SELECT features.name, 'initial', features.created_at + FROM features + LEFT JOIN feature_lifecycles ON features.name = feature_lifecycles.feature + WHERE feature_lifecycles.feature IS NULL + `); + } + async insert( featureLifecycleStages: FeatureLifecycleStage[], ): Promise { diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts index 65e207958a2a..8e5effacf19e 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts @@ -10,6 +10,7 @@ import { FEATURE_CREATED, FEATURE_REVIVED, type IEventStore, + type IFeatureLifecycleStore, type StageName, } from '../../types'; import type EventEmitter from 'events'; @@ -24,6 +25,7 @@ import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model- let app: IUnleashTest; let db: ITestDb; let featureLifecycleService: FeatureLifecycleService; +let featureLifecycleStore: IFeatureLifecycleStore; let eventStore: IEventStore; let eventBus: EventEmitter; let featureLifecycleReadModel: IFeatureLifecycleReadModel; @@ -45,6 +47,7 @@ beforeAll(async () => { eventBus = app.config.eventBus; featureLifecycleService = app.services.featureLifecycleService; featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase); + featureLifecycleStore = db.stores.featureLifecycleStore; await app.request .post(`/auth/demo/login`) @@ -186,3 +189,20 @@ test('should be able to toggle between completed/uncompleted', async () => { expect(body).toEqual([]); }); + +test('should backfill initial stage when no stages', async () => { + await app.createFeature('my_feature_c'); + + await featureLifecycleStore.delete('my_feature_c'); + + const currentStage = await getCurrentStage('my_feature_c'); + expect(currentStage).toBe(undefined); + + await featureLifecycleStore.backfill(); + + const backfilledCurrentStage = await getCurrentStage('my_feature_c'); + expect(backfilledCurrentStage).toEqual({ + stage: 'initial', + enteredStageAt: expect.any(Date), + }); +});