mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: lifecycle stage count (#7434)
This commit is contained in:
		
							parent
							
								
									26d125b495
								
							
						
					
					
						commit
						c14c67f476
					
				| @ -1,9 +1,19 @@ | ||||
| import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type'; | ||||
| import type { | ||||
|     IFeatureLifecycleReadModel, | ||||
|     StageCount, | ||||
|     StageCountByProject, | ||||
| } from './feature-lifecycle-read-model-type'; | ||||
| import type { IFeatureLifecycleStage } from '../../types'; | ||||
| 
 | ||||
| export class FakeFeatureLifecycleReadModel | ||||
|     implements IFeatureLifecycleReadModel | ||||
| { | ||||
|     getStageCount(): Promise<StageCount[]> { | ||||
|         return Promise.resolve([]); | ||||
|     } | ||||
|     getStageCountByProject(): Promise<StageCountByProject[]> { | ||||
|         return Promise.resolve([]); | ||||
|     } | ||||
|     findCurrentStage( | ||||
|         feature: string, | ||||
|     ): Promise<IFeatureLifecycleStage | undefined> { | ||||
|  | ||||
| @ -57,6 +57,10 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore { | ||||
|         this.lifecycles[feature] = []; | ||||
|     } | ||||
| 
 | ||||
|     async deleteAll(): Promise<void> { | ||||
|         this.lifecycles = {}; | ||||
|     } | ||||
| 
 | ||||
|     async stageExists(stage: FeatureLifecycleStage): Promise<boolean> { | ||||
|         const lifecycle = await this.get(stage.feature); | ||||
|         return Boolean(lifecycle.find((s) => s.stage === stage.stage)); | ||||
|  | ||||
| @ -1,7 +1,18 @@ | ||||
| import type { IFeatureLifecycleStage } from '../../types'; | ||||
| import type { IFeatureLifecycleStage, StageName } from '../../types'; | ||||
| 
 | ||||
| export type StageCount = { | ||||
|     stage: StageName; | ||||
|     count: number; | ||||
| }; | ||||
| 
 | ||||
| export type StageCountByProject = StageCount & { | ||||
|     project: string; | ||||
| }; | ||||
| 
 | ||||
| export interface IFeatureLifecycleReadModel { | ||||
|     findCurrentStage( | ||||
|         feature: string, | ||||
|     ): Promise<IFeatureLifecycleStage | undefined>; | ||||
|     getStageCount(): Promise<StageCount[]>; | ||||
|     getStageCountByProject(): Promise<StageCountByProject[]>; | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,64 @@ | ||||
| import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; | ||||
| import getLogger from '../../../test/fixtures/no-logger'; | ||||
| import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model'; | ||||
| import type { IFeatureLifecycleStore } from './feature-lifecycle-store-type'; | ||||
| import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type'; | ||||
| import type { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type'; | ||||
| 
 | ||||
| let db: ITestDb; | ||||
| let featureLifeycycleReadModel: IFeatureLifecycleReadModel; | ||||
| let featureLifecycleStore: IFeatureLifecycleStore; | ||||
| let featureToggleStore: IFeatureToggleStore; | ||||
| 
 | ||||
| beforeAll(async () => { | ||||
|     db = await dbInit('feature_lifecycle_read_model', getLogger); | ||||
|     featureLifeycycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase); | ||||
|     featureLifecycleStore = db.stores.featureLifecycleStore; | ||||
|     featureToggleStore = db.stores.featureToggleStore; | ||||
| }); | ||||
| 
 | ||||
| afterAll(async () => { | ||||
|     if (db) { | ||||
|         await db.destroy(); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| beforeEach(async () => { | ||||
|     await featureToggleStore.deleteAll(); | ||||
| }); | ||||
| 
 | ||||
| test('can return stage count', async () => { | ||||
|     await featureToggleStore.create('default', { | ||||
|         name: 'featureA', | ||||
|         createdByUserId: 9999, | ||||
|     }); | ||||
|     await featureToggleStore.create('default', { | ||||
|         name: 'featureB', | ||||
|         createdByUserId: 9999, | ||||
|     }); | ||||
|     await featureToggleStore.create('default', { | ||||
|         name: 'featureC', | ||||
|         createdByUserId: 9999, | ||||
|     }); | ||||
|     await featureLifecycleStore.insert([ | ||||
|         { feature: 'featureA', stage: 'initial' }, | ||||
|         { feature: 'featureB', stage: 'initial' }, | ||||
|         { feature: 'featureC', stage: 'initial' }, | ||||
|     ]); | ||||
|     await featureLifecycleStore.insert([ | ||||
|         { feature: 'featureA', stage: 'pre-live' }, | ||||
|     ]); | ||||
| 
 | ||||
|     const stageCount = await featureLifeycycleReadModel.getStageCount(); | ||||
|     expect(stageCount).toMatchObject([ | ||||
|         { stage: 'pre-live', count: 1 }, | ||||
|         { stage: 'initial', count: 2 }, | ||||
|     ]); | ||||
| 
 | ||||
|     const stageCountByProject = | ||||
|         await featureLifeycycleReadModel.getStageCountByProject(); | ||||
|     expect(stageCountByProject).toMatchObject([ | ||||
|         { project: 'default', stage: 'pre-live', count: 1 }, | ||||
|         { project: 'default', stage: 'initial', count: 2 }, | ||||
|     ]); | ||||
| }); | ||||
| @ -1,5 +1,9 @@ | ||||
| import type { Db } from '../../db/db'; | ||||
| import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type'; | ||||
| import type { | ||||
|     IFeatureLifecycleReadModel, | ||||
|     StageCount, | ||||
|     StageCountByProject, | ||||
| } from './feature-lifecycle-read-model-type'; | ||||
| import { getCurrentStage } from './get-current-stage'; | ||||
| import type { IFeatureLifecycleStage, StageName } from '../../types'; | ||||
| 
 | ||||
| @ -17,6 +21,61 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel { | ||||
|         this.db = db; | ||||
|     } | ||||
| 
 | ||||
|     async getStageCount(): Promise<StageCount[]> { | ||||
|         const { rows } = await this.db.raw(` | ||||
|             SELECT | ||||
|                 stage, | ||||
|                 COUNT(*) AS feature_count | ||||
|             FROM ( | ||||
|                 SELECT DISTINCT ON (feature) | ||||
|                     feature, | ||||
|                     stage, | ||||
|                     created_at | ||||
|                 FROM | ||||
|                     feature_lifecycles | ||||
|                 ORDER BY | ||||
|                     feature, created_at DESC | ||||
|             ) AS LatestStages | ||||
|             GROUP BY | ||||
|                 stage; | ||||
|         `);
 | ||||
| 
 | ||||
|         return rows.map((row) => ({ | ||||
|             stage: row.stage, | ||||
|             count: Number(row.feature_count), | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     async getStageCountByProject(): Promise<StageCountByProject[]> { | ||||
|         const { rows } = await this.db.raw(` | ||||
|             SELECT | ||||
|                 f.project, | ||||
|                 ls.stage, | ||||
|                 COUNT(*) AS feature_count | ||||
|             FROM ( | ||||
|                 SELECT DISTINCT ON (fl.feature) | ||||
|                     fl.feature, | ||||
|                     fl.stage, | ||||
|                     fl.created_at | ||||
|                 FROM | ||||
|                     feature_lifecycles fl | ||||
|                 ORDER BY | ||||
|                     fl.feature, fl.created_at DESC | ||||
|             ) AS ls | ||||
|             JOIN | ||||
|                 features f ON f.name = ls.feature | ||||
|             GROUP BY | ||||
|                 f.project, | ||||
|                 ls.stage; | ||||
|         `);
 | ||||
| 
 | ||||
|         return rows.map((row) => ({ | ||||
|             stage: row.stage, | ||||
|             count: Number(row.feature_count), | ||||
|             project: row.project, | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     async findCurrentStage( | ||||
|         feature: string, | ||||
|     ): Promise<IFeatureLifecycleStage | undefined> { | ||||
|  | ||||
| @ -20,6 +20,7 @@ export interface IFeatureLifecycleStore { | ||||
|     getAll(): Promise<FeatureLifecycleProjectItem[]>; | ||||
|     stageExists(stage: FeatureLifecycleStage): Promise<boolean>; | ||||
|     delete(feature: string): Promise<void>; | ||||
|     deleteAll(): Promise<void>; | ||||
|     deleteStage(stage: FeatureLifecycleStage): Promise<void>; | ||||
|     backfill(): Promise<void>; | ||||
| } | ||||
|  | ||||
| @ -101,6 +101,10 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore { | ||||
|         await this.db('feature_lifecycles').where({ feature }).del(); | ||||
|     } | ||||
| 
 | ||||
|     async deleteAll(): Promise<void> { | ||||
|         await this.db('feature_lifecycles').del(); | ||||
|     } | ||||
| 
 | ||||
|     async deleteStage(stage: FeatureLifecycleStage): Promise<void> { | ||||
|         await this.db('feature_lifecycles') | ||||
|             .where({ | ||||
|  | ||||
| @ -62,7 +62,9 @@ afterAll(async () => { | ||||
|     await db.destroy(); | ||||
| }); | ||||
| 
 | ||||
| beforeEach(async () => {}); | ||||
| beforeEach(async () => { | ||||
|     await featureLifecycleStore.deleteAll(); | ||||
| }); | ||||
| 
 | ||||
| const getFeatureLifecycle = async (featureName: string, expectedCode = 200) => { | ||||
|     return app.request | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user