mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-12 01:17:04 +02:00
fix: validate the type and length of parameter values (#1559)
* refactor: coerce primitive types in OpenAPI requests * refactor: avoid broken array args to serializeDates * refactor: avoid some spec refs to improve generated types * refactor: remove debug logging * refactor: fix IExpressOpenApi interface name prefix * refactor: ensure that parameter values are strings * refactor: test that parameter values are coerced to strings
This commit is contained in:
parent
61e9588bb0
commit
56615e91f0
@ -53,6 +53,15 @@ interface IFeatureStrategiesTable {
|
|||||||
created_at?: Date;
|
created_at?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureStringValues(data: object): { [key: string]: string } {
|
||||||
|
const stringEntries = Object.entries(data).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
String(value),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Object.fromEntries(stringEntries);
|
||||||
|
}
|
||||||
|
|
||||||
function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy {
|
function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@ -60,7 +69,7 @@ function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy {
|
|||||||
projectId: row.project_name,
|
projectId: row.project_name,
|
||||||
environment: row.environment,
|
environment: row.environment,
|
||||||
strategyName: row.strategy_name,
|
strategyName: row.strategy_name,
|
||||||
parameters: row.parameters,
|
parameters: ensureStringValues(row.parameters),
|
||||||
constraints: (row.constraints as unknown as IConstraint[]) || [],
|
constraints: (row.constraints as unknown as IConstraint[]) || [],
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
sortOrder: row.sort_order,
|
sortOrder: row.sort_order,
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
import { featuresSchema } from './spec/features-schema';
|
import { constraintSchema } from './spec/constraint-schema';
|
||||||
|
import { createFeatureSchema } from './spec/create-feature-schema';
|
||||||
|
import { createStrategySchema } from './spec/create-strategy-schema';
|
||||||
import { featureSchema } from './spec/feature-schema';
|
import { featureSchema } from './spec/feature-schema';
|
||||||
|
import { featuresSchema } from './spec/features-schema';
|
||||||
|
import { overrideSchema } from './spec/override-schema';
|
||||||
|
import { parametersSchema } from './spec/parameters-schema';
|
||||||
import { strategySchema } from './spec/strategy-schema';
|
import { strategySchema } from './spec/strategy-schema';
|
||||||
import { variantSchema } from './spec/variant-schema';
|
import { variantSchema } from './spec/variant-schema';
|
||||||
import { overrideSchema } from './spec/override-schema';
|
|
||||||
import { createFeatureSchema } from './spec/create-feature-schema';
|
|
||||||
import { constraintSchema } from './spec/constraint-schema';
|
|
||||||
|
|
||||||
// Create the base OpenAPI schema, with everything except paths.
|
|
||||||
export const createOpenApiSchema = (
|
export const createOpenApiSchema = (
|
||||||
serverUrl?: string,
|
serverUrl?: string,
|
||||||
): Omit<OpenAPIV3.Document, 'paths'> => {
|
): Omit<OpenAPIV3.Document, 'paths'> => {
|
||||||
@ -32,13 +33,15 @@ export const createOpenApiSchema = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
schemas: {
|
schemas: {
|
||||||
|
constraintSchema,
|
||||||
createFeatureSchema,
|
createFeatureSchema,
|
||||||
featuresSchema,
|
createStrategySchema,
|
||||||
featureSchema,
|
featureSchema,
|
||||||
|
featuresSchema,
|
||||||
|
overrideSchema,
|
||||||
|
parametersSchema,
|
||||||
strategySchema,
|
strategySchema,
|
||||||
variantSchema,
|
variantSchema,
|
||||||
overrideSchema,
|
|
||||||
constraintSchema,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||||
|
|
||||||
export const schema = {
|
const schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['contextName', 'operator'],
|
required: ['contextName', 'operator'],
|
||||||
@ -11,12 +11,21 @@ export const schema = {
|
|||||||
operator: {
|
operator: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
caseInsensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
inverted: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
value: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
12
src/lib/openapi/spec/create-strategy-request.ts
Normal file
12
src/lib/openapi/spec/create-strategy-request.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
|
||||||
|
export const createStrategyRequest: OpenAPIV3.RequestBodyObject = {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
$ref: '#/components/schemas/createStrategySchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
25
src/lib/openapi/spec/create-strategy-schema.ts
Normal file
25
src/lib/openapi/spec/create-strategy-schema.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||||
|
import { parametersSchema } from './parameters-schema';
|
||||||
|
import { constraintSchema } from './constraint-schema';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
sortOrder: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
constraints: {
|
||||||
|
type: 'array',
|
||||||
|
items: constraintSchema,
|
||||||
|
},
|
||||||
|
parameters: parametersSchema,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type CreateStrategySchema = CreateSchemaType<typeof schema>;
|
||||||
|
|
||||||
|
export const createStrategySchema = createSchemaObject(schema);
|
@ -1,4 +1,6 @@
|
|||||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||||
|
import { strategySchema } from './strategy-schema';
|
||||||
|
import { variantSchema } from './variant-schema';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -38,14 +40,10 @@ const schema = {
|
|||||||
},
|
},
|
||||||
strategies: {
|
strategies: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: strategySchema,
|
||||||
$ref: '#/components/schemas/strategySchema',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
items: {
|
items: variantSchema,
|
||||||
$ref: '#/components/schemas/variantSchema',
|
|
||||||
},
|
|
||||||
type: 'array',
|
type: 'array',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||||
|
import { featureSchema } from './feature-schema';
|
||||||
|
|
||||||
export const schema = {
|
const schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['version', 'features'],
|
required: ['version', 'features'],
|
||||||
@ -10,9 +11,7 @@ export const schema = {
|
|||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: featureSchema,
|
||||||
$ref: '#/components/schemas/featureSchema',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||||
|
|
||||||
export const schema = {
|
const schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['contextName', 'values'],
|
required: ['contextName', 'values'],
|
||||||
|
13
src/lib/openapi/spec/parameters-schema.ts
Normal file
13
src/lib/openapi/spec/parameters-schema.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 100,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ParametersSchema = CreateSchemaType<typeof schema>;
|
||||||
|
|
||||||
|
export const parametersSchema = createSchemaObject(schema);
|
12
src/lib/openapi/spec/strategy-response.ts
Normal file
12
src/lib/openapi/spec/strategy-response.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
|
||||||
|
export const strategyResponse: OpenAPIV3.ResponseObject = {
|
||||||
|
description: 'strategyResponse',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
$ref: '#/components/schemas/strategySchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -1,6 +1,8 @@
|
|||||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||||
|
import { constraintSchema } from './constraint-schema';
|
||||||
|
import { parametersSchema } from './parameters-schema';
|
||||||
|
|
||||||
export const schema = {
|
const schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['id', 'name', 'constraints', 'parameters'],
|
required: ['id', 'name', 'constraints', 'parameters'],
|
||||||
@ -13,13 +15,9 @@ export const schema = {
|
|||||||
},
|
},
|
||||||
constraints: {
|
constraints: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: constraintSchema,
|
||||||
$ref: '#/components/schemas/constraintSchema',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
},
|
},
|
||||||
|
parameters: parametersSchema,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||||
|
import { overrideSchema } from './override-schema';
|
||||||
|
|
||||||
export const schema = {
|
const schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['name', 'weight', 'weightType', 'stickiness', 'overrides'],
|
required: ['name', 'weight', 'weightType', 'stickiness'],
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -22,9 +23,7 @@ export const schema = {
|
|||||||
},
|
},
|
||||||
overrides: {
|
overrides: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: overrideSchema,
|
||||||
$ref: '#/components/schemas/overrideSchema',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -11,6 +11,7 @@ import FeatureToggleService from '../../services/feature-toggle-service';
|
|||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
import { featuresResponse } from '../../openapi/spec/features-response';
|
import { featuresResponse } from '../../openapi/spec/features-response';
|
||||||
import { FeaturesSchema } from '../../openapi/spec/features-schema';
|
import { FeaturesSchema } from '../../openapi/spec/features-schema';
|
||||||
|
import { serializeDates } from '../../util/serialize-dates';
|
||||||
|
|
||||||
export default class ArchiveController extends Controller {
|
export default class ArchiveController extends Controller {
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
@ -71,7 +72,11 @@ export default class ArchiveController extends Controller {
|
|||||||
const features = await this.featureService.getMetadataForAllFeatures(
|
const features = await this.featureService.getMetadataForAllFeatures(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
res.json({ version: 2, features });
|
|
||||||
|
res.json({
|
||||||
|
version: 2,
|
||||||
|
features: features.map(serializeDates),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArchivedFeaturesByProjectId(
|
async getArchivedFeaturesByProjectId(
|
||||||
@ -84,7 +89,10 @@ export default class ArchiveController extends Controller {
|
|||||||
true,
|
true,
|
||||||
projectId,
|
projectId,
|
||||||
);
|
);
|
||||||
res.json({ version: 2, features });
|
res.json({
|
||||||
|
version: 2,
|
||||||
|
features: features.map(serializeDates),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFeature(
|
async deleteFeature(
|
||||||
|
@ -20,6 +20,7 @@ import { IAuthRequest } from '../unleash-types';
|
|||||||
import { DEFAULT_ENV } from '../../util/constants';
|
import { DEFAULT_ENV } from '../../util/constants';
|
||||||
import { featuresResponse } from '../../openapi/spec/features-response';
|
import { featuresResponse } from '../../openapi/spec/features-response';
|
||||||
import { FeaturesSchema } from '../../openapi/spec/features-schema';
|
import { FeaturesSchema } from '../../openapi/spec/features-schema';
|
||||||
|
import { serializeDates } from '../../util/serialize-dates';
|
||||||
|
|
||||||
const version = 1;
|
const version = 1;
|
||||||
|
|
||||||
@ -120,7 +121,10 @@ class FeatureController extends Controller {
|
|||||||
const query = await this.prepQuery(req.query);
|
const query = await this.prepQuery(req.query);
|
||||||
const features = await this.service.getFeatureToggles(query);
|
const features = await this.service.getFeatureToggles(query);
|
||||||
|
|
||||||
res.json({ version, features });
|
res.json({
|
||||||
|
version,
|
||||||
|
features: features.map(serializeDates),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToggle(
|
async getToggle(
|
||||||
|
@ -14,11 +14,7 @@ import {
|
|||||||
UPDATE_FEATURE_ENVIRONMENT,
|
UPDATE_FEATURE_ENVIRONMENT,
|
||||||
UPDATE_FEATURE_STRATEGY,
|
UPDATE_FEATURE_STRATEGY,
|
||||||
} from '../../../types/permissions';
|
} from '../../../types/permissions';
|
||||||
import {
|
import { FeatureToggleDTO, IStrategyConfig } from '../../../types/model';
|
||||||
FeatureToggleDTO,
|
|
||||||
IConstraint,
|
|
||||||
IStrategyConfig,
|
|
||||||
} from '../../../types/model';
|
|
||||||
import { extractUsername } from '../../../util/extract-user';
|
import { extractUsername } from '../../../util/extract-user';
|
||||||
import { IAuthRequest } from '../../unleash-types';
|
import { IAuthRequest } from '../../unleash-types';
|
||||||
import { createFeatureRequest } from '../../../openapi/spec/create-feature-request';
|
import { createFeatureRequest } from '../../../openapi/spec/create-feature-request';
|
||||||
@ -26,6 +22,10 @@ import { featureResponse } from '../../../openapi/spec/feature-response';
|
|||||||
import { CreateFeatureSchema } from '../../../openapi/spec/create-feature-schema';
|
import { CreateFeatureSchema } from '../../../openapi/spec/create-feature-schema';
|
||||||
import { FeatureSchema } from '../../../openapi/spec/feature-schema';
|
import { FeatureSchema } from '../../../openapi/spec/feature-schema';
|
||||||
import { serializeDates } from '../../../util/serialize-dates';
|
import { serializeDates } from '../../../util/serialize-dates';
|
||||||
|
import { createStrategyRequest } from '../../../openapi/spec/create-strategy-request';
|
||||||
|
import { CreateStrategySchema } from '../../../openapi/spec/create-strategy-schema';
|
||||||
|
import { strategyResponse } from '../../../openapi/spec/strategy-response';
|
||||||
|
import { StrategySchema } from '../../../openapi/spec/strategy-schema';
|
||||||
import { featuresResponse } from '../../../openapi/spec/features-response';
|
import { featuresResponse } from '../../../openapi/spec/features-response';
|
||||||
|
|
||||||
interface FeatureStrategyParams {
|
interface FeatureStrategyParams {
|
||||||
@ -47,12 +47,6 @@ interface StrategyIdParams extends FeatureStrategyParams {
|
|||||||
strategyId: string;
|
strategyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StrategyUpdateBody {
|
|
||||||
name?: string;
|
|
||||||
constraints?: IConstraint[];
|
|
||||||
parameters?: object;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PATH = '/:projectId/features';
|
const PATH = '/:projectId/features';
|
||||||
const PATH_FEATURE = `${PATH}/:featureName`;
|
const PATH_FEATURE = `${PATH}/:featureName`;
|
||||||
const PATH_FEATURE_CLONE = `${PATH_FEATURE}/clone`;
|
const PATH_FEATURE_CLONE = `${PATH_FEATURE}/clone`;
|
||||||
@ -79,11 +73,13 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
this.logger = config.getLogger('/admin-api/project/features.ts');
|
this.logger = config.getLogger('/admin-api/project/features.ts');
|
||||||
|
|
||||||
this.get(`${PATH_ENV}`, this.getEnvironment);
|
this.get(`${PATH_ENV}`, this.getEnvironment);
|
||||||
|
|
||||||
this.post(
|
this.post(
|
||||||
`${PATH_ENV}/on`,
|
`${PATH_ENV}/on`,
|
||||||
this.toggleEnvironmentOn,
|
this.toggleEnvironmentOn,
|
||||||
UPDATE_FEATURE_ENVIRONMENT,
|
UPDATE_FEATURE_ENVIRONMENT,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.post(
|
this.post(
|
||||||
`${PATH_ENV}/off`,
|
`${PATH_ENV}/off`,
|
||||||
this.toggleEnvironmentOff,
|
this.toggleEnvironmentOff,
|
||||||
@ -91,22 +87,43 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.get(`${PATH_STRATEGIES}`, this.getStrategies);
|
this.get(`${PATH_STRATEGIES}`, this.getStrategies);
|
||||||
this.post(
|
|
||||||
`${PATH_STRATEGIES}`,
|
this.route({
|
||||||
this.addStrategy,
|
method: 'post',
|
||||||
CREATE_FEATURE_STRATEGY,
|
path: PATH_STRATEGIES,
|
||||||
);
|
handler: this.addStrategy,
|
||||||
|
permission: CREATE_FEATURE_STRATEGY,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['admin'],
|
||||||
|
requestBody: createStrategyRequest,
|
||||||
|
responses: { 200: strategyResponse },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
this.get(`${PATH_STRATEGY}`, this.getStrategy);
|
this.get(`${PATH_STRATEGY}`, this.getStrategy);
|
||||||
this.put(
|
|
||||||
`${PATH_STRATEGY}`,
|
this.route({
|
||||||
this.updateStrategy,
|
method: 'put',
|
||||||
UPDATE_FEATURE_STRATEGY,
|
path: PATH_STRATEGY,
|
||||||
);
|
handler: this.updateStrategy,
|
||||||
|
permission: UPDATE_FEATURE_STRATEGY,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['admin'],
|
||||||
|
requestBody: createStrategyRequest,
|
||||||
|
responses: { 200: strategyResponse },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
this.patch(
|
this.patch(
|
||||||
`${PATH_STRATEGY}`,
|
`${PATH_STRATEGY}`,
|
||||||
this.patchStrategy,
|
this.patchStrategy,
|
||||||
UPDATE_FEATURE_STRATEGY,
|
UPDATE_FEATURE_STRATEGY,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.delete(
|
this.delete(
|
||||||
`${PATH_STRATEGY}`,
|
`${PATH_STRATEGY}`,
|
||||||
this.deleteStrategy,
|
this.deleteStrategy,
|
||||||
@ -318,8 +335,8 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addStrategy(
|
async addStrategy(
|
||||||
req: IAuthRequest<FeatureStrategyParams, any, IStrategyConfig, any>,
|
req: IAuthRequest<FeatureStrategyParams, any, IStrategyConfig>,
|
||||||
res: Response,
|
res: Response<StrategySchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { projectId, featureName, environment } = req.params;
|
const { projectId, featureName, environment } = req.params;
|
||||||
const userName = extractUsername(req);
|
const userName = extractUsername(req);
|
||||||
@ -346,8 +363,8 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateStrategy(
|
async updateStrategy(
|
||||||
req: IAuthRequest<StrategyIdParams, any, StrategyUpdateBody, any>,
|
req: IAuthRequest<StrategyIdParams, any, CreateStrategySchema>,
|
||||||
res: Response,
|
res: Response<StrategySchema>,
|
||||||
): 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);
|
||||||
|
@ -69,6 +69,7 @@ import {
|
|||||||
validateString,
|
validateString,
|
||||||
} from '../util/validators/constraint-types';
|
} from '../util/validators/constraint-types';
|
||||||
import { IContextFieldStore } from 'lib/types/stores/context-field-store';
|
import { IContextFieldStore } from 'lib/types/stores/context-field-store';
|
||||||
|
import { Saved, Unsaved } from '../types/saved';
|
||||||
|
|
||||||
interface IFeatureContext {
|
interface IFeatureContext {
|
||||||
featureName: string;
|
featureName: string;
|
||||||
@ -268,7 +269,7 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
featureStrategyToPublic(
|
featureStrategyToPublic(
|
||||||
featureStrategy: IFeatureStrategy,
|
featureStrategy: IFeatureStrategy,
|
||||||
): IStrategyConfig {
|
): Saved<IStrategyConfig> {
|
||||||
return {
|
return {
|
||||||
id: featureStrategy.id,
|
id: featureStrategy.id,
|
||||||
name: featureStrategy.strategyName,
|
name: featureStrategy.strategyName,
|
||||||
@ -278,10 +279,10 @@ class FeatureToggleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createStrategy(
|
async createStrategy(
|
||||||
strategyConfig: Omit<IStrategyConfig, 'id'>,
|
strategyConfig: Unsaved<IStrategyConfig>,
|
||||||
context: IFeatureStrategyContext,
|
context: IFeatureStrategyContext,
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
): Promise<IStrategyConfig> {
|
): Promise<Saved<IStrategyConfig>> {
|
||||||
const { featureName, projectId, environment } = context;
|
const { featureName, projectId, environment } = context;
|
||||||
await this.validateFeatureContext(context);
|
await this.validateFeatureContext(context);
|
||||||
|
|
||||||
@ -342,7 +343,7 @@ class FeatureToggleService {
|
|||||||
updates: Partial<IFeatureStrategy>,
|
updates: Partial<IFeatureStrategy>,
|
||||||
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);
|
||||||
this.validateFeatureStrategyContext(existingStrategy, context);
|
this.validateFeatureStrategyContext(existingStrategy, context);
|
||||||
@ -392,7 +393,7 @@ class FeatureToggleService {
|
|||||||
this.validateFeatureStrategyContext(existingStrategy, context);
|
this.validateFeatureStrategyContext(existingStrategy, context);
|
||||||
|
|
||||||
if (existingStrategy.id === id) {
|
if (existingStrategy.id === id) {
|
||||||
existingStrategy.parameters[name] = value;
|
existingStrategy.parameters[name] = String(value);
|
||||||
const strategy = await this.featureStrategiesStore.updateStrategy(
|
const strategy = await this.featureStrategiesStore.updateStrategy(
|
||||||
id,
|
id,
|
||||||
existingStrategy,
|
existingStrategy,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import openapi, { ExpressOpenApi } from '@unleash/express-openapi';
|
import openapi, { IExpressOpenApi } from '@unleash/express-openapi';
|
||||||
import { Express, RequestHandler } from 'express';
|
import { Express, RequestHandler } from 'express';
|
||||||
import { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
@ -8,13 +8,14 @@ import { AdminApiOperation, ClientApiOperation } from '../openapi/types';
|
|||||||
export class OpenApiService {
|
export class OpenApiService {
|
||||||
private readonly config: IUnleashConfig;
|
private readonly config: IUnleashConfig;
|
||||||
|
|
||||||
private readonly api: ExpressOpenApi;
|
private readonly api: IExpressOpenApi;
|
||||||
|
|
||||||
constructor(config: IUnleashConfig) {
|
constructor(config: IUnleashConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.api = openapi(
|
this.api = openapi(
|
||||||
this.docsPath(),
|
this.docsPath(),
|
||||||
createOpenApiSchema(config.server?.unleashUrl),
|
createOpenApiSchema(config.server?.unleashUrl),
|
||||||
|
{ coerce: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export interface IStrategyConfig {
|
|||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
parameters: object;
|
parameters: { [key: string]: string };
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
}
|
}
|
||||||
export interface IFeatureStrategy {
|
export interface IFeatureStrategy {
|
||||||
@ -28,7 +28,7 @@ export interface IFeatureStrategy {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
strategyName: string;
|
strategyName: string;
|
||||||
parameters: object;
|
parameters: { [key: string]: string };
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
|
8
src/lib/types/openapi.d.ts
vendored
8
src/lib/types/openapi.d.ts
vendored
@ -3,11 +3,15 @@ declare module '@unleash/express-openapi' {
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
|
||||||
export interface ExpressOpenApi extends RequestHandler {
|
export interface IExpressOpenApi extends RequestHandler {
|
||||||
validPath: (operation: OpenAPIV3.OperationObject) => RequestHandler;
|
validPath: (operation: OpenAPIV3.OperationObject) => RequestHandler;
|
||||||
schema: (name: string, schema: OpenAPIV3.SchemaObject) => void;
|
schema: (name: string, schema: OpenAPIV3.SchemaObject) => void;
|
||||||
swaggerui: RequestHandler;
|
swaggerui: RequestHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function openapi(docsPath: string, any): ExpressOpenApi;
|
export default function openapi(
|
||||||
|
docsPath: string,
|
||||||
|
document: Omit<OpenAPIV3.Document, 'paths'>,
|
||||||
|
options?: { coerce: boolean },
|
||||||
|
): IExpressOpenApi;
|
||||||
}
|
}
|
||||||
|
7
src/lib/types/saved.ts
Normal file
7
src/lib/types/saved.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Add an id field to an object.
|
||||||
|
export type Saved<T extends {}, Id extends string | number = string> = T & {
|
||||||
|
id: Id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove an id field from an object.
|
||||||
|
export type Unsaved<T extends {}> = Omit<T, 'id'>;
|
@ -14,7 +14,7 @@ const input = `<!DOCTYPE html>
|
|||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"
|
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@ -80,7 +80,6 @@ test('rewriteHTML swaps out faviconPath if cdnPrefix is set', () => {
|
|||||||
|
|
||||||
test('rewriteHTML sets favicon path to root', () => {
|
test('rewriteHTML sets favicon path to root', () => {
|
||||||
const result = rewriteHTML(input, '');
|
const result = rewriteHTML(input, '');
|
||||||
console.log(result);
|
|
||||||
expect(result.includes('<link rel="icon" href="/favicon.ico" />')).toBe(
|
expect(result.includes('<link rel="icon" href="/favicon.ico" />')).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
@ -2,9 +2,13 @@ type SerializedDates<T> = {
|
|||||||
[P in keyof T]: T[P] extends Date ? string : T[P];
|
[P in keyof T]: T[P] extends Date ? string : T[P];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Disallow array arguments for serializeDates.
|
||||||
|
// Use `array.map(serializeDates)` instead.
|
||||||
|
type NotArray<T> = Exclude<T, unknown[]>;
|
||||||
|
|
||||||
// Serialize top-level date values to strings.
|
// Serialize top-level date values to strings.
|
||||||
export const serializeDates = <T extends object>(
|
export const serializeDates = <T extends object>(
|
||||||
obj: T,
|
obj: NotArray<T>,
|
||||||
): SerializedDates<T> => {
|
): SerializedDates<T> => {
|
||||||
const entries = Object.entries(obj).map(([k, v]) => {
|
const entries = Object.entries(obj).map(([k, v]) => {
|
||||||
if (v instanceof Date) {
|
if (v instanceof Date) {
|
||||||
|
@ -16,6 +16,7 @@ import IncompatibleProjectError from '../../../../../lib/error/incompatible-proj
|
|||||||
import { IVariant, WeightType } from '../../../../../lib/types/model';
|
import { IVariant, WeightType } from '../../../../../lib/types/model';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import supertest from 'supertest';
|
import supertest from 'supertest';
|
||||||
|
import { randomId } from '../../../../../lib/util/random-id';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -704,12 +705,7 @@ test('Can add strategy to feature toggle to a "some-env-2"', async () => {
|
|||||||
.post(
|
.post(
|
||||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||||
)
|
)
|
||||||
.send({
|
.send({ name: 'default', parameters: { userId: 'string' } })
|
||||||
name: 'default',
|
|
||||||
parameters: {
|
|
||||||
userId: 'string',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
await app.request
|
await app.request
|
||||||
.get(`/api/admin/projects/default/features/${featureName}`)
|
.get(`/api/admin/projects/default/features/${featureName}`)
|
||||||
@ -722,7 +718,6 @@ test('Can add strategy to feature toggle to a "some-env-2"', async () => {
|
|||||||
test('Can update strategy on feature toggle', async () => {
|
test('Can update strategy on feature toggle', async () => {
|
||||||
const envName = 'default';
|
const envName = 'default';
|
||||||
const featureName = 'feature.strategy.update.strat';
|
const featureName = 'feature.strategy.update.strat';
|
||||||
|
|
||||||
const projectPath = '/api/admin/projects/default';
|
const projectPath = '/api/admin/projects/default';
|
||||||
const featurePath = `${projectPath}/features/${featureName}`;
|
const featurePath = `${projectPath}/features/${featureName}`;
|
||||||
|
|
||||||
@ -735,23 +730,13 @@ test('Can update strategy on feature toggle', async () => {
|
|||||||
// add strategy
|
// add strategy
|
||||||
const { body: strategy } = await app.request
|
const { body: strategy } = await app.request
|
||||||
.post(`${featurePath}/environments/${envName}/strategies`)
|
.post(`${featurePath}/environments/${envName}/strategies`)
|
||||||
.send({
|
.send({ name: 'default', parameters: { userIds: '' } })
|
||||||
name: 'default',
|
|
||||||
parameters: {
|
|
||||||
userIds: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
// update strategy
|
// update strategy
|
||||||
await app.request
|
await app.request
|
||||||
.put(`${featurePath}/environments/${envName}/strategies/${strategy.id}`)
|
.put(`${featurePath}/environments/${envName}/strategies/${strategy.id}`)
|
||||||
.send({
|
.send({ name: 'default', parameters: { userIds: 1234 } })
|
||||||
name: 'default',
|
|
||||||
parameters: {
|
|
||||||
userIds: '1234',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const { body } = await app.request.get(`${featurePath}`);
|
const { body } = await app.request.get(`${featurePath}`);
|
||||||
@ -764,6 +749,47 @@ test('Can update strategy on feature toggle', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should coerce all strategy parameter values to strings', async () => {
|
||||||
|
const envName = 'default';
|
||||||
|
const featureName = randomId();
|
||||||
|
const projectPath = '/api/admin/projects/default';
|
||||||
|
const featurePath = `${projectPath}/features/${featureName}`;
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post(`${projectPath}/features`)
|
||||||
|
.send({ name: featureName })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post(`${featurePath}/environments/${envName}/strategies`)
|
||||||
|
.send({ name: 'default', parameters: { foo: 1234 } })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const { body } = await app.request.get(`${featurePath}`);
|
||||||
|
const defaultEnv = body.environments[0];
|
||||||
|
expect(defaultEnv.strategies).toHaveLength(1);
|
||||||
|
expect(defaultEnv.strategies[0].parameters).toStrictEqual({
|
||||||
|
foo: '1234',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should limit the length of parameter values', async () => {
|
||||||
|
const envName = 'default';
|
||||||
|
const featureName = randomId();
|
||||||
|
const projectPath = '/api/admin/projects/default';
|
||||||
|
const featurePath = `${projectPath}/features/${featureName}`;
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post(`${projectPath}/features`)
|
||||||
|
.send({ name: featureName })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post(`${featurePath}/environments/${envName}/strategies`)
|
||||||
|
.send({ name: 'default', parameters: { foo: 'x'.repeat(101) } })
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
test('Can NOT delete strategy with wrong projectId', async () => {
|
test('Can NOT delete strategy with wrong projectId', async () => {
|
||||||
const envName = 'default';
|
const envName = 'default';
|
||||||
const featureName = 'feature.strategy.delete.strat.error';
|
const featureName = 'feature.strategy.delete.strat.error';
|
||||||
@ -1110,7 +1136,7 @@ test('Can patch a strategy based on id', async () => {
|
|||||||
)
|
)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
expect(res.body.parameters.rollout).toBe(42);
|
expect(res.body.parameters.rollout).toBe('42');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1209,7 +1235,7 @@ test('Deleting a strategy should include name of feature strategy was deleted fr
|
|||||||
.post(
|
.post(
|
||||||
`/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies`,
|
`/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies`,
|
||||||
)
|
)
|
||||||
.send({ name: 'default', constraints: [], properties: {} })
|
.send({ name: 'default', constraints: [] })
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
strategyId = res.body.id;
|
strategyId = res.body.id;
|
||||||
@ -1257,7 +1283,7 @@ test('Enabling environment creates a FEATURE_ENVIRONMENT_ENABLED event', async (
|
|||||||
.post(
|
.post(
|
||||||
`/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies`,
|
`/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies`,
|
||||||
)
|
)
|
||||||
.send({ name: 'default', constraints: [], properties: {} })
|
.send({ name: 'default', constraints: [] })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
await app.request
|
await app.request
|
||||||
@ -1299,7 +1325,7 @@ test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async
|
|||||||
.post(
|
.post(
|
||||||
`/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies`,
|
`/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies`,
|
||||||
)
|
)
|
||||||
.send({ name: 'default', constraints: [], properties: {} })
|
.send({ name: 'default', constraints: [] })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
await app.request
|
await app.request
|
||||||
|
@ -54,12 +54,21 @@ Object {
|
|||||||
"constraintSchema": Object {
|
"constraintSchema": Object {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": Object {
|
"properties": Object {
|
||||||
|
"caseInsensitive": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"contextName": Object {
|
"contextName": Object {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"inverted": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"operator": Object {
|
"operator": Object {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"value": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
"values": Object {
|
"values": Object {
|
||||||
"items": Object {
|
"items": Object {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -93,6 +102,59 @@ Object {
|
|||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"createStrategySchema": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"constraints": Object {
|
||||||
|
"items": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"caseInsensitive": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"contextName": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"inverted": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"operator": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"value": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"values": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"contextName",
|
||||||
|
"operator",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"name": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"parameters": Object {
|
||||||
|
"additionalProperties": Object {
|
||||||
|
"maxLength": 100,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"sortOrder": Object {
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
"featureSchema": Object {
|
"featureSchema": Object {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": Object {
|
"properties": Object {
|
||||||
@ -126,7 +188,63 @@ Object {
|
|||||||
},
|
},
|
||||||
"strategies": Object {
|
"strategies": Object {
|
||||||
"items": Object {
|
"items": Object {
|
||||||
"$ref": "#/components/schemas/strategySchema",
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"constraints": Object {
|
||||||
|
"items": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"caseInsensitive": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"contextName": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"inverted": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"operator": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"value": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"values": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"contextName",
|
||||||
|
"operator",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"id": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"name": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"parameters": Object {
|
||||||
|
"additionalProperties": Object {
|
||||||
|
"maxLength": 100,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"constraints",
|
||||||
|
"parameters",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
@ -135,7 +253,53 @@ Object {
|
|||||||
},
|
},
|
||||||
"variants": Object {
|
"variants": Object {
|
||||||
"items": Object {
|
"items": Object {
|
||||||
"$ref": "#/components/schemas/variantSchema",
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"name": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"overrides": Object {
|
||||||
|
"items": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"contextName": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"values": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"contextName",
|
||||||
|
"values",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"payload": Object {
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"stickiness": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"weight": Object {
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
"weightType": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"name",
|
||||||
|
"weight",
|
||||||
|
"weightType",
|
||||||
|
"stickiness",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
@ -151,7 +315,159 @@ Object {
|
|||||||
"properties": Object {
|
"properties": Object {
|
||||||
"features": Object {
|
"features": Object {
|
||||||
"items": Object {
|
"items": Object {
|
||||||
"$ref": "#/components/schemas/featureSchema",
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"createdAt": Object {
|
||||||
|
"format": "date",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"description": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"enabled": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"impressionData": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"lastSeenAt": Object {
|
||||||
|
"format": "date",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"name": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"project": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"stale": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"strategies": Object {
|
||||||
|
"items": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"constraints": Object {
|
||||||
|
"items": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"caseInsensitive": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"contextName": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"inverted": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"operator": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"value": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"values": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"contextName",
|
||||||
|
"operator",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"id": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"name": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"parameters": Object {
|
||||||
|
"additionalProperties": Object {
|
||||||
|
"maxLength": 100,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"constraints",
|
||||||
|
"parameters",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"type": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"variants": Object {
|
||||||
|
"items": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"name": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"overrides": Object {
|
||||||
|
"items": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"contextName": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"values": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"contextName",
|
||||||
|
"values",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"payload": Object {
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"stickiness": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"weight": Object {
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
"weightType": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"name",
|
||||||
|
"weight",
|
||||||
|
"weightType",
|
||||||
|
"stickiness",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"name",
|
||||||
|
"project",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
@ -184,12 +500,47 @@ Object {
|
|||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"parametersSchema": Object {
|
||||||
|
"additionalProperties": Object {
|
||||||
|
"maxLength": 100,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
"strategySchema": Object {
|
"strategySchema": Object {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": Object {
|
"properties": Object {
|
||||||
"constraints": Object {
|
"constraints": Object {
|
||||||
"items": Object {
|
"items": Object {
|
||||||
"$ref": "#/components/schemas/constraintSchema",
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"caseInsensitive": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"contextName": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"inverted": Object {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"operator": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"value": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"values": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"contextName",
|
||||||
|
"operator",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
@ -200,6 +551,10 @@ Object {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
"parameters": Object {
|
"parameters": Object {
|
||||||
|
"additionalProperties": Object {
|
||||||
|
"maxLength": 100,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -219,7 +574,23 @@ Object {
|
|||||||
},
|
},
|
||||||
"overrides": Object {
|
"overrides": Object {
|
||||||
"items": Object {
|
"items": Object {
|
||||||
"$ref": "#/components/schemas/overrideSchema",
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"contextName": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"values": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"contextName",
|
||||||
|
"values",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
@ -241,7 +612,6 @@ Object {
|
|||||||
"weight",
|
"weight",
|
||||||
"weightType",
|
"weightType",
|
||||||
"stickiness",
|
"stickiness",
|
||||||
"overrides",
|
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
@ -432,6 +802,124 @@ Object {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/strategies": Object {
|
||||||
|
"post": Object {
|
||||||
|
"parameters": Array [
|
||||||
|
Object {
|
||||||
|
"in": "path",
|
||||||
|
"name": "projectId",
|
||||||
|
"required": true,
|
||||||
|
"schema": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"in": "path",
|
||||||
|
"name": "featureName",
|
||||||
|
"required": true,
|
||||||
|
"schema": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"in": "path",
|
||||||
|
"name": "environment",
|
||||||
|
"required": true,
|
||||||
|
"schema": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"requestBody": Object {
|
||||||
|
"content": Object {
|
||||||
|
"application/json": Object {
|
||||||
|
"schema": Object {
|
||||||
|
"$ref": "#/components/schemas/createStrategySchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": Object {
|
||||||
|
"200": Object {
|
||||||
|
"content": Object {
|
||||||
|
"application/json": Object {
|
||||||
|
"schema": Object {
|
||||||
|
"$ref": "#/components/schemas/strategySchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "strategyResponse",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": Array [
|
||||||
|
"admin",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/strategies/{strategyId}": Object {
|
||||||
|
"put": Object {
|
||||||
|
"parameters": Array [
|
||||||
|
Object {
|
||||||
|
"in": "path",
|
||||||
|
"name": "projectId",
|
||||||
|
"required": true,
|
||||||
|
"schema": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"in": "path",
|
||||||
|
"name": "featureName",
|
||||||
|
"required": true,
|
||||||
|
"schema": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"in": "path",
|
||||||
|
"name": "environment",
|
||||||
|
"required": true,
|
||||||
|
"schema": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"in": "path",
|
||||||
|
"name": "strategyId",
|
||||||
|
"required": true,
|
||||||
|
"schema": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"requestBody": Object {
|
||||||
|
"content": Object {
|
||||||
|
"application/json": Object {
|
||||||
|
"schema": Object {
|
||||||
|
"$ref": "#/components/schemas/createStrategySchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": Object {
|
||||||
|
"200": Object {
|
||||||
|
"content": Object {
|
||||||
|
"application/json": Object {
|
||||||
|
"schema": Object {
|
||||||
|
"$ref": "#/components/schemas/strategySchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "strategyResponse",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": Array [
|
||||||
|
"admin",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"security": Array [
|
"security": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -75,14 +75,12 @@ test('Should be able to update existing strategy configuration', async () => {
|
|||||||
expect(createdConfig.name).toEqual('default');
|
expect(createdConfig.name).toEqual('default');
|
||||||
const updatedConfig = await service.updateStrategy(
|
const updatedConfig = await service.updateStrategy(
|
||||||
createdConfig.id,
|
createdConfig.id,
|
||||||
{
|
{ parameters: { b2b: 'true' } },
|
||||||
parameters: { b2b: true },
|
|
||||||
},
|
|
||||||
{ projectId, featureName, environment: DEFAULT_ENV },
|
{ projectId, featureName, environment: DEFAULT_ENV },
|
||||||
username,
|
username,
|
||||||
);
|
);
|
||||||
expect(createdConfig.id).toEqual(updatedConfig.id);
|
expect(createdConfig.id).toEqual(updatedConfig.id);
|
||||||
expect(updatedConfig.parameters).toEqual({ b2b: true });
|
expect(updatedConfig.parameters).toEqual({ b2b: 'true' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should be able to get strategy by id', async () => {
|
test('Should be able to get strategy by id', async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user