mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: add completed status backend (#7022)
1. Implemented controller, service, store for status saving
This commit is contained in:
		
							parent
							
								
									97d702afeb
								
							
						
					
					
						commit
						79739a1d7f
					
				@ -0,0 +1,16 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Generated by Orval
 | 
			
		||||
 * Do not edit manually.
 | 
			
		||||
 * See `gen:api` script in package.json
 | 
			
		||||
 */
 | 
			
		||||
import type { FeatureLifecycleCompletedSchemaStatus } from './featureLifecycleCompletedSchemaStatus';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A feature that has been marked as completed
 | 
			
		||||
 */
 | 
			
		||||
export interface FeatureLifecycleCompletedSchema {
 | 
			
		||||
    /** The status of the feature after it has been marked as completed */
 | 
			
		||||
    status: FeatureLifecycleCompletedSchemaStatus;
 | 
			
		||||
    /** The metadata value passed in together with status */
 | 
			
		||||
    statusValue?: string;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,17 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Generated by Orval
 | 
			
		||||
 * Do not edit manually.
 | 
			
		||||
 * See `gen:api` script in package.json
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The status of the feature after it has been marked as completed
 | 
			
		||||
 */
 | 
			
		||||
export type FeatureLifecycleCompletedSchemaStatus =
 | 
			
		||||
    (typeof FeatureLifecycleCompletedSchemaStatus)[keyof typeof FeatureLifecycleCompletedSchemaStatus];
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
 | 
			
		||||
export const FeatureLifecycleCompletedSchemaStatus = {
 | 
			
		||||
    kept: 'kept',
 | 
			
		||||
    discarded: 'discarded',
 | 
			
		||||
} as const;
 | 
			
		||||
@ -540,6 +540,8 @@ export * from './featureEnvironmentMetricsSchemaVariants';
 | 
			
		||||
export * from './featureEnvironmentSchema';
 | 
			
		||||
export * from './featureEventsSchema';
 | 
			
		||||
export * from './featureEventsSchemaVersion';
 | 
			
		||||
export * from './featureLifecycleCompletedSchema';
 | 
			
		||||
export * from './featureLifecycleCompletedSchemaStatus';
 | 
			
		||||
export * from './featureLifecycleSchema';
 | 
			
		||||
export * from './featureLifecycleSchemaItem';
 | 
			
		||||
export * from './featureLifecycleSchemaItemStage';
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import {
 | 
			
		||||
    createRequestSchema,
 | 
			
		||||
    createResponseSchema,
 | 
			
		||||
    emptyResponse,
 | 
			
		||||
    type FeatureLifecycleCompletedSchema,
 | 
			
		||||
    featureLifecycleSchema,
 | 
			
		||||
    type FeatureLifecycleSchema,
 | 
			
		||||
    getStandardResponses,
 | 
			
		||||
@ -132,7 +133,11 @@ export default class FeatureLifecycleController extends Controller {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async complete(
 | 
			
		||||
        req: IAuthRequest<FeatureLifecycleParams>,
 | 
			
		||||
        req: IAuthRequest<
 | 
			
		||||
            FeatureLifecycleParams,
 | 
			
		||||
            any,
 | 
			
		||||
            FeatureLifecycleCompletedSchema
 | 
			
		||||
        >,
 | 
			
		||||
        res: Response,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        if (!this.flagResolver.isEnabled('featureLifecycle')) {
 | 
			
		||||
@ -140,8 +145,11 @@ export default class FeatureLifecycleController extends Controller {
 | 
			
		||||
        }
 | 
			
		||||
        const { featureName } = req.params;
 | 
			
		||||
 | 
			
		||||
        const status = req.body;
 | 
			
		||||
 | 
			
		||||
        await this.featureLifecycleService.featureCompleted(
 | 
			
		||||
            featureName,
 | 
			
		||||
            status,
 | 
			
		||||
            req.audit,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ import type { Logger } from '../../logger';
 | 
			
		||||
import type EventService from '../events/event-service';
 | 
			
		||||
import type { ValidatedClientMetrics } from '../metrics/shared/schema';
 | 
			
		||||
import { differenceInMinutes } from 'date-fns';
 | 
			
		||||
import type { FeatureLifecycleCompletedSchema } from '../../openapi';
 | 
			
		||||
 | 
			
		||||
export const STAGE_ENTERED = 'STAGE_ENTERED';
 | 
			
		||||
 | 
			
		||||
@ -167,11 +168,17 @@ export class FeatureLifecycleService extends EventEmitter {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async featureCompleted(feature: string, auditUser: IAuditUser) {
 | 
			
		||||
    public async featureCompleted(
 | 
			
		||||
        feature: string,
 | 
			
		||||
        status: FeatureLifecycleCompletedSchema,
 | 
			
		||||
        auditUser: IAuditUser,
 | 
			
		||||
    ) {
 | 
			
		||||
        await this.featureLifecycleStore.insert([
 | 
			
		||||
            {
 | 
			
		||||
                feature,
 | 
			
		||||
                stage: 'completed',
 | 
			
		||||
                status: status.status,
 | 
			
		||||
                statusValue: status.statusValue,
 | 
			
		||||
            },
 | 
			
		||||
        ]);
 | 
			
		||||
        await this.eventService.storeEvent(
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,8 @@ import type { IFeatureLifecycleStage, StageName } from '../../types';
 | 
			
		||||
export type FeatureLifecycleStage = {
 | 
			
		||||
    feature: string;
 | 
			
		||||
    stage: StageName;
 | 
			
		||||
    status?: string;
 | 
			
		||||
    statusValue?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type FeatureLifecycleView = IFeatureLifecycleStage[];
 | 
			
		||||
 | 
			
		||||
@ -29,28 +29,31 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore {
 | 
			
		||||
    async insert(
 | 
			
		||||
        featureLifecycleStages: FeatureLifecycleStage[],
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const joinedLifecycleStages = featureLifecycleStages
 | 
			
		||||
            .map((stage) => `('${stage.feature}', '${stage.stage}')`)
 | 
			
		||||
            .join(', ');
 | 
			
		||||
        const existingFeatures = await this.db('features')
 | 
			
		||||
            .select('name')
 | 
			
		||||
            .whereIn(
 | 
			
		||||
                'name',
 | 
			
		||||
                featureLifecycleStages.map((stage) => stage.feature),
 | 
			
		||||
            );
 | 
			
		||||
        const existingFeaturesSet = new Set(
 | 
			
		||||
            existingFeatures.map((item) => item.name),
 | 
			
		||||
        );
 | 
			
		||||
        const validStages = featureLifecycleStages.filter((stage) =>
 | 
			
		||||
            existingFeaturesSet.has(stage.feature),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const query = this.db
 | 
			
		||||
            .with(
 | 
			
		||||
                'new_stages',
 | 
			
		||||
                this.db.raw(`
 | 
			
		||||
                    SELECT v.feature, v.stage
 | 
			
		||||
                    FROM (VALUES ${joinedLifecycleStages}) AS v(feature, stage)
 | 
			
		||||
                    JOIN features ON features.name = v.feature
 | 
			
		||||
                    LEFT JOIN feature_lifecycles ON feature_lifecycles.feature = v.feature AND feature_lifecycles.stage = v.stage
 | 
			
		||||
                    WHERE feature_lifecycles.feature IS NULL AND feature_lifecycles.stage IS NULL
 | 
			
		||||
                `),
 | 
			
		||||
        await this.db('feature_lifecycles')
 | 
			
		||||
            .insert(
 | 
			
		||||
                validStages.map((stage) => ({
 | 
			
		||||
                    feature: stage.feature,
 | 
			
		||||
                    stage: stage.stage,
 | 
			
		||||
                    status: stage.status,
 | 
			
		||||
                    status_value: stage.statusValue,
 | 
			
		||||
                })),
 | 
			
		||||
            )
 | 
			
		||||
            .insert((query) => {
 | 
			
		||||
                query.select('feature', 'stage').from('new_stages');
 | 
			
		||||
            })
 | 
			
		||||
            .into('feature_lifecycles')
 | 
			
		||||
            .returning('*')
 | 
			
		||||
            .onConflict(['feature', 'stage'])
 | 
			
		||||
            .ignore();
 | 
			
		||||
        await query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async get(feature: string): Promise<FeatureLifecycleView> {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user