1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

fix: split features schema into archived and project features (#7973)

This commit is contained in:
Mateusz Kwasniewski 2024-08-23 12:59:39 +02:00 committed by GitHub
parent 4693f7c598
commit 1f3cc3917e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 354 additions and 89 deletions

View File

@ -9,10 +9,6 @@ import {
import { DELETE_FEATURE, NONE, UPDATE_FEATURE } from '../../types/permissions'; import { DELETE_FEATURE, NONE, UPDATE_FEATURE } from '../../types/permissions';
import type FeatureToggleService from './feature-toggle-service'; import type FeatureToggleService from './feature-toggle-service';
import type { IAuthRequest } from '../../routes/unleash-types'; import type { IAuthRequest } from '../../routes/unleash-types';
import {
featuresSchema,
type FeaturesSchema,
} from '../../openapi/spec/features-schema';
import { serializeDates } from '../../types/serialize-dates'; import { serializeDates } from '../../types/serialize-dates';
import type { OpenApiService } from '../../services/openapi-service'; import type { OpenApiService } from '../../services/openapi-service';
import { createResponseSchema } from '../../openapi/util/create-response-schema'; import { createResponseSchema } from '../../openapi/util/create-response-schema';
@ -24,6 +20,10 @@ import type {
TransactionCreator, TransactionCreator,
UnleashTransaction, UnleashTransaction,
} from '../../db/transaction'; } from '../../db/transaction';
import {
archivedFeaturesSchema,
type ArchivedFeaturesSchema,
} from '../../openapi';
export default class ArchiveController extends Controller { export default class ArchiveController extends Controller {
private featureService: FeatureToggleService; private featureService: FeatureToggleService;
@ -67,7 +67,7 @@ export default class ArchiveController extends Controller {
'Retrieve a list of all [archived feature flags](https://docs.getunleash.io/reference/archived-toggles).', 'Retrieve a list of all [archived feature flags](https://docs.getunleash.io/reference/archived-toggles).',
operationId: 'getArchivedFeatures', operationId: 'getArchivedFeatures',
responses: { responses: {
200: createResponseSchema('featuresSchema'), 200: createResponseSchema('archivedFeaturesSchema'),
...getStandardResponses(401, 403), ...getStandardResponses(401, 403),
}, },
@ -89,7 +89,7 @@ export default class ArchiveController extends Controller {
description: description:
'Retrieves a list of archived features that belong to the provided project.', 'Retrieves a list of archived features that belong to the provided project.',
responses: { responses: {
200: createResponseSchema('featuresSchema'), 200: createResponseSchema('archivedFeaturesSchema'),
...getStandardResponses(401, 403), ...getStandardResponses(401, 403),
}, },
@ -143,7 +143,7 @@ export default class ArchiveController extends Controller {
async getArchivedFeatures( async getArchivedFeatures(
req: IAuthRequest, req: IAuthRequest,
res: Response<FeaturesSchema>, res: Response<ArchivedFeaturesSchema>,
): Promise<void> { ): Promise<void> {
const { user } = req; const { user } = req;
const features = await this.featureService.getAllArchivedFeatures( const features = await this.featureService.getAllArchivedFeatures(
@ -153,14 +153,14 @@ export default class ArchiveController extends Controller {
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
res, res,
featuresSchema.$id, archivedFeaturesSchema.$id,
{ version: 2, features: serializeDates(features) }, { version: 2, features: serializeDates(features) },
); );
} }
async getArchivedFeaturesByProjectId( async getArchivedFeaturesByProjectId(
req: Request<{ projectId: string }, any, any, any>, req: Request<{ projectId: string }, any, any, any>,
res: Response<FeaturesSchema>, res: Response<ArchivedFeaturesSchema>,
): Promise<void> { ): Promise<void> {
const { projectId } = req.params; const { projectId } = req.params;
const features = const features =
@ -171,7 +171,7 @@ export default class ArchiveController extends Controller {
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
res, res,
featuresSchema.$id, archivedFeaturesSchema.$id,
{ version: 2, features: serializeDates(features) }, { version: 2, features: serializeDates(features) },
); );
} }

View File

@ -30,12 +30,11 @@ import {
type FeatureEnvironmentSchema, type FeatureEnvironmentSchema,
featureSchema, featureSchema,
type FeatureSchema, type FeatureSchema,
featuresSchema,
type FeaturesSchema,
featureStrategySchema, featureStrategySchema,
type FeatureStrategySchema, type FeatureStrategySchema,
getStandardResponses, getStandardResponses,
type ParametersSchema, projectFeaturesSchema,
type ProjectFeaturesSchema,
type SetStrategySortOrderSchema, type SetStrategySortOrderSchema,
type TagsBulkAddSchema, type TagsBulkAddSchema,
type TagSchema, type TagSchema,
@ -421,7 +420,7 @@ export default class ProjectFeaturesController extends Controller {
tags: ['Features'], tags: ['Features'],
operationId: 'getFeatures', operationId: 'getFeatures',
responses: { responses: {
200: createResponseSchema('featuresSchema'), 200: createResponseSchema('projectFeaturesSchema'),
...getStandardResponses(400, 401, 403), ...getStandardResponses(400, 401, 403),
}, },
}), }),
@ -606,7 +605,7 @@ export default class ProjectFeaturesController extends Controller {
async getFeatures( async getFeatures(
req: IAuthRequest<ProjectParam, any, any, AdminFeaturesQuerySchema>, req: IAuthRequest<ProjectParam, any, any, AdminFeaturesQuerySchema>,
res: Response<FeaturesSchema>, res: Response<ProjectFeaturesSchema>,
): Promise<void> { ): Promise<void> {
const { projectId } = req.params; const { projectId } = req.params;
const query = await this.prepQuery(req.query, projectId); const query = await this.prepQuery(req.query, projectId);
@ -617,7 +616,7 @@ export default class ProjectFeaturesController extends Controller {
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
res, res,
featuresSchema.$id, projectFeaturesSchema.$id,
{ version: 2, features: serializeDates(features) }, { version: 2, features: serializeDates(features) },
); );
} }
@ -1155,14 +1154,4 @@ export default class ProjectFeaturesController extends Controller {
); );
res.status(200).end(); res.status(200).end();
} }
async getStrategyParameters(
req: Request<StrategyIdParams, any, any, any>,
res: Response<ParametersSchema>,
): Promise<void> {
this.logger.info('Getting strategy parameters');
const { strategyId } = req.params;
const strategy = await this.featureService.getStrategy(strategyId);
res.status(200).json(strategy.parameters);
}
} }

