mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: kept and discarded read model (#7045)
This commit is contained in:
		
							parent
							
								
									4241e36819
								
							
						
					
					
						commit
						dfc065500d
					
				@ -43,7 +43,10 @@ export const populateCurrentStage = (
 | 
				
			|||||||
        case 'completed':
 | 
					        case 'completed':
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
                name: 'completed',
 | 
					                name: 'completed',
 | 
				
			||||||
                status: 'kept',
 | 
					                status:
 | 
				
			||||||
 | 
					                    feature.lifecycle.status === 'discarded'
 | 
				
			||||||
 | 
					                        ? 'discarded'
 | 
				
			||||||
 | 
					                        : 'kept',
 | 
				
			||||||
                environments: getFilteredEnvironments(() => true),
 | 
					                environments: getFilteredEnvironments(() => true),
 | 
				
			||||||
                enteredStageAt,
 | 
					                enteredStageAt,
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
				
			|||||||
@ -137,7 +137,10 @@ const FeatureOverviewMetaData = () => {
 | 
				
			|||||||
                        <span>{project}</span>
 | 
					                        <span>{project}</span>
 | 
				
			||||||
                    </SpacedBodyItem>
 | 
					                    </SpacedBodyItem>
 | 
				
			||||||
                    <ConditionallyRender
 | 
					                    <ConditionallyRender
 | 
				
			||||||
                        condition={featureLifecycleEnabled}
 | 
					                        condition={
 | 
				
			||||||
 | 
					                            featureLifecycleEnabled &&
 | 
				
			||||||
 | 
					                            Boolean(feature.lifecycle)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                        show={
 | 
					                        show={
 | 
				
			||||||
                            <SpacedBodyItem data-loading>
 | 
					                            <SpacedBodyItem data-loading>
 | 
				
			||||||
                                <StyledLabel>Lifecycle:</StyledLabel>
 | 
					                                <StyledLabel>Lifecycle:</StyledLabel>
 | 
				
			||||||
 | 
				
			|||||||
@ -34,6 +34,7 @@ export type ILastSeenEnvironments = Pick<
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export type Lifecycle = {
 | 
					export type Lifecycle = {
 | 
				
			||||||
    stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
 | 
					    stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
 | 
				
			||||||
 | 
					    status?: string;
 | 
				
			||||||
    enteredStageAt: string;
 | 
					    enteredStageAt: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,9 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
 | 
				
			|||||||
            ...existingStages,
 | 
					            ...existingStages,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                stage: featureLifecycleStage.stage,
 | 
					                stage: featureLifecycleStage.stage,
 | 
				
			||||||
 | 
					                ...(featureLifecycleStage.status
 | 
				
			||||||
 | 
					                    ? { status: featureLifecycleStage.status }
 | 
				
			||||||
 | 
					                    : {}),
 | 
				
			||||||
                enteredStageAt: new Date(),
 | 
					                enteredStageAt: new Date(),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ import type { IFeatureLifecycleStage, StageName } from '../../types';
 | 
				
			|||||||
type DBType = {
 | 
					type DBType = {
 | 
				
			||||||
    feature: string;
 | 
					    feature: string;
 | 
				
			||||||
    stage: StageName;
 | 
					    stage: StageName;
 | 
				
			||||||
 | 
					    status: string | null;
 | 
				
			||||||
    created_at: Date;
 | 
					    created_at: Date;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,8 +24,9 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
 | 
				
			|||||||
            .where({ feature })
 | 
					            .where({ feature })
 | 
				
			||||||
            .orderBy('created_at', 'asc');
 | 
					            .orderBy('created_at', 'asc');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const stages = results.map(({ stage, created_at }: DBType) => ({
 | 
					        const stages = results.map(({ stage, status, created_at }: DBType) => ({
 | 
				
			||||||
            stage,
 | 
					            stage,
 | 
				
			||||||
 | 
					            ...(status ? { status } : {}),
 | 
				
			||||||
            enteredStageAt: created_at,
 | 
					            enteredStageAt: created_at,
 | 
				
			||||||
        }));
 | 
					        }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -10,8 +10,8 @@ import type { StageName } from '../../types';
 | 
				
			|||||||
type DBType = {
 | 
					type DBType = {
 | 
				
			||||||
    stage: StageName;
 | 
					    stage: StageName;
 | 
				
			||||||
    created_at: string;
 | 
					    created_at: string;
 | 
				
			||||||
    status?: string;
 | 
					    status: string | null;
 | 
				
			||||||
    status_value?: string;
 | 
					    status_value: string | null;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DBProjectType = DBType & {
 | 
					type DBProjectType = DBType & {
 | 
				
			||||||
@ -64,8 +64,9 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore {
 | 
				
			|||||||
            .where({ feature })
 | 
					            .where({ feature })
 | 
				
			||||||
            .orderBy('created_at', 'asc');
 | 
					            .orderBy('created_at', 'asc');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return results.map(({ stage, created_at }: DBType) => ({
 | 
					        return results.map(({ stage, status, created_at }: DBType) => ({
 | 
				
			||||||
            stage,
 | 
					            stage,
 | 
				
			||||||
 | 
					            ...(status ? { status } : {}),
 | 
				
			||||||
            enteredStageAt: new Date(created_at),
 | 
					            enteredStageAt: new Date(created_at),
 | 
				
			||||||
        }));
 | 
					        }));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -18,12 +18,15 @@ import {
 | 
				
			|||||||
    STAGE_ENTERED,
 | 
					    STAGE_ENTERED,
 | 
				
			||||||
} from './feature-lifecycle-service';
 | 
					} from './feature-lifecycle-service';
 | 
				
			||||||
import type { FeatureLifecycleCompletedSchema } from '../../openapi';
 | 
					import type { FeatureLifecycleCompletedSchema } from '../../openapi';
 | 
				
			||||||
 | 
					import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model';
 | 
				
			||||||
 | 
					import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let app: IUnleashTest;
 | 
					let app: IUnleashTest;
 | 
				
			||||||
let db: ITestDb;
 | 
					let db: ITestDb;
 | 
				
			||||||
let featureLifecycleService: FeatureLifecycleService;
 | 
					let featureLifecycleService: FeatureLifecycleService;
 | 
				
			||||||
let eventStore: IEventStore;
 | 
					let eventStore: IEventStore;
 | 
				
			||||||
let eventBus: EventEmitter;
 | 
					let eventBus: EventEmitter;
 | 
				
			||||||
 | 
					let featureLifecycleReadModel: IFeatureLifecycleReadModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
beforeAll(async () => {
 | 
					beforeAll(async () => {
 | 
				
			||||||
    db = await dbInit('feature_lifecycle', getLogger);
 | 
					    db = await dbInit('feature_lifecycle', getLogger);
 | 
				
			||||||
@ -41,6 +44,7 @@ beforeAll(async () => {
 | 
				
			|||||||
    eventStore = db.stores.eventStore;
 | 
					    eventStore = db.stores.eventStore;
 | 
				
			||||||
    eventBus = app.config.eventBus;
 | 
					    eventBus = app.config.eventBus;
 | 
				
			||||||
    featureLifecycleService = app.services.featureLifecycleService;
 | 
					    featureLifecycleService = app.services.featureLifecycleService;
 | 
				
			||||||
 | 
					    featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .post(`/auth/demo/login`)
 | 
					        .post(`/auth/demo/login`)
 | 
				
			||||||
@ -62,6 +66,11 @@ const getFeatureLifecycle = async (featureName: string, expectedCode = 200) => {
 | 
				
			|||||||
        .get(`/api/admin/projects/default/features/${featureName}/lifecycle`)
 | 
					        .get(`/api/admin/projects/default/features/${featureName}/lifecycle`)
 | 
				
			||||||
        .expect(expectedCode);
 | 
					        .expect(expectedCode);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getCurrentStage = async (featureName: string) => {
 | 
				
			||||||
 | 
					    return featureLifecycleReadModel.findCurrentStage(featureName);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const completeFeature = async (
 | 
					const completeFeature = async (
 | 
				
			||||||
    featureName: string,
 | 
					    featureName: string,
 | 
				
			||||||
    status: FeatureLifecycleCompletedSchema,
 | 
					    status: FeatureLifecycleCompletedSchema,
 | 
				
			||||||
@ -166,6 +175,8 @@ test('should be able to toggle between completed/uncompleted', async () => {
 | 
				
			|||||||
        status: 'kept',
 | 
					        status: 'kept',
 | 
				
			||||||
        statusValue: 'variant1',
 | 
					        statusValue: 'variant1',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    const currentStage = await getCurrentStage('my_feature_b');
 | 
				
			||||||
 | 
					    expect(currentStage).toMatchObject({ stage: 'completed', status: 'kept' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await expectFeatureStage('my_feature_b', 'completed');
 | 
					    await expectFeatureStage('my_feature_b', 'completed');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,7 @@ describe('getCurrentStage', () => {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                stage: 'completed',
 | 
					                stage: 'completed',
 | 
				
			||||||
 | 
					                status: 'kept',
 | 
				
			||||||
                enteredStageAt: irrelevantDate,
 | 
					                enteredStageAt: irrelevantDate,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
				
			|||||||
@ -79,6 +79,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
 | 
				
			|||||||
            .select(
 | 
					            .select(
 | 
				
			||||||
                'feature as stage_feature',
 | 
					                'feature as stage_feature',
 | 
				
			||||||
                'stage as latest_stage',
 | 
					                'stage as latest_stage',
 | 
				
			||||||
 | 
					                'status as stage_status',
 | 
				
			||||||
                'created_at as entered_stage_at',
 | 
					                'created_at as entered_stage_at',
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .distinctOn('stage_feature')
 | 
					            .distinctOn('stage_feature')
 | 
				
			||||||
@ -416,6 +417,9 @@ class FeatureSearchStore implements IFeatureSearchStore {
 | 
				
			|||||||
                    entry.lifecycle = row.latest_stage
 | 
					                    entry.lifecycle = row.latest_stage
 | 
				
			||||||
                        ? {
 | 
					                        ? {
 | 
				
			||||||
                              stage: row.latest_stage,
 | 
					                              stage: row.latest_stage,
 | 
				
			||||||
 | 
					                              ...(row.stage_status
 | 
				
			||||||
 | 
					                                  ? { status: row.stage_status }
 | 
				
			||||||
 | 
					                                  : {}),
 | 
				
			||||||
                              enteredStageAt: row.entered_stage_at,
 | 
					                              enteredStageAt: row.entered_stage_at,
 | 
				
			||||||
                          }
 | 
					                          }
 | 
				
			||||||
                        : undefined;
 | 
					                        : undefined;
 | 
				
			||||||
 | 
				
			|||||||
@ -962,7 +962,7 @@ test('should return environment usage metrics and lifecycle', async () => {
 | 
				
			|||||||
        { feature: 'my_feature_b', stage: 'initial' },
 | 
					        { feature: 'my_feature_b', stage: 'initial' },
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
    await stores.featureLifecycleStore.insert([
 | 
					    await stores.featureLifecycleStore.insert([
 | 
				
			||||||
        { feature: 'my_feature_b', stage: 'pre-live' },
 | 
					        { feature: 'my_feature_b', stage: 'completed', status: 'discarded' },
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body } = await searchFeatures({
 | 
					    const { body } = await searchFeatures({
 | 
				
			||||||
@ -972,7 +972,7 @@ test('should return environment usage metrics and lifecycle', async () => {
 | 
				
			|||||||
        features: [
 | 
					        features: [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                name: 'my_feature_b',
 | 
					                name: 'my_feature_b',
 | 
				
			||||||
                lifecycle: { stage: 'pre-live' },
 | 
					                lifecycle: { stage: 'completed', status: 'discarded' },
 | 
				
			||||||
                environments: [
 | 
					                environments: [
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        name: 'default',
 | 
					                        name: 'default',
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,12 @@ export const featureLifecycleSchema = {
 | 
				
			|||||||
                description:
 | 
					                description:
 | 
				
			||||||
                    'The name of the lifecycle stage that got recorded for a given feature',
 | 
					                    'The name of the lifecycle stage that got recorded for a given feature',
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            status: {
 | 
				
			||||||
 | 
					                type: 'string',
 | 
				
			||||||
 | 
					                example: 'kept',
 | 
				
			||||||
 | 
					                description:
 | 
				
			||||||
 | 
					                    'The name of the detailed status of a given stage. E.g. completed stage can be kept or discarded.',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            enteredStageAt: {
 | 
					            enteredStageAt: {
 | 
				
			||||||
                type: 'string',
 | 
					                type: 'string',
 | 
				
			||||||
                format: 'date-time',
 | 
					                format: 'date-time',
 | 
				
			||||||
 | 
				
			|||||||
@ -161,6 +161,13 @@ export const featureSearchResponseSchema = {
 | 
				
			|||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                    example: 'initial',
 | 
					                    example: 'initial',
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
					                status: {
 | 
				
			||||||
 | 
					                    type: 'string',
 | 
				
			||||||
 | 
					                    nullable: true,
 | 
				
			||||||
 | 
					                    example: 'kept',
 | 
				
			||||||
 | 
					                    description:
 | 
				
			||||||
 | 
					                        'The name of the detailed status of a given stage. E.g. completed stage can be kept or discarded.',
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
                enteredStageAt: {
 | 
					                enteredStageAt: {
 | 
				
			||||||
                    description: 'When the feature entered this stage',
 | 
					                    description: 'When the feature entered this stage',
 | 
				
			||||||
                    type: 'string',
 | 
					                    type: 'string',
 | 
				
			||||||
 | 
				
			|||||||
@ -163,6 +163,7 @@ export type StageName =
 | 
				
			|||||||
export interface IFeatureLifecycleStage {
 | 
					export interface IFeatureLifecycleStage {
 | 
				
			||||||
    stage: StageName;
 | 
					    stage: StageName;
 | 
				
			||||||
    enteredStageAt: Date;
 | 
					    enteredStageAt: Date;
 | 
				
			||||||
 | 
					    status?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IProjectLifecycleStageDuration = {
 | 
					export type IProjectLifecycleStageDuration = {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user