1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +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 { contextFieldsSchema } from './spec/context-fields-schema';
import { createApiTokenSchema } from './spec/create-api-token-schema'; import { createApiTokenSchema } from './spec/create-api-token-schema';
import { createFeatureSchema } from './spec/create-feature-schema'; import { createFeatureSchema } from './spec/create-feature-schema';
import { createStrategySchema } from './spec/create-strategy-schema';
import { createUserSchema } from './spec/create-user-schema'; import { createUserSchema } from './spec/create-user-schema';
import { createFeatureStrategySchema } from './spec/create-feature-strategy-schema';
import { environmentSchema } from './spec/environment-schema'; import { environmentSchema } from './spec/environment-schema';
import { environmentsSchema } from './spec/environments-schema'; import { environmentsSchema } from './spec/environments-schema';
import { featureEnvironmentSchema } from './spec/feature-environment-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 { roleSchema } from './spec/role-schema';
import { sortOrderSchema } from './spec/sort-order-schema'; import { sortOrderSchema } from './spec/sort-order-schema';
import { splashSchema } from './spec/splash-schema'; import { splashSchema } from './spec/splash-schema';
import { strategySchema } from './spec/strategy-schema';
import { tagSchema } from './spec/tag-schema'; import { tagSchema } from './spec/tag-schema';
import { tagsSchema } from './spec/tags-schema'; import { tagsSchema } from './spec/tags-schema';
import { tagTypeSchema } from './spec/tag-type-schema'; import { tagTypeSchema } from './spec/tag-type-schema';
import { tagTypesSchema } from './spec/tag-types-schema'; import { tagTypesSchema } from './spec/tag-types-schema';
import { uiConfigSchema } from './spec/ui-config-schema'; import { uiConfigSchema } from './spec/ui-config-schema';
import { updateFeatureSchema } from './spec/update-feature-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 { updateApiTokenSchema } from './spec/update-api-token-schema';
import { updateTagTypeSchema } from './spec/update-tag-type-schema'; import { updateTagTypeSchema } from './spec/update-tag-type-schema';
import { upsertContextFieldSchema } from './spec/upsert-context-field-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 { featureTagSchema } from './spec/feature-tag-schema';
import { exportParametersSchema } from './spec/export-parameters-schema'; import { exportParametersSchema } from './spec/export-parameters-schema';
import { emailSchema } from './spec/email-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. // All schemas in `openapi/spec` should be listed here.
export const schemas = { export const schemas = {
@ -94,7 +96,7 @@ export const schemas = {
contextFieldsSchema, contextFieldsSchema,
createApiTokenSchema, createApiTokenSchema,
createFeatureSchema, createFeatureSchema,
createStrategySchema, createFeatureStrategySchema,
createUserSchema, createUserSchema,
emailSchema, emailSchema,
environmentSchema, environmentSchema,
@ -132,6 +134,7 @@ export const schemas = {
sortOrderSchema, sortOrderSchema,
splashSchema, splashSchema,
stateSchema, stateSchema,
strategiesSchema,
strategySchema, strategySchema,
tagSchema, tagSchema,
tagWithVersionSchema, tagWithVersionSchema,
@ -141,10 +144,11 @@ export const schemas = {
tokenUserSchema, tokenUserSchema,
uiConfigSchema, uiConfigSchema,
updateFeatureSchema, updateFeatureSchema,
updateStrategySchema, updateFeatureStrategySchema,
updateApiTokenSchema, updateApiTokenSchema,
updateTagTypeSchema, updateTagTypeSchema,
upsertContextFieldSchema, upsertContextFieldSchema,
upsertStrategySchema,
validatePasswordSchema, validatePasswordSchema,
validateTagTypeSchema, validateTagTypeSchema,
updateUserSchema, 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 [ "errors": Array [
Object { Object {
"instancePath": "/strategies/0/constraints/0", "instancePath": "/strategies/0",
"keyword": "required", "keyword": "required",
"message": "must have required property 'operator'", "message": "must have required property 'id'",
"params": Object { "params": Object {
"missingProperty": "operator", "missingProperty": "id",
}, },
"schemaPath": "#/components/schemas/constraintSchema/required", "schemaPath": "#/required",
}, },
], ],
"schema": "#/components/schemas/featureSchema", "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 { parametersSchema } from './parameters-schema';
import { constraintSchema } from './constraint-schema'; import { constraintSchema } from './constraint-schema';
export const createStrategySchema = { export const createFeatureStrategySchema = {
$id: '#/components/schemas/createStrategySchema', $id: '#/components/schemas/createFeatureStrategySchema',
type: 'object', type: 'object',
required: ['name'], required: ['name'],
properties: { properties: {
@ -31,4 +31,6 @@ export const createStrategySchema = {
}, },
} as const; } 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 { FromSchema } from 'json-schema-to-ts';
import { featureStrategySchema } from './feature-strategy-schema';
import { constraintSchema } from './constraint-schema'; import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema'; import { parametersSchema } from './parameters-schema';
@ -24,13 +23,55 @@ export const featureEnvironmentSchema = {
strategies: { strategies: {
type: 'array', type: 'array',
items: { 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: { components: {
schemas: { schemas: {
featureStrategySchema,
constraintSchema, constraintSchema,
parametersSchema, parametersSchema,
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { FromSchema } from 'json-schema-to-ts'; import { FromSchema } from 'json-schema-to-ts';
import { featureSchema } from './feature-schema'; import { featureSchema } from './feature-schema';
import { strategySchema } from './strategy-schema';
import { tagSchema } from './tag-schema'; import { tagSchema } from './tag-schema';
import { tagTypeSchema } from './tag-type-schema'; import { tagTypeSchema } from './tag-type-schema';
import { featureTagSchema } from './feature-tag-schema'; import { featureTagSchema } from './feature-tag-schema';
@ -10,6 +9,7 @@ import { featureEnvironmentSchema } from './feature-environment-schema';
import { environmentSchema } from './environment-schema'; import { environmentSchema } from './environment-schema';
import { segmentSchema } from './segment-schema'; import { segmentSchema } from './segment-schema';
import { featureStrategySegmentSchema } from './feature-strategy-segment-schema'; import { featureStrategySegmentSchema } from './feature-strategy-segment-schema';
import { strategySchema } from './strategy-schema';
export const stateSchema = { export const stateSchema = {
$id: '#/components/schemas/stateSchema', $id: '#/components/schemas/stateSchema',
@ -90,7 +90,6 @@ export const stateSchema = {
components: { components: {
schemas: { schemas: {
featureSchema, featureSchema,
strategySchema,
tagSchema, tagSchema,
tagTypeSchema, tagTypeSchema,
featureTagSchema, featureTagSchema,
@ -100,6 +99,7 @@ export const stateSchema = {
environmentSchema, environmentSchema,
segmentSchema, segmentSchema,
featureStrategySegmentSchema, featureStrategySegmentSchema,
strategySchema,
}, },
}, },
} as const; } 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 { FromSchema } from 'json-schema-to-ts';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
export const strategySchema = { export const strategySchema = {
$id: '#/components/schemas/strategySchema', $id: '#/components/schemas/strategySchema',
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
required: ['name'], required: [
'name',
'displayName',
'description',
'editable',
'deprecated',
'parameters',
],
properties: { properties: {
id: {
type: 'string',
},
name: { name: {
type: 'string', type: 'string',
}, },
sortOrder: { displayName: {
type: 'number', type: 'string',
nullable: true,
}, },
constraints: { description: {
type: 'array', type: 'string',
items: { },
$ref: '#/components/schemas/constraintSchema', editable: {
}, type: 'boolean',
},
deprecated: {
type: 'boolean',
}, },
parameters: { parameters: {
$ref: '#/components/schemas/parametersSchema', type: 'array',
}, items: {
}, type: 'object',
components: { additionalProperties: false,
schemas: { properties: {
constraintSchema, name: {
parametersSchema, type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
required: {
type: 'boolean',
},
},
},
}, },
}, },
components: {},
} as const; } as const;
export type StrategySchema = FromSchema<typeof strategySchema>; 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,
FeatureSchema, FeatureSchema,
} from '../../../openapi/spec/feature-schema'; } 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 { ParametersSchema } from '../../../openapi/spec/parameters-schema';
import { import {
featuresSchema, featuresSchema,
FeaturesSchema, FeaturesSchema,
} from '../../../openapi/spec/features-schema'; } from '../../../openapi/spec/features-schema';
import { UpdateFeatureSchema } from '../../../openapi/spec/update-feature-schema'; import { UpdateFeatureSchema } from '../../../openapi/spec/update-feature-schema';
import { UpdateStrategySchema } from '../../../openapi/spec/update-strategy-schema'; import { UpdateFeatureStrategySchema } from '../../../openapi/spec/update-feature-strategy-schema';
import { CreateStrategySchema } from '../../../openapi/spec/create-strategy-schema'; import { CreateFeatureStrategySchema } from '../../../openapi/spec/create-feature-strategy-schema';
import { serializeDates } from '../../../types/serialize-dates'; import { serializeDates } from '../../../types/serialize-dates';
import { OpenApiService } from '../../../services/openapi-service'; import { OpenApiService } from '../../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../../openapi'; import { createRequestSchema, createResponseSchema } from '../../../openapi';
@ -133,13 +133,15 @@ export default class ProjectFeaturesController extends Controller {
this.route({ this.route({
method: 'get', method: 'get',
path: PATH_STRATEGIES, path: PATH_STRATEGIES,
handler: this.getStrategies, handler: this.getFeatureStrategies,
permission: NONE, permission: NONE,
middleware: [ middleware: [
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'getStrategies', operationId: 'getFeatureStrategies',
responses: { 200: createResponseSchema('strategySchema') }, responses: {
200: createResponseSchema('featureStrategySchema'),
},
}), }),
], ],
}); });
@ -147,13 +149,15 @@ export default class ProjectFeaturesController extends Controller {
this.route({ this.route({
method: 'post', method: 'post',
path: PATH_STRATEGIES, path: PATH_STRATEGIES,
handler: this.addStrategy, handler: this.addFeatureStrategy,
permission: CREATE_FEATURE_STRATEGY, permission: CREATE_FEATURE_STRATEGY,
middleware: [ middleware: [
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'addStrategy', operationId: 'addFeatureStrategy',
requestBody: createRequestSchema('createStrategySchema'), requestBody: createRequestSchema(
'createFeatureStrategySchema',
),
responses: { responses: {
200: createResponseSchema('featureStrategySchema'), 200: createResponseSchema('featureStrategySchema'),
}, },
@ -164,12 +168,12 @@ export default class ProjectFeaturesController extends Controller {
this.route({ this.route({
method: 'get', method: 'get',
path: PATH_STRATEGY, path: PATH_STRATEGY,
handler: this.getStrategy, handler: this.getFeatureStrategy,
permission: NONE, permission: NONE,
middleware: [ middleware: [
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'getStrategy', operationId: 'getFeatureStrategy',
responses: { responses: {
200: createResponseSchema('featureStrategySchema'), 200: createResponseSchema('featureStrategySchema'),
}, },
@ -180,28 +184,31 @@ export default class ProjectFeaturesController extends Controller {
this.route({ this.route({
method: 'put', method: 'put',
path: PATH_STRATEGY, path: PATH_STRATEGY,
handler: this.updateStrategy, handler: this.updateFeatureStrategy,
permission: UPDATE_FEATURE_STRATEGY, permission: UPDATE_FEATURE_STRATEGY,
middleware: [ middleware: [
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'updateStrategy', operationId: 'updateFeatureStrategy',
requestBody: createRequestSchema('updateStrategySchema'), requestBody: createRequestSchema(
'updateFeatureStrategySchema',
),
responses: { responses: {
200: createResponseSchema('featureStrategySchema'), 200: createResponseSchema('featureStrategySchema'),
}, },
}), }),
], ],
}); });
this.route({ this.route({
method: 'patch', method: 'patch',
path: PATH_STRATEGY, path: PATH_STRATEGY,
handler: this.patchStrategy, handler: this.patchFeatureStrategy,
permission: UPDATE_FEATURE_STRATEGY, permission: UPDATE_FEATURE_STRATEGY,
middleware: [ middleware: [
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'patchStrategy', operationId: 'patchFeatureStrategy',
requestBody: createRequestSchema('patchesSchema'), requestBody: createRequestSchema('patchesSchema'),
responses: { responses: {
200: createResponseSchema('featureStrategySchema'), 200: createResponseSchema('featureStrategySchema'),
@ -213,11 +220,11 @@ export default class ProjectFeaturesController extends Controller {
method: 'delete', method: 'delete',
path: PATH_STRATEGY, path: PATH_STRATEGY,
acceptAnyContentType: true, acceptAnyContentType: true,
handler: this.deleteStrategy, handler: this.deleteFeatureStrategy,
permission: DELETE_FEATURE_STRATEGY, permission: DELETE_FEATURE_STRATEGY,
middleware: [ middleware: [
openApiService.validPath({ openApiService.validPath({
operationId: 'deleteStrategy', operationId: 'deleteFeatureStrategy',
tags: ['admin'], tags: ['admin'],
responses: { 200: emptyResponse }, responses: { 200: emptyResponse },
}), }),
@ -516,9 +523,13 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).end(); res.status(200).end();
} }
async addStrategy( async addFeatureStrategy(
req: IAuthRequest<FeatureStrategyParams, any, CreateStrategySchema>, req: IAuthRequest<
res: Response<StrategySchema>, FeatureStrategyParams,
any,
CreateFeatureStrategySchema
>,
res: Response<FeatureStrategySchema>,
): Promise<void> { ): Promise<void> {
const { projectId, featureName, environment } = req.params; const { projectId, featureName, environment } = req.params;
const userName = extractUsername(req); const userName = extractUsername(req);
@ -530,9 +541,9 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(strategy); res.status(200).json(strategy);
} }
async getStrategies( async getFeatureStrategies(
req: Request<FeatureStrategyParams, any, any, any>, req: Request<FeatureStrategyParams, any, any, any>,
res: Response<StrategySchema[]>, res: Response<FeatureStrategySchema[]>,
): Promise<void> { ): Promise<void> {
const { projectId, featureName, environment } = req.params; const { projectId, featureName, environment } = req.params;
const featureStrategies = const featureStrategies =
@ -544,9 +555,9 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(featureStrategies); res.status(200).json(featureStrategies);
} }
async updateStrategy( async updateFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, UpdateStrategySchema>, req: IAuthRequest<StrategyIdParams, any, UpdateFeatureStrategySchema>,
res: Response<StrategySchema>, res: Response<FeatureStrategySchema>,
): Promise<void> { ): Promise<void> {
const { strategyId, environment, projectId, featureName } = req.params; const { strategyId, environment, projectId, featureName } = req.params;
const userName = extractUsername(req); const userName = extractUsername(req);
@ -559,9 +570,9 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(updatedStrategy); res.status(200).json(updatedStrategy);
} }
async patchStrategy( async patchFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, Operation[], any>, req: IAuthRequest<StrategyIdParams, any, Operation[], any>,
res: Response<StrategySchema>, res: Response<FeatureStrategySchema>,
): Promise<void> { ): Promise<void> {
const { strategyId, projectId, environment, featureName } = req.params; const { strategyId, projectId, environment, featureName } = req.params;
const userName = extractUsername(req); const userName = extractUsername(req);
@ -577,9 +588,9 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(updatedStrategy); res.status(200).json(updatedStrategy);
} }
async getStrategy( async getFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, any, any>, req: IAuthRequest<StrategyIdParams, any, any, any>,
res: Response<StrategySchema>, res: Response<FeatureStrategySchema>,
): Promise<void> { ): Promise<void> {
this.logger.info('Getting strategy'); this.logger.info('Getting strategy');
const { strategyId } = req.params; const { strategyId } = req.params;
@ -588,7 +599,7 @@ export default class ProjectFeaturesController extends Controller {
res.status(200).json(strategy); res.status(200).json(strategy);
} }
async deleteStrategy( async deleteFeatureStrategy(
req: IAuthRequest<StrategyIdParams, any, any, any>, req: IAuthRequest<StrategyIdParams, any, any, any>,
res: Response<void>, res: Response<void>,
): Promise<void> { ): Promise<void> {
@ -612,7 +623,7 @@ export default class ProjectFeaturesController extends Controller {
{ name: string; value: string | number }, { name: string; value: string | number },
any any
>, >,
res: Response<StrategySchema>, res: Response<FeatureStrategySchema>,
): Promise<void> { ): Promise<void> {
const { strategyId, environment, projectId, featureName } = req.params; const { strategyId, environment, projectId, featureName } = req.params;
const userName = extractUsername(req); const userName = extractUsername(req);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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