View File

@ -0,0 +1,102 @@
import type { FromSchema } from 'json-schema-to-ts';
export const archivedFeatureSchema = {
$id: '#/components/schemas/archivedFeatureSchema',
type: 'object',
additionalProperties: false,
required: ['name', 'project'],
description: 'An archived project feature flag definition',
properties: {
name: {
type: 'string',
example: 'disable-comments',
description: 'Unique feature name',
},
type: {
type: 'string',
example: 'kill-switch',
description:
'Type of the flag e.g. experiment, kill-switch, release, operational, permission',
},
description: {
type: 'string',
nullable: true,
example:
'Controls disabling of the comments section in case of an incident',
description: 'Detailed description of the feature',
},
project: {
type: 'string',
example: 'dx-squad',
description: 'Name of the project the feature belongs to',
},
stale: {
type: 'boolean',
example: false,
description:
'`true` if the feature is stale based on the age and feature type, otherwise `false`.',
},
impressionData: {
type: 'boolean',
example: false,
description:
'`true` if the impression data collection is enabled for the feature, otherwise `false`.',
},
createdAt: {
type: 'string',
format: 'date-time',
example: '2023-01-28T15:21:39.975Z',
description: 'The date the feature was created',
},
archivedAt: {
type: 'string',
format: 'date-time',
example: '2023-01-29T15:21:39.975Z',
description: 'The date the feature was archived',
},
lastSeenAt: {
type: 'string',
format: 'date-time',
nullable: true,
deprecated: true,
example: '2023-01-28T16:21:39.975Z',
description:
'The date when metrics where last collected for the feature. This field was deprecated in v5, use the one in featureEnvironmentSchema',
},
environments: {
type: 'array',
deprecated: true,
description:
'The list of environments where the feature can be used',
items: {
type: 'object',
properties: {
name: {
type: 'string',
example: 'my-dev-env',
description: 'The name of the environment',
},
lastSeenAt: {
type: 'string',
format: 'date-time',
nullable: true,
example: '2023-01-28T16:21:39.975Z',
description:
'The date when metrics where last collected for the feature environment',
},
enabled: {
type: 'boolean',
example: true,
description:
'`true` if the feature is enabled for the environment, otherwise `false`.',
},
},
},
},
},
components: {
schemas: {},
},
} as const;
export type ArchivedFeatureSchema = FromSchema<typeof archivedFeatureSchema>;

