mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +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 './featureEnvironmentSchema';
|
||||||
export * from './featureEventsSchema';
|
export * from './featureEventsSchema';
|
||||||
export * from './featureEventsSchemaVersion';
|
export * from './featureEventsSchemaVersion';
|
||||||
|
export * from './featureLifecycleCompletedSchema';
|
||||||
|
export * from './featureLifecycleCompletedSchemaStatus';
|
||||||
export * from './featureLifecycleSchema';
|
export * from './featureLifecycleSchema';
|
||||||
export * from './featureLifecycleSchemaItem';
|
export * from './featureLifecycleSchemaItem';
|
||||||
export * from './featureLifecycleSchemaItemStage';
|
export * from './featureLifecycleSchemaItemStage';
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
createRequestSchema,
|
createRequestSchema,
|
||||||
createResponseSchema,
|
createResponseSchema,
|
||||||
emptyResponse,
|
emptyResponse,
|
||||||
|
type FeatureLifecycleCompletedSchema,
|
||||||
featureLifecycleSchema,
|
featureLifecycleSchema,
|
||||||
type FeatureLifecycleSchema,
|
type FeatureLifecycleSchema,
|
||||||
getStandardResponses,
|
getStandardResponses,
|
||||||
@ -132,7 +133,11 @@ export default class FeatureLifecycleController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async complete(
|
async complete(
|
||||||
req: IAuthRequest<FeatureLifecycleParams>,
|
req: IAuthRequest<
|
||||||
|
FeatureLifecycleParams,
|
||||||
|
any,
|
||||||
|
FeatureLifecycleCompletedSchema
|
||||||
|
>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.flagResolver.isEnabled('featureLifecycle')) {
|
if (!this.flagResolver.isEnabled('featureLifecycle')) {
|
||||||
@ -140,8 +145,11 @@ export default class FeatureLifecycleController extends Controller {
|
|||||||
}
|
}
|
||||||
const { featureName } = req.params;
|
const { featureName } = req.params;
|
||||||
|
|
||||||
|
const status = req.body;
|
||||||
|
|
||||||
await this.featureLifecycleService.featureCompleted(
|
await this.featureLifecycleService.featureCompleted(
|
||||||
featureName,
|
featureName,
|
||||||
|
status,
|
||||||
req.audit,
|
req.audit,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import type { Logger } from '../../logger';
|
|||||||
import type EventService from '../events/event-service';
|
import type EventService from '../events/event-service';
|
||||||
import type { ValidatedClientMetrics } from '../metrics/shared/schema';
|
import type { ValidatedClientMetrics } from '../metrics/shared/schema';
|
||||||
import { differenceInMinutes } from 'date-fns';
|
import { differenceInMinutes } from 'date-fns';
|
||||||
|
import type { FeatureLifecycleCompletedSchema } from '../../openapi';
|
||||||
|
|
||||||
export const STAGE_ENTERED = 'STAGE_ENTERED';
|
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([
|
await this.featureLifecycleStore.insert([
|
||||||
{
|
{
|
||||||
feature,
|
feature,
|
||||||
stage: 'completed',
|
stage: 'completed',
|
||||||
|
status: status.status,
|
||||||
|
statusValue: status.statusValue,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
await this.eventService.storeEvent(
|
await this.eventService.storeEvent(
|
||||||
|
@ -3,6 +3,8 @@ import type { IFeatureLifecycleStage, StageName } from '../../types';
|
|||||||
export type FeatureLifecycleStage = {
|
export type FeatureLifecycleStage = {
|
||||||
feature: string;
|
feature: string;
|
||||||
stage: StageName;
|
stage: StageName;
|
||||||
|
status?: string;
|
||||||
|
statusValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FeatureLifecycleView = IFeatureLifecycleStage[];
|
export type FeatureLifecycleView = IFeatureLifecycleStage[];
|
||||||
|
@ -29,28 +29,31 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore {
|
|||||||
async insert(
|
async insert(
|
||||||
featureLifecycleStages: FeatureLifecycleStage[],
|
featureLifecycleStages: FeatureLifecycleStage[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const joinedLifecycleStages = featureLifecycleStages
|
const existingFeatures = await this.db('features')
|
||||||
.map((stage) => `('${stage.feature}', '${stage.stage}')`)
|
.select('name')
|
||||||
.join(', ');
|
.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
|
await this.db('feature_lifecycles')
|
||||||
.with(
|
.insert(
|
||||||
'new_stages',
|
validStages.map((stage) => ({
|
||||||
this.db.raw(`
|
feature: stage.feature,
|
||||||
SELECT v.feature, v.stage
|
stage: stage.stage,
|
||||||
FROM (VALUES ${joinedLifecycleStages}) AS v(feature, stage)
|
status: stage.status,
|
||||||
JOIN features ON features.name = v.feature
|
status_value: stage.statusValue,
|
||||||
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
|
|
||||||
`),
|
|
||||||
)
|
)
|
||||||
.insert((query) => {
|
.returning('*')
|
||||||
query.select('feature', 'stage').from('new_stages');
|
|
||||||
})
|
|
||||||
.into('feature_lifecycles')
|
|
||||||
.onConflict(['feature', 'stage'])
|
.onConflict(['feature', 'stage'])
|
||||||
.ignore();
|
.ignore();
|
||||||
await query;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(feature: string): Promise<FeatureLifecycleView> {
|
async get(feature: string): Promise<FeatureLifecycleView> {
|
||||||
|
Loading…
Reference in New Issue
Block a user