1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

refactor: add schemas to strategy controller (#1744)

* refactor: avoid duplicate feature strategy operationIds

* refactor: fix flaky feature tests

* refactor: remove duplicate controller error handling

* refactor: unify feature strategy schemas

* refactor: add schemas to strategy controller
This commit is contained in:
olav 2022-06-23 08:10:20 +02:00 committed by GitHub
parent e013a72ddd
commit ac3f076a31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 883 additions and 263 deletions

View File

@ -7,8 +7,8 @@ import { contextFieldSchema } from './spec/context-field-schema';
import { contextFieldsSchema } from './spec/context-fields-schema';
import { createApiTokenSchema } from './spec/create-api-token-schema';
import { createFeatureSchema } from './spec/create-feature-schema';
import { createStrategySchema } from './spec/create-strategy-schema';
import { createUserSchema } from './spec/create-user-schema';
import { createFeatureStrategySchema } from './spec/create-feature-strategy-schema';
import { environmentSchema } from './spec/environment-schema';
import { environmentsSchema } from './spec/environments-schema';
import { featureEnvironmentSchema } from './spec/feature-environment-schema';
@ -40,14 +40,13 @@ import { projectsSchema } from './spec/projects-schema';
import { roleSchema } from './spec/role-schema';
import { sortOrderSchema } from './spec/sort-order-schema';
import { splashSchema } from './spec/splash-schema';
import { strategySchema } from './spec/strategy-schema';
import { tagSchema } from './spec/tag-schema';
import { tagsSchema } from './spec/tags-schema';
import { tagTypeSchema } from './spec/tag-type-schema';
import { tagTypesSchema } from './spec/tag-types-schema';
import { uiConfigSchema } from './spec/ui-config-schema';
import { updateFeatureSchema } from './spec/update-feature-schema';
import { updateStrategySchema } from './spec/update-strategy-schema';
import { updateFeatureStrategySchema } from './spec/update-feature-strategy-schema';
import { updateApiTokenSchema } from './spec/update-api-token-schema';
import { updateTagTypeSchema } from './spec/update-tag-type-schema';
import { upsertContextFieldSchema } from './spec/upsert-context-field-schema';
@ -76,6 +75,9 @@ import { stateSchema } from './spec/state-schema';
import { featureTagSchema } from './spec/feature-tag-schema';
import { exportParametersSchema } from './spec/export-parameters-schema';
import { emailSchema } from './spec/email-schema';
import { strategySchema } from './spec/strategy-schema';
import { strategiesSchema } from './spec/strategies-schema';
import { upsertStrategySchema } from './spec/upsert-strategy-schema';
// All schemas in `openapi/spec` should be listed here.
export const schemas = {
@ -94,7 +96,7 @@ export const schemas = {
contextFieldsSchema,
createApiTokenSchema,
createFeatureSchema,
createStrategySchema,
createFeatureStrategySchema,
createUserSchema,
emailSchema,
environmentSchema,
@ -132,6 +134,7 @@ export const schemas = {
sortOrderSchema,
splashSchema,
stateSchema,
strategiesSchema,
strategySchema,
tagSchema,
tagWithVersionSchema,
@ -141,10 +144,11 @@ export const schemas = {
tokenUserSchema,
uiConfigSchema,
updateFeatureSchema,
updateStrategySchema,
updateFeatureStrategySchema,
updateApiTokenSchema,
updateTagTypeSchema,
upsertContextFieldSchema,
upsertStrategySchema,
validatePasswordSchema,
validateTagTypeSchema,
updateUserSchema,

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`featureEnvironmentSchema empty 1`] = `
Object {
"data": Object {},
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'name'",
"params": Object {
"missingProperty": "name",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/featureEnvironmentSchema",
}
`;

View File

@ -17,13 +17,13 @@ Object {
},
"errors": Array [
Object {
"instancePath": "/strategies/0/constraints/0",
"instancePath": "/strategies/0",
"keyword": "required",
"message": "must have required property 'operator'",
"message": "must have required property 'id'",
"params": Object {
"missingProperty": "operator",
"missingProperty": "id",
},
"schemaPath": "#/components/schemas/constraintSchema/required",
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/featureSchema",

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`strategySchema 1`] = `
Object {
"data": Object {},
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'name'",
"params": Object {
"missingProperty": "name",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/strategySchema",
}
`;

View File

@ -2,8 +2,8 @@ import { FromSchema } from 'json-schema-to-ts';
import { parametersSchema } from './parameters-schema';
import { constraintSchema } from './constraint-schema';
export const createStrategySchema = {
$id: '#/components/schemas/createStrategySchema',
export const createFeatureStrategySchema = {
$id: '#/components/schemas/createFeatureStrategySchema',
type: 'object',
required: ['name'],
properties: {
@ -31,4 +31,6 @@ export const createStrategySchema = {
},
} as const;
export type CreateStrategySchema = FromSchema<typeof createStrategySchema>;
export type CreateFeatureStrategySchema = FromSchema<
typeof createFeatureStrategySchema
>;

View File

@ -0,0 +1,30 @@
import { validateSchema } from '../validate';
import { FeatureEnvironmentSchema } from './feature-environment-schema';
test('featureEnvironmentSchema', () => {
const data: FeatureEnvironmentSchema = {
name: '',
enabled: true,
strategies: [
{
id: '',
featureName: '',
projectId: '',
environment: '',
strategyName: '',
constraints: [{ contextName: '', operator: 'IN' }],
parameters: { a: '' },
},
],
};
expect(
validateSchema('#/components/schemas/featureEnvironmentSchema', data),
).toBeUndefined();
});
test('featureEnvironmentSchema empty', () => {
expect(
validateSchema('#/components/schemas/featureEnvironmentSchema', {}),
).toMatchSnapshot();
});

View File

@ -1,5 +1,4 @@
import { FromSchema } from 'json-schema-to-ts';
import { featureStrategySchema } from './feature-strategy-schema';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
@ -24,13 +23,55 @@ export const featureEnvironmentSchema = {
strategies: {
type: 'array',
items: {
$ref: '#/components/schemas/featureStrategySchema',
type: 'object',
additionalProperties: false,
required: [
'id',
'featureName',
'projectId',
'environment',
'strategyName',
'constraints',
'parameters',
],
properties: {
id: {
type: 'string',
},
featureName: {
type: 'string',
},
projectId: {
type: 'string',
},
environment: {
type: 'string',
},
strategyName: {
type: 'string',
},
sortOrder: {
type: 'number',
},
createdAt: {
type: 'string',
format: 'date-time',
},
constraints: {
type: 'array',
items: {
$ref: '#/components/schemas/constraintSchema',
},
},
parameters: {
$ref: '#/components/schemas/parametersSchema',
},
},
},
},
},
components: {
schemas: {
featureStrategySchema,
constraintSchema,
parametersSchema,
},

View File

@ -6,6 +6,7 @@ test('featureSchema', () => {
name: 'a',
strategies: [
{
id: 'a',
name: 'a',
constraints: [
{

View File

@ -1,10 +1,10 @@
import { FromSchema } from 'json-schema-to-ts';
import { variantSchema } from './variant-schema';
import { strategySchema } from './strategy-schema';
import { constraintSchema } from './constraint-schema';
import { overrideSchema } from './override-schema';
import { parametersSchema } from './parameters-schema';
import { environmentSchema } from './environment-schema';
import { featureStrategySchema } from './feature-strategy-schema';
export const featureSchema = {
$id: '#/components/schemas/featureSchema',
@ -55,7 +55,7 @@ export const featureSchema = {
strategies: {
type: 'array',
items: {
$ref: '#/components/schemas/strategySchema',
$ref: '#/components/schemas/featureStrategySchema',
},
},
variants: {
@ -71,7 +71,7 @@ export const featureSchema = {
environmentSchema,
overrideSchema,
parametersSchema,
strategySchema,
featureStrategySchema,
variantSchema,
},
},

View File

@ -6,14 +6,7 @@ export const featureStrategySchema = {
$id: '#/components/schemas/featureStrategySchema',
type: 'object',
additionalProperties: false,
required: [
'id',
'featureName',
'strategyName',
'constraints',
'parameters',
'environment',
],
required: ['name', 'id'],
properties: {
id: {
type: 'string',
@ -21,23 +14,6 @@ export const featureStrategySchema = {
name: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
featureName: {
type: 'string',
},
projectId: {
type: 'string',
},
environment: {
type: 'string',
},
strategyName: {
type: 'string',
},
sortOrder: {
type: 'number',
},

View File

@ -4,7 +4,7 @@ import { parametersSchema } from './parameters-schema';
import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema';
import { constraintSchema } from './constraint-schema';
import { strategySchema } from './strategy-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { environmentSchema } from './environment-schema';
export const featuresSchema = {
@ -30,7 +30,7 @@ export const featuresSchema = {
featureSchema,
overrideSchema,
parametersSchema,
strategySchema,
featureStrategySchema,
variantSchema,
},
},

View File

@ -2,7 +2,7 @@ import { FromSchema } from 'json-schema-to-ts';
import { parametersSchema } from './parameters-schema';
import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema';
import { strategySchema } from './strategy-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { featureSchema } from './feature-schema';
import { constraintSchema } from './constraint-schema';
import { environmentSchema } from './environment-schema';
@ -53,7 +53,7 @@ export const healthOverviewSchema = {
featureSchema,
overrideSchema,
parametersSchema,
strategySchema,
featureStrategySchema,
variantSchema,
},
},

View File

@ -1,6 +1,5 @@
import { FromSchema } from 'json-schema-to-ts';
import { featureSchema } from './feature-schema';
import { strategySchema } from './strategy-schema';
import { tagSchema } from './tag-schema';
import { tagTypeSchema } from './tag-type-schema';
import { featureTagSchema } from './feature-tag-schema';
@ -10,6 +9,7 @@ import { featureEnvironmentSchema } from './feature-environment-schema';
import { environmentSchema } from './environment-schema';
import { segmentSchema } from './segment-schema';
import { featureStrategySegmentSchema } from './feature-strategy-segment-schema';
import { strategySchema } from './strategy-schema';
export const stateSchema = {
$id: '#/components/schemas/stateSchema',
@ -90,7 +90,6 @@ export const stateSchema = {
components: {
schemas: {
featureSchema,
strategySchema,
tagSchema,
tagTypeSchema,
featureTagSchema,
@ -100,6 +99,7 @@ export const stateSchema = {
environmentSchema,
segmentSchema,
featureStrategySegmentSchema,
strategySchema,
},
},
} as const;

View File

@ -0,0 +1,27 @@
import { FromSchema } from 'json-schema-to-ts';
import { strategySchema } from './strategy-schema';
export const strategiesSchema = {
$id: '#/components/schemas/strategiesSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'strategies'],
properties: {
version: {
type: 'integer',
},
strategies: {
type: 'array',
items: {
$ref: '#/components/schemas/strategySchema',
},
},
},
components: {
schemas: {
strategySchema,
},
},
} as const;
export type StrategiesSchema = FromSchema<typeof strategiesSchema>;

View File

@ -0,0 +1,28 @@
import { validateSchema } from '../validate';
import { StrategySchema } from './strategy-schema';
test('strategySchema', () => {
const data: StrategySchema = {
description: '',
name: '',
displayName: '',
editable: false,
deprecated: false,
parameters: [
{
name: '',
type: '',
description: '',
required: true,
},
],
};
expect(
validateSchema('#/components/schemas/strategySchema', data),
).toBeUndefined();
expect(
validateSchema('#/components/schemas/strategySchema', {}),
).toMatchSnapshot();
});

View File

@ -1,38 +1,57 @@
import { FromSchema } from 'json-schema-to-ts';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
export const strategySchema = {
$id: '#/components/schemas/strategySchema',
type: 'object',
additionalProperties: false,
required: ['name'],
required: [
'name',
'displayName',
'description',
'editable',
'deprecated',
'parameters',
],
properties: {
id: {
type: 'string',
},
name: {
type: 'string',
},
sortOrder: {
type: 'number',
displayName: {
type: 'string',
nullable: true,
},
constraints: {
type: 'array',
items: {
$ref: '#/components/schemas/constraintSchema',
},
description: {
type: 'string',
},
editable: {
type: 'boolean',
},
deprecated: {
type: 'boolean',
},
parameters: {
$ref: '#/components/schemas/parametersSchema',
},
},
components: {
schemas: {
constraintSchema,
parametersSchema,
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: {
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
required: {
type: 'boolean',
},
},
},
},
},
components: {},
} as const;
export type StrategySchema = FromSchema<typeof strategySchema>;

View File

@ -0,0 +1,35 @@
import { FromSchema } from 'json-schema-to-ts';
import { parametersSchema } from './parameters-schema';
import { constraintSchema } from './constraint-schema';
export const updateFeatureStrategySchema = {
$id: '#/components/schemas/updateFeatureStrategySchema',
type: 'object',
properties: {
name: {
type: 'string',
},
sortOrder: {
type: 'number',
},
constraints: {
type: 'array',
items: {
$ref: '#/components/schemas/constraintSchema',
},
},
parameters: {
$ref: '#/components/schemas/parametersSchema',
},
},
components: {
schemas: {
constraintSchema,
parametersSchema,
},
},
} as const;
export type UpdateFeatureStrategySchema = FromSchema<
typeof updateFeatureStrategySchema
>;

View File

@ -1,11 +0,0 @@
import { FromSchema } from 'json-schema-to-ts';
import { strategySchema } from './strategy-schema';
export const updateStrategySchema = {
...strategySchema,
$id: '#/components/schemas/updateStrategySchema',
required: [],
components: {},
} as const;
export type UpdateStrategySchema = FromSchema<typeof updateStrategySchema>;

View File

@ -0,0 +1,41 @@
import { FromSchema } from 'json-schema-to-ts';
export const upsertStrategySchema = {
$id: '#/components/schemas/upsertStrategySchema',
type: 'object',
required: ['name'],
properties: {
name: {
type: 'string',
},
description: {
type: 'string',
},
editable: {
type: 'boolean',
},
parameters: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
required: {
type: 'boolean',
},
},
},
},
},
components: {},
} as const;
export type UpsertStrategySchema = FromSchema<typeof upsertStrategySchema>;

View File

@ -22,15 +22,15 @@ import {
featureSchema,
FeatureSchema,
} from '../../../openapi/spec/feature-schema';
import { StrategySchema } from '../../../openapi/spec/strategy-schema';
import { FeatureStrategySchema } from '../../../openapi/spec/feature-strategy-schema';
import { ParametersSchema } from '../../../openapi/spec/parameters-schema';
import {
featuresSchema,
FeaturesSchema,
} from '../../../openapi/spec/features-schema';
import { UpdateFeatureSchema } from '../../../openapi/spec/update-feature-schema';
import { UpdateStrategySchema } from '../../../openapi/spec/update-strategy-schema';
import { CreateStrategySchema } from '../../../openapi/spec/create-strategy-schema';
import { UpdateFeatureStrategySchema } from '../../../openapi/spec/update-feature-strategy-schema';
import { CreateFeatureStrategySchema } from '../../../openapi/spec/create-feature-strategy-schema';
import { serializeDates } from '../../../types/serialize-dates';
import { OpenApiService } from '../../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../../openapi';
@ -133,13 +133,15 @@ export default class ProjectFeaturesController extends Controller {
this.route({
method: 'get',
path: PATH_STRATEGIES,
handler: this.getStrategies,
handler: this.getFeatureStrategies,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getStrategies',
responses: { 200: createResponseSchema('strategySchema') },
operationId: 'getFeatureStrategies',
responses: {
200: createResponseSchema('featureStrategySchema'),
},
}),
],
});
@ -147,13 +149,15 @@ export default class ProjectFeaturesController extends Controller {
this.route({
method: 'post',
path: PATH_STRATEGIES,
handler: this.addStrategy,
handler: this.addFeatureStrategy,
permission: CREATE_FEATURE_STRATEGY,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'addStrategy',
requestBody: createRequestSchema('createStrategySchema'),
operationId: 'addFeatureStrategy',
requestBody: createRequestSchema(
'createFeatureStrategySchema',
),
responses: {
200: createResponseSchema('featureStrategySchema'),
},
@ -164,12 +168,12 @@ export default class ProjectFeaturesController extends Controller {
this.route({
method: 'get',
path: PATH_STRATEGY,
handler: this.getStrategy,
handler: this.getFeatureStrategy,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getStrategy',
operationId: 'getFeatureStrategy',
responses: {
200: createResponseSchema('featureStrategySchema'),
},
@ -180,28 +184,31 @@ export default class ProjectFeaturesController extends Controller {
this.route({
method: 'put',
path: PATH_STRATEGY,
handler: this.updateStrategy,
handler: this.updateFeatureStrategy,
permission: UPDATE_FEATURE_STRATEGY,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'updateStrategy',
requestBody: createRequestSchema('updateStrategySchema'),
operationId: 'updateFeatureStrategy',
requestBody: createRequestSchema(
'updateFeatureStrategySchema',
),
responses: {
200: createResponseSchema('featureStrategySchema'),
},
}),
],
});
this.route({
method: 'patch',
path: PATH_STRATEGY,
handler: this.patchStrategy,
handler: this.patchFeatureStrategy,
permission: UPDATE_FEATURE_STRATEGY,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'patchStrategy',
operationId: 'patchFeatureStrategy',
requestBody: createRequestSchema('patchesSchema'),
responses: {
200: createResponseSchema('featureStrategySchema'),
@ -213,11 +220,11 @@ export default class ProjectFeaturesController extends Controller {
method: 'delete',
path: PATH_STRATEGY,
acceptAnyContentType: true,
handler: this.deleteStrategy,
handler: this.deleteFeatureStrategy,
permission: DELETE_FEATURE_STRATEGY,
middleware: [
openApiService.validPath({
operationId: 'deleteStrategy',
operationId: 'deleteFeatureStrategy',
tags: ['admin'],
responses: { 200: emptyResponse },
}),
@ -516,9 +523,13 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).end();
}
async addStrategy(
req: IAuthRequest<FeatureStrategyParams, any, CreateStrategySchema>,
res: Response<StrategySchema>,
async addFeatureStrategy(
req: IAuthRequest<
FeatureStrategyParams,
any,
CreateFeatureStrategySchema
>,
res: Response<FeatureStrategySchema>,
): Promise<void> {
const { projectId, featureName, environment } = req.params;
const userName = extractUsername(req);
@ -530,9 +541,9 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(strategy);
}
async getStrategies(
async getFeatureStrategies(
req: Request<FeatureStrategyParams, any, any, any>,
res: Response<StrategySchema[]>,
res: Response<FeatureStrategySchema[]>,
): Promise<void> {
const { projectId, featureName, environment } = req.params;
const featureStrategies =
@ -544,9 +555,9 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(featureStrategies);
}
async updateStrategy(
req: IAuthRequest<StrategyIdParams, any, UpdateStrategySchema>,
res: Response<StrategySchema>,
async updateFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, UpdateFeatureStrategySchema>,
res: Response<FeatureStrategySchema>,
): Promise<void> {
const { strategyId, environment, projectId, featureName } = req.params;
const userName = extractUsername(req);
@ -559,9 +570,9 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(updatedStrategy);
}
async patchStrategy(
async patchFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, Operation[], any>,
res: Response<StrategySchema>,
res: Response<FeatureStrategySchema>,
): Promise<void> {
const { strategyId, projectId, environment, featureName } = req.params;
const userName = extractUsername(req);
@ -577,9 +588,9 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(updatedStrategy);
}
async getStrategy(
async getFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, any, any>,
res: Response<StrategySchema>,
res: Response<FeatureStrategySchema>,
): Promise<void> {
this.logger.info('Getting strategy');
const { strategyId } = req.params;
@ -588,7 +599,7 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(strategy);
}
async deleteStrategy(
async deleteFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, any, any>,
res: Response<void>,
): Promise<void> {
@ -612,7 +623,7 @@ export default class ProjectFeaturesController extends Controller {
{ name: string; value: string | number },
any
>,
res: Response<StrategySchema>,
res: Response<FeatureStrategySchema>,
): Promise<void> {
const { strategyId, environment, projectId, featureName } = req.params;
const userName = extractUsername(req);

View File

@ -54,8 +54,8 @@ test('require a name when creating a new strategy', async () => {
.send({})
.expect(400)
.expect((res) => {
expect(res.body.details[0].message === '"name" is required').toBe(
true,
expect(res.body.validation[0].message).toEqual(
"should have required property 'name'",
);
});
});

View File

@ -2,18 +2,28 @@ import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import StrategyService from '../../services/strategy-service';
import { Logger } from '../../logger';
import Controller from '../controller';
import { extractUsername } from '../../util/extract-user';
import { handleErrors } from '../util';
import {
DELETE_STRATEGY,
CREATE_STRATEGY,
UPDATE_STRATEGY,
NONE,
} from '../../types/permissions';
import { Request, Response } from 'express';
import { IAuthRequest } from '../unleash-types';
import { OpenApiService } from '../../services/openapi-service';
import { emptyResponse } from '../../openapi/spec/empty-response';
import { createRequestSchema, createResponseSchema } from '../../openapi';
import {
strategySchema,
StrategySchema,
} from '../../openapi/spec/strategy-schema';
import {
strategiesSchema,
StrategiesSchema,
} from '../../openapi/spec/strategies-schema';
import { UpsertStrategySchema } from '../../openapi/spec/upsert-strategy-schema';
const version = 1;
@ -22,112 +32,210 @@ class StrategyController extends Controller {
private strategyService: StrategyService;
private openApiService: OpenApiService;
constructor(
config: IUnleashConfig,
{ strategyService }: Pick<IUnleashServices, 'strategyService'>,
{
strategyService,
openApiService,
}: Pick<IUnleashServices, 'strategyService' | 'openApiService'>,
) {
super(config);
this.logger = config.getLogger('/admin-api/strategy.js');
this.strategyService = strategyService;
this.openApiService = openApiService;
this.get('/', this.getAllStrategies);
this.get('/:name', this.getStrategy);
this.delete('/:name', this.removeStrategy, DELETE_STRATEGY);
this.post('/', this.createStrategy, CREATE_STRATEGY);
this.put('/:strategyName', this.updateStrategy, UPDATE_STRATEGY);
this.post(
'/:strategyName/deprecate',
this.deprecateStrategy,
UPDATE_STRATEGY,
);
this.post(
'/:strategyName/reactivate',
this.reactivateStrategy,
UPDATE_STRATEGY,
this.route({
method: 'get',
path: '',
handler: this.getAllStrategies,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getAllStrategies',
responses: {
200: createResponseSchema('strategiesSchema'),
},
}),
],
});
this.route({
method: 'get',
path: '/:name',
handler: this.getStrategy,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getStrategy',
responses: { 200: createResponseSchema('strategySchema') },
}),
],
});
this.route({
method: 'delete',
path: '/:name',
handler: this.removeStrategy,
permission: DELETE_STRATEGY,
acceptAnyContentType: true,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'removeStrategy',
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'post',
path: '',
handler: this.createStrategy,
permission: CREATE_STRATEGY,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'createStrategy',
requestBody: createRequestSchema('upsertStrategySchema'),
responses: { 201: emptyResponse },
}),
],
});
this.route({
method: 'put',
path: '/:strategyName',
handler: this.updateStrategy,
permission: UPDATE_STRATEGY,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'updateStrategy',
requestBody: createRequestSchema('upsertStrategySchema'),
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'post',
path: '/:strategyName/deprecate',
handler: this.deprecateStrategy,
permission: UPDATE_STRATEGY,
acceptAnyContentType: true,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'deprecateStrategy',
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'post',
path: '/:strategyName/reactivate',
handler: this.reactivateStrategy,
permission: UPDATE_STRATEGY,
acceptAnyContentType: true,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'reactivateStrategy',
responses: { 200: emptyResponse },
}),
],
});
}
async getAllStrategies(
req: Request,
res: Response<StrategiesSchema>,
): Promise<void> {
const strategies = await this.strategyService.getStrategies();
this.openApiService.respondWithValidation(
200,
res,
strategiesSchema.$id,
{ version, strategies },
);
}
async getAllStrategies(req: Request, res: Response): Promise<void> {
try {
const strategies = await this.strategyService.getStrategies();
res.json({ version, strategies });
} catch (err) {
handleErrors(res, this.logger, err);
}
}
async getStrategy(
req: Request,
res: Response<StrategySchema>,
): Promise<void> {
const strategy = await this.strategyService.getStrategy(
req.params.name,
);
async getStrategy(req: Request, res: Response): Promise<void> {
try {
const { name } = req.params;
const strategy = await this.strategyService.getStrategy(name);
res.json(strategy).end();
} catch (err) {
res.status(404).json({ error: 'Could not find strategy' });
}
this.openApiService.respondWithValidation(
200,
res,
strategySchema.$id,
strategy,
);
}
async removeStrategy(req: IAuthRequest, res: Response): Promise<void> {
const strategyName = req.params.name;
const userName = extractUsername(req);
try {
await this.strategyService.removeStrategy(strategyName, userName);
res.status(200).end();
} catch (error) {
handleErrors(res, this.logger, error);
}
await this.strategyService.removeStrategy(strategyName, userName);
res.status(200).end();
}
async createStrategy(req: IAuthRequest, res: Response): Promise<void> {
async createStrategy(
req: IAuthRequest<unknown, UpsertStrategySchema>,
res: Response<void>,
): Promise<void> {
const userName = extractUsername(req);
try {
await this.strategyService.createStrategy(req.body, userName);
res.status(201).end();
} catch (error) {
handleErrors(res, this.logger, error);
}
await this.strategyService.createStrategy(req.body, userName);
res.status(201).end();
}
async updateStrategy(req: IAuthRequest, res: Response): Promise<void> {
async updateStrategy(
req: IAuthRequest<unknown, UpsertStrategySchema>,
res: Response<void>,
): Promise<void> {
const userName = extractUsername(req);
try {
await this.strategyService.updateStrategy(req.body, userName);
res.status(200).end();
} catch (error) {
handleErrors(res, this.logger, error);
}
await this.strategyService.updateStrategy(req.body, userName);
res.status(200).end();
}
async deprecateStrategy(req: IAuthRequest, res: Response): Promise<void> {
async deprecateStrategy(
req: IAuthRequest,
res: Response<void>,
): Promise<void> {
const userName = extractUsername(req);
const { strategyName } = req.params;
if (strategyName === 'default') {
res.status(403).end();
} else {
try {
await this.strategyService.deprecateStrategy(
strategyName,
userName,
);
res.status(200).end();
} catch (error) {
handleErrors(res, this.logger, error);
}
return;
}
await this.strategyService.deprecateStrategy(strategyName, userName);
res.status(200).end();
}
async reactivateStrategy(req: IAuthRequest, res: Response): Promise<void> {
async reactivateStrategy(
req: IAuthRequest,
res: Response<void>,
): Promise<void> {
const userName = extractUsername(req);
const { strategyName } = req.params;
try {
await this.strategyService.reactivateStrategy(
strategyName,
userName,
);
res.status(200).end();
} catch (error) {
handleErrors(res, this.logger, error);
}
await this.strategyService.reactivateStrategy(strategyName, userName);
res.status(200).end();
}
}
export default StrategyController;

View File

@ -390,7 +390,7 @@ class FeatureToggleService {
value: string | number,
context: IFeatureStrategyContext,
userName: string,
): Promise<IStrategyConfig> {
): Promise<Saved<IStrategyConfig>> {
const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id);
@ -466,7 +466,7 @@ class FeatureToggleService {
project: string,
featureName: string,
environment: string = DEFAULT_ENV,
): Promise<IStrategyConfig[]> {
): Promise<Saved<IStrategyConfig>[]> {
const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment(
environment,
featureName,
@ -701,7 +701,7 @@ class FeatureToggleService {
);
}
async getStrategy(strategyId: string): Promise<IStrategyConfig> {
async getStrategy(strategyId: string): Promise<Saved<IStrategyConfig>> {
const strategy = await this.featureStrategiesStore.getStrategyById(
strategyId,
);

View File

@ -188,7 +188,7 @@ export interface ITag {
type: string;
}
export interface IParameterDefinition {
export interface IAddonParameterDefinition {
name: string;
displayName: string;
type: string;
@ -203,7 +203,7 @@ export interface IAddonDefinition {
displayName: string;
documentationUrl: string;
description: string;
parameters?: IParameterDefinition[];
parameters?: IAddonParameterDefinition[];
events?: string[];
tagTypes?: ITagType[];
}

View File

@ -4,7 +4,7 @@ export interface IStrategy {
name: string;
editable: boolean;
description: string;
parameters: object;
parameters: object[];
deprecated: boolean;
displayName: string;
}

View File

@ -7,9 +7,9 @@ import {
} from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
import { StrategySchema } from '../../../../lib/openapi/spec/strategy-schema';
import { FeatureSchema } from '../../../../lib/openapi/spec/feature-schema';
import { VariantSchema } from '../../../../lib/openapi/spec/variant-schema';
import { FeatureStrategySchema } from '../../../../lib/openapi/spec/feature-strategy-schema';
let app: IUnleashTest;
let db: ITestDb;
@ -26,7 +26,7 @@ beforeAll(async () => {
const createToggle = async (
toggle: Omit<FeatureSchema, 'createdAt'>,
strategy: Omit<StrategySchema, 'id'> = defaultStrategy,
strategy: Omit<FeatureStrategySchema, 'id'> = defaultStrategy,
projectId: string = 'default',
username: string = 'test',
) => {
@ -576,7 +576,7 @@ test('tagging a feature with an already existing tag should be a noop', async ()
test('can untag feature', async () => {
expect.assertions(1);
const feature1Name = faker.lorem.slug(3);
const feature1Name = faker.datatype.uuid();
await app.request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',
@ -584,7 +584,7 @@ test('can untag feature', async () => {
strategies: [{ name: 'default' }],
});
const tag = {
value: faker.lorem.slug(1),
value: faker.lorem.word(),
type: 'simple',
};
await app.request
@ -607,8 +607,8 @@ test('can untag feature', async () => {
test('Can get features tagged by tag', async () => {
expect.assertions(2);
const feature1Name = faker.helpers.slugify(faker.lorem.words(3));
const feature2Name = faker.helpers.slugify(faker.lorem.words(3));
const feature1Name = faker.datatype.uuid();
const feature2Name = faker.datatype.uuid();
await app.request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',
@ -637,8 +637,8 @@ test('Can get features tagged by tag', async () => {
});
test('Can query for multiple tags using OR', async () => {
expect.assertions(3);
const feature1Name = faker.helpers.slugify(faker.lorem.words(3));
const feature2Name = faker.helpers.slugify(faker.lorem.words(3));
const feature1Name = faker.datatype.uuid();
const feature2Name = faker.datatype.uuid();
await app.request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',
@ -678,8 +678,8 @@ test('Can query for multiple tags using OR', async () => {
});
});
test('Querying with multiple filters ANDs the filters', async () => {
const feature1Name = `test.${faker.helpers.slugify(faker.hacker.phrase())}`;
const feature2Name = faker.helpers.slugify(faker.lorem.words());
const feature1Name = `test.${faker.datatype.uuid()}`;
const feature2Name = faker.datatype.uuid();
await app.request.post('/api/admin/features').send({
name: feature1Name,
@ -729,7 +729,7 @@ test('Querying with multiple filters ANDs the filters', async () => {
});
test('Tagging a feature with a tag it already has should return 409', async () => {
const feature1Name = `test.${faker.helpers.slugify(faker.lorem.words(3))}`;
const feature1Name = `test.${faker.datatype.uuid()}`;
await app.request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',

View File

@ -465,7 +465,7 @@ Object {
],
"type": "object",
},
"createStrategySchema": Object {
"createFeatureStrategySchema": Object {
"properties": Object {
"constraints": Object {
"items": Object {
@ -612,7 +612,50 @@ Object {
},
"strategies": Object {
"items": Object {
"$ref": "#/components/schemas/featureStrategySchema",
"additionalProperties": false,
"properties": Object {
"constraints": Object {
"items": Object {
"$ref": "#/components/schemas/constraintSchema",
},
"type": "array",
},
"createdAt": Object {
"format": "date-time",
"type": "string",
},
"environment": Object {
"type": "string",
},
"featureName": Object {
"type": "string",
},
"id": Object {
"type": "string",
},
"parameters": Object {
"$ref": "#/components/schemas/parametersSchema",
},
"projectId": Object {
"type": "string",
},
"sortOrder": Object {
"type": "number",
},
"strategyName": Object {
"type": "string",
},
},
"required": Array [
"id",
"featureName",
"projectId",
"environment",
"strategyName",
"constraints",
"parameters",
],
"type": "object",
},
"type": "array",
},
@ -668,7 +711,7 @@ Object {
},
"strategies": Object {
"items": Object {
"$ref": "#/components/schemas/strategySchema",
"$ref": "#/components/schemas/featureStrategySchema",
},
"type": "array",
},
@ -696,17 +739,6 @@ Object {
},
"type": "array",
},
"createdAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"environment": Object {
"type": "string",
},
"featureName": Object {
"type": "string",
},
"id": Object {
"type": "string",
},
@ -716,23 +748,13 @@ Object {
"parameters": Object {
"$ref": "#/components/schemas/parametersSchema",
},
"projectId": Object {
"type": "string",
},
"sortOrder": Object {
"type": "number",
},
"strategyName": Object {
"type": "string",
},
},
"required": Array [
"name",
"id",
"featureName",
"strategyName",
"constraints",
"parameters",
"environment",
],
"type": "object",
},
@ -1383,30 +1405,73 @@ Object {
],
"type": "object",
},
"strategySchema": Object {
"strategiesSchema": Object {
"additionalProperties": false,
"properties": Object {
"constraints": Object {
"strategies": Object {
"items": Object {
"$ref": "#/components/schemas/constraintSchema",
"$ref": "#/components/schemas/strategySchema",
},
"type": "array",
},
"id": Object {
"version": Object {
"type": "integer",
},
},
"required": Array [
"version",
"strategies",
],
"type": "object",
},
"strategySchema": Object {
"additionalProperties": false,
"properties": Object {
"deprecated": Object {
"type": "boolean",
},
"description": Object {
"type": "string",
},
"displayName": Object {
"nullable": true,
"type": "string",
},
"editable": Object {
"type": "boolean",
},
"name": Object {
"type": "string",
},
"parameters": Object {
"$ref": "#/components/schemas/parametersSchema",
},
"sortOrder": Object {
"type": "number",
"items": Object {
"additionalProperties": false,
"properties": Object {
"description": Object {
"type": "string",
},
"name": Object {
"type": "string",
},
"required": Object {
"type": "boolean",
},
"type": Object {
"type": "string",
},
},
"type": "object",
},
"type": "array",
},
},
"required": Array [
"name",
"displayName",
"description",
"editable",
"deprecated",
"parameters",
],
"type": "object",
},
@ -1647,8 +1712,7 @@ Object {
],
"type": "object",
},
"updateStrategySchema": Object {
"additionalProperties": false,
"updateFeatureStrategySchema": Object {
"properties": Object {
"constraints": Object {
"items": Object {
@ -1656,9 +1720,6 @@ Object {
},
"type": "array",
},
"id": Object {
"type": "string",
},
"name": Object {
"type": "string",
},
@ -1669,7 +1730,6 @@ Object {
"type": "number",
},
},
"required": Array [],
"type": "object",
},
"updateTagTypeSchema": Object {
@ -1724,6 +1784,43 @@ Object {
],
"type": "object",
},
"upsertStrategySchema": Object {
"properties": Object {
"description": Object {
"type": "string",
},
"editable": Object {
"type": "boolean",
},
"name": Object {
"type": "string",
},
"parameters": Object {
"items": Object {
"properties": Object {
"description": Object {
"type": "string",
},
"name": Object {
"type": "string",
},
"required": Object {
"type": "boolean",
},
"type": Object {
"type": "string",
},
},
"type": "object",
},
"type": "array",
},
},
"required": Array [
"name",
],
"type": "object",
},
"userSchema": Object {
"additionalProperties": false,
"properties": Object {
@ -3434,7 +3531,7 @@ Object {
},
"/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/strategies": Object {
"get": Object {
"operationId": "getStrategies",
"operationId": "getFeatureStrategies",
"parameters": Array [
Object {
"in": "path",
@ -3466,11 +3563,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/strategySchema",
"$ref": "#/components/schemas/featureStrategySchema",
},
},
},
"description": "strategySchema",
"description": "featureStrategySchema",
},
},
"tags": Array [
@ -3478,7 +3575,7 @@ Object {
],
},
"post": Object {
"operationId": "addStrategy",
"operationId": "addFeatureStrategy",
"parameters": Array [
Object {
"in": "path",
@ -3509,11 +3606,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/createStrategySchema",
"$ref": "#/components/schemas/createFeatureStrategySchema",
},
},
},
"description": "createStrategySchema",
"description": "createFeatureStrategySchema",
"required": true,
},
"responses": Object {
@ -3535,7 +3632,7 @@ Object {
},
"/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/strategies/{strategyId}": Object {
"delete": Object {
"operationId": "deleteStrategy",
"operationId": "deleteFeatureStrategy",
"parameters": Array [
Object {
"in": "path",
@ -3580,7 +3677,7 @@ Object {
],
},
"get": Object {
"operationId": "getStrategy",
"operationId": "getFeatureStrategy",
"parameters": Array [
Object {
"in": "path",
@ -3632,7 +3729,7 @@ Object {
],
},
"patch": Object {
"operationId": "patchStrategy",
"operationId": "patchFeatureStrategy",
"parameters": Array [
Object {
"in": "path",
@ -3695,7 +3792,7 @@ Object {
],
},
"put": Object {
"operationId": "updateStrategy",
"operationId": "updateFeatureStrategy",
"parameters": Array [
Object {
"in": "path",
@ -3734,11 +3831,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/updateStrategySchema",
"$ref": "#/components/schemas/updateFeatureStrategySchema",
},
},
},
"description": "updateStrategySchema",
"description": "updateFeatureStrategySchema",
"required": true,
},
"responses": Object {
@ -3999,6 +4096,179 @@ Object {
],
},
},
"/api/admin/strategies": Object {
"get": Object {
"operationId": "getAllStrategies",
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/strategiesSchema",
},
},
},
"description": "strategiesSchema",
},
},
"tags": Array [
"admin",
],
},
"post": Object {
"operationId": "createStrategy",
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/upsertStrategySchema",
},
},
},
"description": "upsertStrategySchema",
"required": true,
},
"responses": Object {
"201": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/strategies/{name}": Object {
"delete": Object {
"operationId": "removeStrategy",
"parameters": Array [
Object {
"in": "path",
"name": "name",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
"get": Object {
"operationId": "getStrategy",
"parameters": Array [
Object {
"in": "path",
"name": "name",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/strategySchema",
},
},
},
"description": "strategySchema",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/strategies/{strategyName}": Object {
"put": Object {
"operationId": "updateStrategy",
"parameters": Array [
Object {
"in": "path",
"name": "strategyName",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/upsertStrategySchema",
},
},
},
"description": "upsertStrategySchema",
"required": true,
},
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/strategies/{strategyName}/deprecate": Object {
"post": Object {
"operationId": "deprecateStrategy",
"parameters": Array [
Object {
"in": "path",
"name": "strategyName",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/strategies/{strategyName}/reactivate": Object {
"post": Object {
"operationId": "reactivateStrategy",
"parameters": Array [
Object {
"in": "path",
"name": "strategyName",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/tag-types": Object {
"get": Object {
"operationId": "getTagTypes",

View File

@ -43,7 +43,7 @@ test('Apps registered should be announced', async () => {
color: faker.internet.color(),
};
const differentClient = {
appName: faker.lorem.slug(2),
appName: faker.datatype.uuid(),
instanceId: faker.datatype.uuid(),
strategies: ['default'],
started: Date.now(),

View File

@ -2,8 +2,8 @@ import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import { createTestConfig } from '../../config/test-config';
import dbInit from '../helpers/database-init';
import { DEFAULT_ENV } from '../../../lib/util/constants';
import { StrategySchema } from '../../../lib/openapi/spec/strategy-schema';
import { SegmentService } from '../../../lib/services/segment-service';
import { FeatureStrategySchema } from '../../../lib/openapi/spec/feature-strategy-schema';
let stores;
let db;
@ -30,7 +30,7 @@ afterAll(async () => {
test('Should create feature toggle strategy configuration', async () => {
const projectId = 'default';
const username = 'feature-toggle';
const config: Omit<StrategySchema, 'id'> = {
const config: Omit<FeatureStrategySchema, 'id'> = {
name: 'default',
constraints: [],
parameters: {},
@ -58,7 +58,7 @@ test('Should be able to update existing strategy configuration', async () => {
const projectId = 'default';
const username = 'existing-strategy';
const featureName = 'update-existing-strategy';
const config: Omit<StrategySchema, 'id'> = {
const config: Omit<FeatureStrategySchema, 'id'> = {
name: 'default',
constraints: [],
parameters: {},
@ -93,7 +93,7 @@ test('Should be able to get strategy by id', async () => {
const projectId = 'default';
const userName = 'strategy';
const config: Omit<StrategySchema, 'id'> = {
const config: Omit<FeatureStrategySchema, 'id'> = {
name: 'default',
constraints: [],
parameters: {},

View File

@ -12,7 +12,7 @@ export default class FakeStrategiesStore implements IStrategyStore {
description: 'default strategy',
displayName: 'Default',
editable: false,
parameters: {},
parameters: [],
deprecated: false,
};