mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-27 13:49:10 +02:00
feat: Feature lifecycle controller (#6788)
This commit is contained in:
parent
efda70ac5d
commit
28a3a064b9
@ -0,0 +1,86 @@
|
|||||||
|
import type { FeatureLifecycleService } from './feature-lifecycle-service';
|
||||||
|
import {
|
||||||
|
type IFlagResolver,
|
||||||
|
type IUnleashConfig,
|
||||||
|
type IUnleashServices,
|
||||||
|
NONE,
|
||||||
|
serializeDates,
|
||||||
|
} from '../../types';
|
||||||
|
import type { OpenApiService } from '../../services';
|
||||||
|
import {
|
||||||
|
createResponseSchema,
|
||||||
|
featureLifecycleSchema,
|
||||||
|
type FeatureLifecycleSchema,
|
||||||
|
getStandardResponses,
|
||||||
|
} from '../../openapi';
|
||||||
|
import Controller from '../../routes/controller';
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { NotFoundError } from '../../error';
|
||||||
|
|
||||||
|
interface FeatureLifecycleParams {
|
||||||
|
projectId: string;
|
||||||
|
featureName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PATH = '/:projectId/features/:featureName/lifecycle';
|
||||||
|
|
||||||
|
export default class FeatureLifecycleController extends Controller {
|
||||||
|
private featureLifecycleService: FeatureLifecycleService;
|
||||||
|
|
||||||
|
private openApiService: OpenApiService;
|
||||||
|
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: IUnleashConfig,
|
||||||
|
{
|
||||||
|
featureLifecycleService,
|
||||||
|
openApiService,
|
||||||
|
}: Pick<IUnleashServices, 'openApiService' | 'featureLifecycleService'>,
|
||||||
|
) {
|
||||||
|
super(config);
|
||||||
|
this.featureLifecycleService = featureLifecycleService;
|
||||||
|
this.openApiService = openApiService;
|
||||||
|
this.flagResolver = config.flagResolver;
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'get',
|
||||||
|
path: PATH,
|
||||||
|
handler: this.getFeatureLifecycle,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Unstable'],
|
||||||
|
summary: 'Get feature lifecycle',
|
||||||
|
description:
|
||||||
|
'Information about the lifecycle stages of the feature.',
|
||||||
|
operationId: 'getFeatureLifecycle',
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('featureLifecycleSchema'),
|
||||||
|
...getStandardResponses(401, 403, 404),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFeatureLifecycle(
|
||||||
|
req: Request<FeatureLifecycleParams, any, any, any>,
|
||||||
|
res: Response<FeatureLifecycleSchema>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.flagResolver.isEnabled('featureLifecycle')) {
|
||||||
|
throw new NotFoundError('Feature lifecycle is disabled.');
|
||||||
|
}
|
||||||
|
const { featureName } = req.params;
|
||||||
|
|
||||||
|
const result =
|
||||||
|
await this.featureLifecycleService.getFeatureLifecycle(featureName);
|
||||||
|
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
200,
|
||||||
|
res,
|
||||||
|
featureLifecycleSchema.$id,
|
||||||
|
serializeDates(result),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
|
import {
|
||||||
|
type IUnleashTest,
|
||||||
|
setupAppWithAuth,
|
||||||
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
|
|
||||||
|
let app: IUnleashTest;
|
||||||
|
let db: ITestDb;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('feature_lifecycle', getLogger);
|
||||||
|
app = await setupAppWithAuth(
|
||||||
|
db.stores,
|
||||||
|
{
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
featureLifecycle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db.rawDatabase,
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post(`/auth/demo/login`)
|
||||||
|
.send({
|
||||||
|
email: 'user@getunleash.io',
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {});
|
||||||
|
|
||||||
|
const getFeatureLifecycle = async (featureName: string, expectedCode = 200) => {
|
||||||
|
return app.request
|
||||||
|
.get(`/api/admin/projects/default/features/${featureName}/lifecycle`)
|
||||||
|
.expect(expectedCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should return lifecycle stages', async () => {
|
||||||
|
await app.createFeature('my_feature_a');
|
||||||
|
|
||||||
|
const { body } = await getFeatureLifecycle('my_feature_a');
|
||||||
|
|
||||||
|
expect(body).toEqual([]);
|
||||||
|
});
|
@ -40,6 +40,7 @@ import {
|
|||||||
import { projectApplicationsQueryParameters } from '../../openapi/spec/project-applications-query-parameters';
|
import { projectApplicationsQueryParameters } from '../../openapi/spec/project-applications-query-parameters';
|
||||||
import { normalizeQueryParams } from '../feature-search/search-utils';
|
import { normalizeQueryParams } from '../feature-search/search-utils';
|
||||||
import ProjectInsightsController from '../project-insights/project-insights-controller';
|
import ProjectInsightsController from '../project-insights/project-insights-controller';
|
||||||
|
import FeatureLifecycleController from '../feature-lifecycle/feature-lifecycle-controller';
|
||||||
|
|
||||||
export default class ProjectController extends Controller {
|
export default class ProjectController extends Controller {
|
||||||
private projectService: ProjectService;
|
private projectService: ProjectService;
|
||||||
@ -181,6 +182,7 @@ export default class ProjectController extends Controller {
|
|||||||
).router,
|
).router,
|
||||||
);
|
);
|
||||||
this.use('/', new ProjectInsightsController(config, services).router);
|
this.use('/', new ProjectInsightsController(config, services).router);
|
||||||
|
this.use('/', new FeatureLifecycleController(config, services).router);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(
|
async getProjects(
|
||||||
|
33
src/lib/openapi/spec/feature-lifecycle-schema.ts
Normal file
33
src/lib/openapi/spec/feature-lifecycle-schema.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const featureLifecycleSchema = {
|
||||||
|
$id: '#/components/schemas/featureLifecycleSchema',
|
||||||
|
type: 'array',
|
||||||
|
description: 'A list of lifecycle stages for a given feature',
|
||||||
|
items: {
|
||||||
|
additionalProperties: false,
|
||||||
|
type: 'object',
|
||||||
|
required: ['stage', 'enteredStageAt'],
|
||||||
|
properties: {
|
||||||
|
stage: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['initial', 'pre-live', 'live', 'completed', 'archived'],
|
||||||
|
example: 'initial',
|
||||||
|
description:
|
||||||
|
'The name of the lifecycle stage that got recorded for a given feature',
|
||||||
|
},
|
||||||
|
enteredStageAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
example: '2023-01-28T16:21:39.975Z',
|
||||||
|
description: 'The date when the feature entered a given stage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'The lifecycle stage of the feature',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type FeatureLifecycleSchema = FromSchema<typeof featureLifecycleSchema>;
|
@ -75,6 +75,7 @@ export * from './feature-dependencies-schema';
|
|||||||
export * from './feature-environment-metrics-schema';
|
export * from './feature-environment-metrics-schema';
|
||||||
export * from './feature-environment-schema';
|
export * from './feature-environment-schema';
|
||||||
export * from './feature-events-schema';
|
export * from './feature-events-schema';
|
||||||
|
export * from './feature-lifecycle-schema';
|
||||||
export * from './feature-metrics-schema';
|
export * from './feature-metrics-schema';
|
||||||
export * from './feature-schema';
|
export * from './feature-schema';
|
||||||
export * from './feature-search-environment-schema';
|
export * from './feature-search-environment-schema';
|
||||||
|
Loading…
Reference in New Issue
Block a user