View File

@ -0,0 +1,31 @@
import type { FromSchema } from 'json-schema-to-ts';
import { archivedFeatureSchema } from './archived-feature-schema';
export const archivedFeaturesSchema = {
$id: '#/components/schemas/archivedFeaturesSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'features'],
description: 'A list of archived features',
deprecated: true,
properties: {
version: {
type: 'integer',
description: "The version of the feature's schema",
},
features: {
type: 'array',
items: {
$ref: '#/components/schemas/archivedFeatureSchema',
},
description: 'A list of features',
},
},
components: {
schemas: {
archivedFeatureSchema,
},
},
} as const;
export type ArchivedFeaturesSchema = FromSchema<typeof archivedFeaturesSchema>;

View File

@ -1,13 +0,0 @@
import { validateSchema } from '../validate';
import type { FeaturesSchema } from './features-schema';
test('featuresSchema', () => {
const data: FeaturesSchema = {
version: 1,
features: [],
};
expect(
validateSchema('#/components/schemas/featuresSchema', data),
).toBeUndefined();
});

View File

@ -1,49 +0,0 @@
import type { FromSchema } from 'json-schema-to-ts';
import { featureSchema } from './feature-schema';
import { parametersSchema } from './parameters-schema';
import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema';
import { constraintSchema } from './constraint-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { environmentSchema } from './environment-schema';
import { featureEnvironmentSchema } from './feature-environment-schema';
import { strategyVariantSchema } from './strategy-variant-schema';
import { tagSchema } from './tag-schema';
export const featuresSchema = {
$id: '#/components/schemas/featuresSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'features'],
description: 'A list of features',
deprecated: true,
properties: {
version: {
type: 'integer',
description: "The version of the feature's schema",
},
features: {
type: 'array',
items: {
$ref: '#/components/schemas/featureSchema',
},
description: 'A list of features',
},
},
components: {
schemas: {
constraintSchema,
environmentSchema,
featureSchema,
overrideSchema,
featureEnvironmentSchema,
featureStrategySchema,
strategyVariantSchema,
parametersSchema,
variantSchema,
tagSchema,
},
},
} as const;
export type FeaturesSchema = FromSchema<typeof featuresSchema>;

View File

@ -24,6 +24,8 @@ export * from './application-overview-schema';
export * from './application-schema'; export * from './application-schema';
export * from './application-usage-schema'; export * from './application-usage-schema';
export * from './applications-schema'; export * from './applications-schema';
export * from './archived-feature-schema';
export * from './archived-features-schema';
export * from './batch-features-schema'; export * from './batch-features-schema';
export * from './batch-stale-schema'; export * from './batch-stale-schema';
export * from './bulk-metrics-schema'; export * from './bulk-metrics-schema';
@ -93,7 +95,6 @@ export * from './feature-type-schema';
export * from './feature-types-schema'; export * from './feature-types-schema';
export * from './feature-usage-schema'; export * from './feature-usage-schema';
export * from './feature-variants-schema'; export * from './feature-variants-schema';
export * from './features-schema';
export * from './feedback-create-schema'; export * from './feedback-create-schema';
export * from './feedback-response-schema'; export * from './feedback-response-schema';
export * from './feedback-update-schema'; export * from './feedback-update-schema';
@ -144,6 +145,9 @@ export * from './project-application-sdk-schema';
export * from './project-applications-schema'; export * from './project-applications-schema';
export * from './project-dora-metrics-schema'; export * from './project-dora-metrics-schema';
export * from './project-environment-schema'; export * from './project-environment-schema';
export * from './project-feature-environment-schema';
export * from './project-feature-schema';
export * from './project-features-schema';
export * from './project-flag-creators-schema'; export * from './project-flag-creators-schema';
export * from './project-insights-schema'; export * from './project-insights-schema';
export * from './project-overview-schema'; export * from './project-overview-schema';

View File

