mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
1140 lines
38 KiB
TypeScript
1140 lines
38 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import { applyPatch, Operation } from 'fast-json-patch';
|
|
import Controller from '../../routes/controller';
|
|
import {
|
|
CREATE_FEATURE,
|
|
CREATE_FEATURE_STRATEGY,
|
|
DELETE_FEATURE,
|
|
DELETE_FEATURE_STRATEGY,
|
|
IFlagResolver,
|
|
IUnleashConfig,
|
|
IUnleashServices,
|
|
NONE,
|
|
serializeDates,
|
|
UPDATE_FEATURE,
|
|
UPDATE_FEATURE_ENVIRONMENT,
|
|
UPDATE_FEATURE_STRATEGY,
|
|
} from '../../types';
|
|
import { Logger } from '../../logger';
|
|
import { extractUsername } from '../../util';
|
|
import { IAuthRequest } from '../../routes/unleash-types';
|
|
import {
|
|
AdminFeaturesQuerySchema,
|
|
BulkToggleFeaturesSchema,
|
|
CreateFeatureSchema,
|
|
CreateFeatureStrategySchema,
|
|
createRequestSchema,
|
|
createResponseSchema,
|
|
emptyResponse,
|
|
featureEnvironmentSchema,
|
|
FeatureEnvironmentSchema,
|
|
featureSchema,
|
|
FeatureSchema,
|
|
featuresSchema,
|
|
FeaturesSchema,
|
|
FeatureStrategySchema,
|
|
getStandardResponses,
|
|
ParametersSchema,
|
|
SetStrategySortOrderSchema,
|
|
TagsBulkAddSchema,
|
|
TagSchema,
|
|
UpdateFeatureSchema,
|
|
UpdateFeatureStrategySchema,
|
|
} from '../../openapi';
|
|
import {
|
|
FeatureTagService,
|
|
FeatureToggleService,
|
|
OpenApiService,
|
|
} from '../../services';
|
|
import { querySchema } from '../../schema/feature-schema';
|
|
import { BatchStaleSchema } from '../../openapi/spec/batch-stale-schema';
|
|
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
|
|
import { BadDataError } from '../../error';
|
|
|
|
interface FeatureStrategyParams {
|
|
projectId: string;
|
|
featureName: string;
|
|
environment: string;
|
|
sortOrder?: number;
|
|
}
|
|
|
|
interface BulkFeaturesStrategyParams {
|
|
projectId: string;
|
|
environment: string;
|
|
}
|
|
|
|
interface FeatureStrategyQuery {
|
|
shouldActivateDisabledStrategies: string;
|
|
}
|
|
|
|
interface FeatureParams extends ProjectParam {
|
|
featureName: string;
|
|
}
|
|
|
|
interface ProjectParam {
|
|
projectId: string;
|
|
}
|
|
|
|
interface StrategyIdParams extends FeatureStrategyParams {
|
|
strategyId: string;
|
|
}
|
|
|
|
export interface IFeatureProjectUserParams extends ProjectParam {
|
|
archived?: boolean;
|
|
userId?: number;
|
|
|
|
tag?: string[][];
|
|
namePrefix?: string;
|
|
}
|
|
|
|
const PATH = '/:projectId/features';
|
|
const PATH_STALE = '/:projectId/stale';
|
|
const PATH_TAGS = `/:projectId/tags`;
|
|
const PATH_FEATURE = `${PATH}/:featureName`;
|
|
const PATH_FEATURE_CLONE = `${PATH_FEATURE}/clone`;
|
|
const PATH_ENV = `${PATH_FEATURE}/environments/:environment`;
|
|
const BULK_PATH_ENV = `/:projectId/bulk_features/environments/:environment`;
|
|
const PATH_STRATEGIES = `${PATH_ENV}/strategies`;
|
|
const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
|
|
|
|
type ProjectFeaturesServices = Pick<
|
|
IUnleashServices,
|
|
| 'featureToggleServiceV2'
|
|
| 'projectHealthService'
|
|
| 'openApiService'
|
|
| 'transactionalFeatureToggleService'
|
|
| 'featureTagService'
|
|
>;
|
|
|
|
export default class ProjectFeaturesController extends Controller {
|
|
private featureService: FeatureToggleService;
|
|
|
|
private featureTagService: FeatureTagService;
|
|
|
|
private transactionalFeatureToggleService: (
|
|
db: UnleashTransaction,
|
|
) => FeatureToggleService;
|
|
|
|
private openApiService: OpenApiService;
|
|
|
|
private flagResolver: IFlagResolver;
|
|
|
|
private readonly logger: Logger;
|
|
|
|
private readonly startTransaction: TransactionCreator<UnleashTransaction>;
|
|
|
|
constructor(
|
|
config: IUnleashConfig,
|
|
{
|
|
featureToggleServiceV2,
|
|
openApiService,
|
|
transactionalFeatureToggleService,
|
|
featureTagService,
|
|
}: ProjectFeaturesServices,
|
|
startTransaction: TransactionCreator<UnleashTransaction>,
|
|
) {
|
|
super(config);
|
|
this.featureService = featureToggleServiceV2;
|
|
this.transactionalFeatureToggleService =
|
|
transactionalFeatureToggleService;
|
|
this.startTransaction = startTransaction;
|
|
this.openApiService = openApiService;
|
|
this.featureTagService = featureTagService;
|
|
this.flagResolver = config.flagResolver;
|
|
this.logger = config.getLogger('/admin-api/project/features.ts');
|
|
|
|
this.route({
|
|
method: 'get',
|
|
path: PATH_ENV,
|
|
permission: NONE,
|
|
handler: this.getFeatureEnvironment,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
summary: 'Get a feature environment',
|
|
description:
|
|
'Information about the enablement status and strategies for a feature toggle in specified environment.',
|
|
tags: ['Features'],
|
|
operationId: 'getFeatureEnvironment',
|
|
responses: {
|
|
200: createResponseSchema('featureEnvironmentSchema'),
|
|
...getStandardResponses(401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: `${PATH_ENV}/off`,
|
|
acceptAnyContentType: true,
|
|
handler: this.toggleFeatureEnvironmentOff,
|
|
permission: UPDATE_FEATURE_ENVIRONMENT,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
summary: 'Disable a feature toggle',
|
|
description:
|
|
'Disable a feature toggle in the specified environment.',
|
|
tags: ['Features'],
|
|
operationId: 'toggleFeatureEnvironmentOff',
|
|
responses: {
|
|
200: createResponseSchema('featureSchema'),
|
|
...getStandardResponses(400, 401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: `${PATH_ENV}/on`,
|
|
acceptAnyContentType: true,
|
|
handler: this.toggleFeatureEnvironmentOn,
|
|
permission: UPDATE_FEATURE_ENVIRONMENT,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
summary: 'Enable a feature toggle',
|
|
description:
|
|
'Enable a feature toggle in the specified environment.',
|
|
tags: ['Features'],
|
|
operationId: 'toggleFeatureEnvironmentOn',
|
|
responses: {
|
|
200: createResponseSchema('featureSchema'),
|
|
...getStandardResponses(400, 401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: `${BULK_PATH_ENV}/on`,
|
|
handler: this.bulkToggleFeaturesEnvironmentOn,
|
|
permission: UPDATE_FEATURE_ENVIRONMENT,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
summary: 'Bulk enable a list of features',
|
|
description:
|
|
'This endpoint enables multiple feature toggles.',
|
|
operationId: 'bulkToggleFeaturesEnvironmentOn',
|
|
requestBody: createRequestSchema(
|
|
'bulkToggleFeaturesSchema',
|
|
),
|
|
responses: {
|
|
200: emptyResponse,
|
|
...getStandardResponses(400, 401, 403, 404, 413, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: `${BULK_PATH_ENV}/off`,
|
|
handler: this.bulkToggleFeaturesEnvironmentOff,
|
|
permission: UPDATE_FEATURE_ENVIRONMENT,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
summary: 'Bulk disable a list of features',
|
|
description:
|
|
'This endpoint disables multiple feature toggles.',
|
|
operationId: 'bulkToggleFeaturesEnvironmentOff',
|
|
requestBody: createRequestSchema(
|
|
'bulkToggleFeaturesSchema',
|
|
),
|
|
responses: {
|
|
200: emptyResponse,
|
|
...getStandardResponses(400, 401, 403, 404, 413, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'get',
|
|
path: PATH_STRATEGIES,
|
|
handler: this.getFeatureStrategies,
|
|
permission: NONE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
summary: 'Get feature toggle strategies',
|
|
operationId: 'getFeatureStrategies',
|
|
description:
|
|
'Get strategies defined for a feature toggle in the specified environment.',
|
|
responses: {
|
|
200: createResponseSchema('featureStrategySchema'),
|
|
...getStandardResponses(401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: PATH_STRATEGIES,
|
|
handler: this.addFeatureStrategy,
|
|
permission: CREATE_FEATURE_STRATEGY,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
summary: 'Add a strategy to a feature toggle',
|
|
description:
|
|
'Add a strategy to a feature toggle in the specified environment.',
|
|
operationId: 'addFeatureStrategy',
|
|
requestBody: createRequestSchema(
|
|
'createFeatureStrategySchema',
|
|
),
|
|
responses: {
|
|
200: createResponseSchema('featureStrategySchema'),
|
|
...getStandardResponses(401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'get',
|
|
path: PATH_STRATEGY,
|
|
handler: this.getFeatureStrategy,
|
|
permission: NONE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
summary: 'Get a strategy configuration',
|
|
description:
|
|
'Get a strategy configuration for an environment in a feature toggle.',
|
|
operationId: 'getFeatureStrategy',
|
|
responses: {
|
|
200: createResponseSchema('featureStrategySchema'),
|
|
...getStandardResponses(401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: `${PATH_STRATEGIES}/set-sort-order`,
|
|
handler: this.setStrategiesSortOrder,
|
|
permission: UPDATE_FEATURE_STRATEGY,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
operationId: 'setStrategySortOrder',
|
|
summary: 'Set strategy sort order',
|
|
description:
|
|
'Set the sort order of the provided list of strategies.',
|
|
requestBody: createRequestSchema(
|
|
'setStrategySortOrderSchema',
|
|
),
|
|
responses: {
|
|
200: emptyResponse,
|
|
...getStandardResponses(400, 401, 403),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'put',
|
|
path: PATH_STRATEGY,
|
|
handler: this.updateFeatureStrategy,
|
|
permission: UPDATE_FEATURE_STRATEGY,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
summary: 'Update a strategy',
|
|
description:
|
|
'Replace strategy configuration for a feature toggle in the specified environment.',
|
|
operationId: 'updateFeatureStrategy',
|
|
requestBody: createRequestSchema(
|
|
'updateFeatureStrategySchema',
|
|
),
|
|
responses: {
|
|
200: createResponseSchema('featureStrategySchema'),
|
|
...getStandardResponses(400, 401, 403, 404, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'patch',
|
|
path: PATH_STRATEGY,
|
|
handler: this.patchFeatureStrategy,
|
|
permission: UPDATE_FEATURE_STRATEGY,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
summary: 'Change specific properties of a strategy',
|
|
description:
|
|
'Change specific properties of a strategy configuration in a feature toggle.',
|
|
operationId: 'patchFeatureStrategy',
|
|
requestBody: createRequestSchema('patchesSchema'),
|
|
responses: {
|
|
200: createResponseSchema('featureStrategySchema'),
|
|
...getStandardResponses(400, 401, 403, 404, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'delete',
|
|
path: PATH_STRATEGY,
|
|
acceptAnyContentType: true,
|
|
handler: this.deleteFeatureStrategy,
|
|
permission: DELETE_FEATURE_STRATEGY,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
summary: 'Delete a strategy from a feature toggle',
|
|
description:
|
|
'Delete a strategy configuration from a feature toggle in the specified environment.',
|
|
operationId: 'deleteFeatureStrategy',
|
|
responses: {
|
|
200: emptyResponse,
|
|
...getStandardResponses(401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'get',
|
|
path: PATH,
|
|
handler: this.getFeatures,
|
|
permission: NONE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
summary: 'Get all features in a project',
|
|
description:
|
|
'A list of all features for the specified project.',
|
|
tags: ['Features'],
|
|
operationId: 'getFeatures',
|
|
responses: {
|
|
200: createResponseSchema('featuresSchema'),
|
|
...getStandardResponses(400, 401, 403),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: PATH,
|
|
handler: this.createFeature,
|
|
permission: CREATE_FEATURE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
summary: 'Add a new feature toggle',
|
|
description:
|
|
'Create a new feature toggle in a specified project.',
|
|
tags: ['Features'],
|
|
operationId: 'createFeature',
|
|
requestBody: createRequestSchema('createFeatureSchema'),
|
|
responses: {
|
|
200: createResponseSchema('featureSchema'),
|
|
...getStandardResponses(401, 403, 404, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: PATH_FEATURE_CLONE,
|
|
acceptAnyContentType: true,
|
|
handler: this.cloneFeature,
|
|
permission: CREATE_FEATURE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
summary: 'Clone a feature toggle',
|
|
description:
|
|
'Creates a copy of the specified feature toggle. The copy can be created in any project.',
|
|
tags: ['Features'],
|
|
operationId: 'cloneFeature',
|
|
requestBody: createRequestSchema('cloneFeatureSchema'),
|
|
responses: {
|
|
200: createResponseSchema('featureSchema'),
|
|
...getStandardResponses(401, 403, 404, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'get',
|
|
path: PATH_FEATURE,
|
|
handler: this.getFeature,
|
|
permission: NONE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
operationId: 'getFeature',
|
|
tags: ['Features'],
|
|
summary: 'Get a feature',
|
|
description:
|
|
'This endpoint returns the information about the requested feature if the feature belongs to the specified project.',
|
|
responses: {
|
|
200: createResponseSchema('featureSchema'),
|
|
403: {
|
|
description:
|
|
'You either do not have the required permissions or used an invalid URL.',
|
|
},
|
|
...getStandardResponses(401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'put',
|
|
path: PATH_FEATURE,
|
|
handler: this.updateFeature,
|
|
permission: UPDATE_FEATURE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
operationId: 'updateFeature',
|
|
summary: 'Update a feature toggle',
|
|
description:
|
|
'Updates the specified feature if the feature belongs to the specified project. Only the provided properties are updated; any feature properties left out of the request body are left untouched.',
|
|
requestBody: createRequestSchema('updateFeatureSchema'),
|
|
responses: {
|
|
200: createResponseSchema('featureSchema'),
|
|
...getStandardResponses(401, 403, 404, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'patch',
|
|
path: PATH_FEATURE,
|
|
handler: this.patchFeature,
|
|
permission: UPDATE_FEATURE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
operationId: 'patchFeature',
|
|
summary: 'Modify a feature toggle',
|
|
description:
|
|
'Change specific properties of a feature toggle.',
|
|
requestBody: createRequestSchema('patchesSchema'),
|
|
responses: {
|
|
200: createResponseSchema('featureSchema'),
|
|
...getStandardResponses(401, 403, 404, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'delete',
|
|
path: PATH_FEATURE,
|
|
acceptAnyContentType: true,
|
|
handler: this.archiveFeature,
|
|
permission: DELETE_FEATURE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
operationId: 'archiveFeature',
|
|
summary: 'Archive a feature toggle',
|
|
description:
|
|
'This endpoint archives the specified feature if the feature belongs to the specified project.',
|
|
responses: {
|
|
202: emptyResponse,
|
|
403: {
|
|
description:
|
|
'You either do not have the required permissions or used an invalid URL.',
|
|
},
|
|
...getStandardResponses(401, 403, 404),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'post',
|
|
path: PATH_STALE,
|
|
handler: this.staleFeatures,
|
|
permission: UPDATE_FEATURE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Features'],
|
|
operationId: 'staleFeatures',
|
|
summary: 'Mark features as stale / not stale',
|
|
description: `This endpoint marks the provided list of features as either [stale](https://docs.getunleash.io/reference/technical-debt#stale-and-potentially-stale-toggles) or not stale depending on the request body you send. Any provided features that don't exist are ignored.`,
|
|
requestBody: createRequestSchema('batchStaleSchema'),
|
|
responses: {
|
|
202: emptyResponse,
|
|
...getStandardResponses(401, 403, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
this.route({
|
|
method: 'put',
|
|
path: PATH_TAGS,
|
|
handler: this.updateFeaturesTags,
|
|
permission: UPDATE_FEATURE,
|
|
middleware: [
|
|
openApiService.validPath({
|
|
tags: ['Tags'],
|
|
operationId: 'addTagToFeatures',
|
|
summary: 'Adds a tag to the specified features',
|
|
description:
|
|
'Add a tag to a list of features. Create tags if needed.',
|
|
requestBody: createRequestSchema('tagsBulkAddSchema'),
|
|
responses: {
|
|
200: emptyResponse,
|
|
...getStandardResponses(401, 403, 404, 415),
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
}
|
|
|
|
async getFeatures(
|
|
req: IAuthRequest<ProjectParam, any, any, AdminFeaturesQuerySchema>,
|
|
res: Response<FeaturesSchema>,
|
|
): Promise<void> {
|
|
const { projectId } = req.params;
|
|
const query = await this.prepQuery(req.query, projectId);
|
|
const features = await this.featureService.getFeatureOverview({
|
|
...query,
|
|
userId: req.user.id,
|
|
});
|
|
this.openApiService.respondWithValidation(
|
|
200,
|
|
res,
|
|
featuresSchema.$id,
|
|
{ version: 2, features: serializeDates(features) },
|
|
);
|
|
}
|
|
|
|
async prepQuery(
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
{ tag, namePrefix }: AdminFeaturesQuerySchema,
|
|
projectId: string,
|
|
): Promise<IFeatureProjectUserParams> {
|
|
if (!tag && !namePrefix) {
|
|
return { projectId };
|
|
}
|
|
const tagQuery = this.paramToArray(tag);
|
|
const query = await querySchema.validateAsync({
|
|
tag: tagQuery,
|
|
namePrefix,
|
|
});
|
|
if (query.tag) {
|
|
query.tag = query.tag.map((q) => q.split(':'));
|
|
}
|
|
return { projectId, ...query };
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
paramToArray(param: any): Array<any> {
|
|
if (!param) {
|
|
return param;
|
|
}
|
|
return Array.isArray(param) ? param : [param];
|
|
}
|
|
|
|
async cloneFeature(
|
|
req: IAuthRequest<
|
|
FeatureParams,
|
|
any,
|
|
{ name: string; replaceGroupId?: boolean }
|
|
>,
|
|
res: Response<FeatureSchema>,
|
|
): Promise<void> {
|
|
const { projectId, featureName } = req.params;
|
|
const { name, replaceGroupId } = req.body;
|
|
const userName = extractUsername(req);
|
|
const created = await this.featureService.cloneFeatureToggle(
|
|
featureName,
|
|
projectId,
|
|
name,
|
|
userName,
|
|
replaceGroupId,
|
|
);
|
|
|
|
this.openApiService.respondWithValidation(
|
|
201,
|
|
res,
|
|
featureSchema.$id,
|
|
serializeDates(created),
|
|
);
|
|
}
|
|
|
|
async createFeature(
|
|
req: IAuthRequest<FeatureParams, FeatureSchema, CreateFeatureSchema>,
|
|
res: Response<FeatureSchema>,
|
|
): Promise<void> {
|
|
const { projectId } = req.params;
|
|
|
|
const userName = extractUsername(req);
|
|
const created = await this.featureService.createFeatureToggle(
|
|
projectId,
|
|
{
|
|
...req.body,
|
|
description: req.body.description || undefined,
|
|
},
|
|
userName,
|
|
);
|
|
|
|
this.openApiService.respondWithValidation(
|
|
201,
|
|
res,
|
|
featureSchema.$id,
|
|
serializeDates(created),
|
|
);
|
|
}
|
|
|
|
async getFeature(
|
|
req: IAuthRequest<FeatureParams, any, any, any>,
|
|
res: Response,
|
|
): Promise<void> {
|
|
const { featureName, projectId } = req.params;
|
|
const { variantEnvironments } = req.query;
|
|
const { user } = req;
|
|
const feature = await this.featureService.getFeature({
|
|
featureName,
|
|
archived: false,
|
|
projectId,
|
|
environmentVariants: variantEnvironments === 'true',
|
|
userId: user.id,
|
|
});
|
|
res.status(200).json(feature);
|
|
}
|
|
|
|
async updateFeature(
|
|
req: IAuthRequest<
|
|
{ projectId: string; featureName: string },
|
|
any,
|
|
UpdateFeatureSchema
|
|
>,
|
|
res: Response<FeatureSchema>,
|
|
): Promise<void> {
|
|
const { projectId, featureName } = req.params;
|
|
const { createdAt, ...data } = req.body;
|
|
const userName = extractUsername(req);
|
|
if (data.name && data.name !== featureName) {
|
|
throw new BadDataError('Cannot change name of feature toggle');
|
|
}
|
|
const created = await this.featureService.updateFeatureToggle(
|
|
projectId,
|
|
{
|
|
...data,
|
|
name: featureName,
|
|
},
|
|
userName,
|
|
featureName,
|
|
);
|
|
|
|
this.openApiService.respondWithValidation(
|
|
200,
|
|
res,
|
|
featureSchema.$id,
|
|
serializeDates(created),
|
|
);
|
|
}
|
|
|
|
async patchFeature(
|
|
req: IAuthRequest<
|
|
{ projectId: string; featureName: string },
|
|
any,
|
|
Operation[],
|
|
any
|
|
>,
|
|
res: Response<FeatureSchema>,
|
|
): Promise<void> {
|
|
const { projectId, featureName } = req.params;
|
|
const updated = await this.featureService.patchFeature(
|
|
projectId,
|
|
featureName,
|
|
extractUsername(req),
|
|
req.body,
|
|
);
|
|
this.openApiService.respondWithValidation(
|
|
200,
|
|
res,
|
|
featureSchema.$id,
|
|
serializeDates(updated),
|
|
);
|
|
}
|
|
|
|
async archiveFeature(
|
|
req: IAuthRequest<
|
|
{ projectId: string; featureName: string },
|
|
any,
|
|
any,
|
|
any
|
|
>,
|
|
res: Response<void>,
|
|
): Promise<void> {
|
|
const { featureName, projectId } = req.params;
|
|
await this.startTransaction(async (tx) =>
|
|
this.transactionalFeatureToggleService(tx).archiveToggle(
|
|
featureName,
|
|
req.user,
|
|
projectId,
|
|
),
|
|
);
|
|
res.status(202).send();
|
|
}
|
|
|
|
async staleFeatures(
|
|
req: IAuthRequest<{ projectId: string }, void, BatchStaleSchema>,
|
|
res: Response,
|
|
): Promise<void> {
|
|
const { features, stale } = req.body;
|
|
const { projectId } = req.params;
|
|
const userName = extractUsername(req);
|
|
|
|
await this.featureService.setToggleStaleness(
|
|
features,
|
|
stale,
|
|
userName,
|
|
projectId,
|
|
);
|
|
res.status(202).end();
|
|
}
|
|
|
|
async getFeatureEnvironment(
|
|
req: Request<FeatureStrategyParams, any, any, any>,
|
|
res: Response<FeatureEnvironmentSchema>,
|
|
): Promise<void> {
|
|
const { environment, featureName, projectId } = req.params;
|
|
const { defaultStrategy, ...environmentInfo } =
|
|
await this.featureService.getEnvironmentInfo(
|
|
projectId,
|
|
environment,
|
|
featureName,
|
|
);
|
|
|
|
const result = {
|
|
...environmentInfo,
|
|
strategies: environmentInfo.strategies.map((strategy) => {
|
|
const {
|
|
strategyName,
|
|
projectId: project,
|
|
environment: environmentId,
|
|
createdAt,
|
|
...rest
|
|
} = strategy;
|
|
return { ...rest, name: strategyName };
|
|
}),
|
|
};
|
|
|
|
this.openApiService.respondWithValidation(
|
|
200,
|
|
res,
|
|
featureEnvironmentSchema.$id,
|
|
serializeDates(result),
|
|
);
|
|
}
|
|
|
|
async toggleFeatureEnvironmentOn(
|
|
req: IAuthRequest<
|
|
FeatureStrategyParams,
|
|
any,
|
|
any,
|
|
FeatureStrategyQuery
|
|
>,
|
|
res: Response<void>,
|
|
): Promise<void> {
|
|
const { featureName, environment, projectId } = req.params;
|
|
const { shouldActivateDisabledStrategies } = req.query;
|
|
await this.featureService.updateEnabled(
|
|
projectId,
|
|
featureName,
|
|
environment,
|
|
true,
|
|
extractUsername(req),
|
|
req.user,
|
|
shouldActivateDisabledStrategies === 'true',
|
|
);
|
|
res.status(200).end();
|
|
}
|
|
|
|
async bulkToggleFeaturesEnvironmentOn(
|
|
req: IAuthRequest<
|
|
BulkFeaturesStrategyParams,
|
|
any,
|
|
BulkToggleFeaturesSchema,
|
|
FeatureStrategyQuery
|
|
>,
|
|
res: Response<void>,
|
|
): Promise<void> {
|
|
const { environment, projectId } = req.params;
|
|
const { shouldActivateDisabledStrategies } = req.query;
|
|
const { features } = req.body;
|
|
|
|
if (this.flagResolver.isEnabled('disableBulkToggle')) {
|
|
res.status(403).end();
|
|
return;
|
|
}
|
|
|
|
await this.startTransaction(async (tx) =>
|
|
this.transactionalFeatureToggleService(tx).bulkUpdateEnabled(
|
|
projectId,
|
|
features,
|
|
environment,
|
|
true,
|
|
extractUsername(req),
|
|
req.user,
|
|
shouldActivateDisabledStrategies === 'true',
|
|
),
|
|
);
|
|
res.status(200).end();
|
|
}
|
|
|
|
async bulkToggleFeaturesEnvironmentOff(
|
|
req: IAuthRequest<
|
|
BulkFeaturesStrategyParams,
|
|
any,
|
|
BulkToggleFeaturesSchema,
|
|
FeatureStrategyQuery
|
|
>,
|
|
res: Response<void>,
|
|
): Promise<void> {
|
|
const { environment, projectId } = req.params;
|
|
const { shouldActivateDisabledStrategies } = req.query;
|
|
const { features } = req.body;
|
|
|
|
if (this.flagResolver.isEnabled('disableBulkToggle')) {
|
|
res.status(403).end();
|
|
return;
|
|
}
|
|
|
|
await this.startTransaction(async (tx) =>
|
|
this.transactionalFeatureToggleService(tx).bulkUpdateEnabled(
|
|
projectId,
|
|
features,
|
|
environment,
|
|
false,
|
|
extractUsername(req),
|
|
req.user,
|
|
shouldActivateDisabledStrategies === 'true',
|
|
),
|
|
);
|
|
res.status(200).end();
|
|
}
|
|
|
|
async toggleFeatureEnvironmentOff(
|
|
req: IAuthRequest<FeatureStrategyParams, any, any, any>,
|
|
res: Response<void>,
|
|
): Promise<void> {
|
|
const { featureName, environment, projectId } = req.params;
|
|
await this.featureService.updateEnabled(
|
|
projectId,
|
|
featureName,
|
|
environment,
|
|
false,
|
|
extractUsername(req),
|
|
req.user,
|
|
);
|
|
res.status(200).end();
|
|
}
|
|
|
|
async addFeatureStrategy(
|
|
req: IAuthRequest<
|
|
FeatureStrategyParams,
|
|
any,
|
|
CreateFeatureStrategySchema
|
|
>,
|
|
res: Response<FeatureStrategySchema>,
|
|
): Promise<void> {
|
|
const { projectId, featureName, environment } = req.params;
|
|
const { ...strategyConfig } = req.body;
|
|
|
|
if (!strategyConfig.segmentIds) {
|
|
strategyConfig.segmentIds = [];
|
|
}
|
|
|
|
const userName = extractUsername(req);
|
|
const strategy = await this.featureService.createStrategy(
|
|
strategyConfig,
|
|
{ environment, projectId, featureName },
|
|
userName,
|
|
req.user,
|
|
);
|
|
|
|
const updatedStrategy = await this.featureService.getStrategy(
|
|
strategy.id,
|
|
);
|
|
res.status(200).json(updatedStrategy);
|
|
}
|
|
|
|
async getFeatureStrategies(
|
|
req: Request<FeatureStrategyParams, any, any, any>,
|
|
res: Response<FeatureStrategySchema[]>,
|
|
): Promise<void> {
|
|
const { projectId, featureName, environment } = req.params;
|
|
const featureStrategies =
|
|
await this.featureService.getStrategiesForEnvironment(
|
|
projectId,
|
|
featureName,
|
|
environment,
|
|
);
|
|
res.status(200).json(featureStrategies);
|
|
}
|
|
|
|
async setStrategiesSortOrder(
|
|
req: IAuthRequest<
|
|
FeatureStrategyParams,
|
|
any,
|
|
SetStrategySortOrderSchema,
|
|
any
|
|
>,
|
|
res: Response,
|
|
): Promise<void> {
|
|
const { featureName, projectId, environment } = req.params;
|
|
const createdBy = extractUsername(req);
|
|
await this.startTransaction(async (tx) =>
|
|
this.transactionalFeatureToggleService(
|
|
tx,
|
|
).updateStrategiesSortOrder(
|
|
{
|
|
featureName,
|
|
environment,
|
|
projectId,
|
|
},
|
|
req.body,
|
|
createdBy,
|
|
),
|
|
);
|
|
|
|
res.status(200).send();
|
|
}
|
|
|
|
async updateFeatureStrategy(
|
|
req: IAuthRequest<StrategyIdParams, any, UpdateFeatureStrategySchema>,
|
|
res: Response<FeatureStrategySchema>,
|
|
): Promise<void> {
|
|
const { strategyId, environment, projectId, featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
|
|
if (!req.body.segmentIds) {
|
|
req.body.segmentIds = [];
|
|
}
|
|
|
|
const updatedStrategy = await this.startTransaction(async (tx) =>
|
|
this.transactionalFeatureToggleService(tx).updateStrategy(
|
|
strategyId,
|
|
req.body,
|
|
{ environment, projectId, featureName },
|
|
userName,
|
|
req.user,
|
|
),
|
|
);
|
|
|
|
res.status(200).json(updatedStrategy);
|
|
}
|
|
|
|
async patchFeatureStrategy(
|
|
req: IAuthRequest<StrategyIdParams, any, Operation[], any>,
|
|
res: Response<FeatureStrategySchema>,
|
|
): Promise<void> {
|
|
const { strategyId, projectId, environment, featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
const patch = req.body;
|
|
const strategy = await this.featureService.getStrategy(strategyId);
|
|
|
|
const { newDocument } = applyPatch(strategy, patch);
|
|
const updatedStrategy = await this.startTransaction(async (tx) =>
|
|
this.transactionalFeatureToggleService(tx).updateStrategy(
|
|
strategyId,
|
|
newDocument,
|
|
{ environment, projectId, featureName },
|
|
userName,
|
|
req.user,
|
|
),
|
|
);
|
|
|
|
res.status(200).json(updatedStrategy);
|
|
}
|
|
|
|
async getFeatureStrategy(
|
|
req: IAuthRequest<StrategyIdParams, any, any, any>,
|
|
res: Response<FeatureStrategySchema>,
|
|
): Promise<void> {
|
|
this.logger.info('Getting strategy');
|
|
const { strategyId } = req.params;
|
|
this.logger.info(strategyId);
|
|
const strategy = await this.featureService.getStrategy(strategyId);
|
|
res.status(200).json(strategy);
|
|
}
|
|
|
|
async deleteFeatureStrategy(
|
|
req: IAuthRequest<StrategyIdParams, any, any, any>,
|
|
res: Response<void>,
|
|
): Promise<void> {
|
|
this.logger.info('Deleting strategy');
|
|
const { environment, projectId, featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
const { strategyId } = req.params;
|
|
this.logger.info(strategyId);
|
|
const strategy = await this.featureService.deleteStrategy(
|
|
strategyId,
|
|
{ environment, projectId, featureName },
|
|
userName,
|
|
req.user,
|
|
);
|
|
res.status(200).json(strategy);
|
|
}
|
|
|
|
async updateStrategyParameter(
|
|
req: IAuthRequest<
|
|
StrategyIdParams,
|
|
any,
|
|
{ name: string; value: string | number },
|
|
any
|
|
>,
|
|
res: Response<FeatureStrategySchema>,
|
|
): Promise<void> {
|
|
const { strategyId, environment, projectId, featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
const { name, value } = req.body;
|
|
|
|
const updatedStrategy =
|
|
await this.featureService.updateStrategyParameter(
|
|
strategyId,
|
|
name,
|
|
value,
|
|
{ environment, projectId, featureName },
|
|
userName,
|
|
);
|
|
res.status(200).json(updatedStrategy);
|
|
}
|
|
|
|
async updateFeaturesTags(
|
|
req: IAuthRequest<void, void, TagsBulkAddSchema>,
|
|
res: Response<TagSchema>,
|
|
): Promise<void> {
|
|
const { features, tags } = req.body;
|
|
const userName = extractUsername(req);
|
|
await this.featureTagService.updateTags(
|
|
features,
|
|
tags.addedTags,
|
|
tags.removedTags,
|
|
userName,
|
|
);
|
|
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);
|
|
}
|
|
}
|