From 79739a1d7f0c73a55c2692ccfe3f306c072f6f6b Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Thu, 9 May 2024 12:05:19 +0300 Subject: [PATCH] feat: add completed status backend (#7022) 1. Implemented controller, service, store for status saving --- .../models/featureLifecycleCompletedSchema.ts | 16 ++++++++ .../featureLifecycleCompletedSchemaStatus.ts | 17 ++++++++ frontend/src/openapi/models/index.ts | 2 + .../feature-lifecycle-controller.ts | 10 ++++- .../feature-lifecycle-service.ts | 9 ++++- .../feature-lifecycle-store-type.ts | 2 + .../feature-lifecycle-store.ts | 39 ++++++++++--------- 7 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 frontend/src/openapi/models/featureLifecycleCompletedSchema.ts create mode 100644 frontend/src/openapi/models/featureLifecycleCompletedSchemaStatus.ts diff --git a/frontend/src/openapi/models/featureLifecycleCompletedSchema.ts b/frontend/src/openapi/models/featureLifecycleCompletedSchema.ts new file mode 100644 index 0000000000..11a0e668a6 --- /dev/null +++ b/frontend/src/openapi/models/featureLifecycleCompletedSchema.ts @@ -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; +} diff --git a/frontend/src/openapi/models/featureLifecycleCompletedSchemaStatus.ts b/frontend/src/openapi/models/featureLifecycleCompletedSchemaStatus.ts new file mode 100644 index 0000000000..bea19fb2bf --- /dev/null +++ b/frontend/src/openapi/models/featureLifecycleCompletedSchemaStatus.ts @@ -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; diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts index c3a95360a3..0c59f3de68 100644 --- a/frontend/src/openapi/models/index.ts +++ b/frontend/src/openapi/models/index.ts @@ -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'; diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-controller.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-controller.ts index be415264a2..0d926520e8 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-controller.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-controller.ts @@ -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, + req: IAuthRequest< + FeatureLifecycleParams, + any, + FeatureLifecycleCompletedSchema + >, res: Response, ): Promise { 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, ); diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts index 38b56ffa47..294dd50070 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts @@ -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( 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 668891c7a6..a0659ffa8a 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts @@ -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[]; diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts index 6893ca8434..4ec14b7e85 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts @@ -29,28 +29,31 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore { async insert( featureLifecycleStages: FeatureLifecycleStage[], ): Promise { - 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 {