@ -0,0 +1,68 @@
import type { FromSchema } from 'json-schema-to-ts';
export const projectFeatureEnvironmentSchema = {
$id: '#/components/schemas/projectFeatureEnvironmentSchema',
type: 'object',
additionalProperties: false,
required: [
'name',
'type',
'enabled',
'sortOrder',
'variantCount',
'lastSeenAt',
],
description: 'A detailed description of the feature environment',
properties: {
name: {
type: 'string',
example: 'my-dev-env',
description: 'The name of the environment',
},
type: {
type: 'string',
example: 'development',
description: 'The type of the environment',
},
enabled: {
type: 'boolean',
example: true,
description:
'`true` if the feature is enabled for the environment, otherwise `false`.',
},
sortOrder: {
type: 'number',
example: 3,
description:
'The sort order of the feature environment in the feature environments list',
},
variantCount: {
type: 'number',
description: 'The number of defined variants',
},
lastSeenAt: {
type: 'string',
format: 'date-time',
nullable: true,
example: '2023-01-28T16:21:39.975Z',
description:
'The date when metrics where last collected for the feature environment',
},
hasStrategies: {
type: 'boolean',
description: 'Whether the feature has any strategies defined.',
},
hasEnabledStrategies: {
type: 'boolean',
description:
'Whether the feature has any enabled strategies defined.',
},
},
components: {
schemas: {},
},
} as const;
export type ProjectFeatureEnvironmentSchema = FromSchema<
typeof projectFeatureEnvironmentSchema
>;

View File

@ -0,0 +1,98 @@
import type { FromSchema } from 'json-schema-to-ts';
import { tagSchema } from './tag-schema';
import { projectFeatureEnvironmentSchema } from './project-feature-environment-schema';
export const projectFeatureSchema = {
$id: '#/components/schemas/projectFeatureSchema',
type: 'object',
additionalProperties: false,
required: [
'name',
'type',
'description',
'stale',
'favorite',
'impressionData',
'createdAt',
'lastSeenAt',
'environments',
],
description: 'A project feature flag definition',
properties: {
name: {
type: 'string',
example: 'disable-comments',
description: 'Unique feature name',
},
type: {
type: 'string',
example: 'kill-switch',
description:
'Type of the flag e.g. experiment, kill-switch, release, operational, permission',
},
description: {
type: 'string',
nullable: true,
example:
'Controls disabling of the comments section in case of an incident',
description: 'Detailed description of the feature',
},
stale: {
type: 'boolean',
example: false,
description:
'`true` if the feature is stale based on the age and feature type, otherwise `false`.',
},
favorite: {
type: 'boolean',
example: true,
description:
'`true` if the feature was favorited, otherwise `false`.',
},
impressionData: {
type: 'boolean',
example: false,
description:
'`true` if the impression data collection is enabled for the feature, otherwise `false`.',
},
createdAt: {
type: 'string',
format: 'date-time',
example: '2023-01-28T15:21:39.975Z',
description: 'The date the feature was created',
},
lastSeenAt: {
type: 'string',
format: 'date-time',
nullable: true,
deprecated: true,
example: '2023-01-28T16:21:39.975Z',
description:
'The date and time when metrics where last collected for this flag in any environment. This field was deprecated in v5. You should instead use the `lastSeenAt` property on the individual environments listed under the `environments` property.',
},
environments: {
type: 'array',
items: {
$ref: '#/components/schemas/projectFeatureEnvironmentSchema',
},
description:
'The list of environments where the feature can be used',
},
tags: {
type: 'array',
items: {
$ref: '#/components/schemas/tagSchema',
},
nullable: true,
description: 'The list of feature tags',
},
},
components: {
schemas: {
projectFeatureEnvironmentSchema,
tagSchema,
},
},
} as const;
export type ProjectFeatureSchema = FromSchema<typeof projectFeatureSchema>;

View File

@ -0,0 +1,35 @@
import type { FromSchema } from 'json-schema-to-ts';
import { tagSchema } from './tag-schema';
import { projectFeatureSchema } from './project-feature-schema';
import { projectFeatureEnvironmentSchema } from './project-feature-environment-schema';
export const projectFeaturesSchema = {
$id: '#/components/schemas/projectFeaturesSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'features'],
description: 'A list of features in a project',
deprecated: true,
properties: {
version: {
type: 'integer',
description: "The version of the feature's schema",
},
features: {
type: 'array',
items: {
$ref: '#/components/schemas/projectFeatureSchema',
},
description: 'A list of features',
},
},
components: {
schemas: {
projectFeatureSchema,
projectFeatureEnvironmentSchema,
tagSchema,
},
},
} as const;
export type ProjectFeaturesSchema = FromSchema<typeof projectFeaturesSchema>;