1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +02:00

refactor: add soft response schema validation (#1657)

* refactor: remove most schema refs

* refactor: generalize request/response schemas

* refactor: simplify schema date formats

* refactor: add soft response schema validation

* refactor: fix emptySchema definition

* refactor: update json-schema-to-ts and use refs
This commit is contained in:
olav 2022-06-08 08:01:14 +02:00 committed by GitHub
parent dadbc3addc
commit 13ef025fab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 906 additions and 639 deletions

View File

@ -77,6 +77,8 @@
},
"dependencies": {
"@unleash/express-openapi": "^0.2.0",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"async": "^3.2.3",
"bcryptjs": "^2.4.3",
"compression": "^1.7.4",
@ -97,7 +99,7 @@
"helmet": "^5.0.0",
"joi": "^17.3.0",
"js-yaml": "^4.1.0",
"json-schema-to-ts": "^2.0.0",
"json-schema-to-ts": "^2.5.3",
"knex": "^2.0.0",
"log4js": "^6.0.0",
"make-fetch-happen": "^10.1.2",

View File

@ -1,22 +1,98 @@
import { OpenAPIV3 } from 'openapi-types';
import { featuresSchema } from './spec/features-schema';
import { overrideSchema } from './spec/override-schema';
import { strategySchema } from './spec/strategy-schema';
import { variantSchema } from './spec/variant-schema';
import { createFeatureSchema } from './spec/create-feature-schema';
import { constraintSchema } from './spec/constraint-schema';
import { tagSchema } from './spec/tag-schema';
import { tagsResponseSchema } from './spec/tags-response-schema';
import { createStrategySchema } from './spec/create-strategy-schema';
import { featureSchema } from './spec/feature-schema';
import { parametersSchema } from './spec/parameters-schema';
import { featureEnvironmentSchema } from './spec/feature-environment-schema';
import { emptyResponseSchema } from './spec/empty-response-schema';
import { patchOperationSchema } from './spec/patch-operation-schema';
import { updateFeatureSchema } from './spec/updateFeatureSchema';
import { updateStrategySchema } from './spec/update-strategy-schema';
import { cloneFeatureSchema } from './spec/clone-feature-schema';
import { constraintSchema } from './spec/constraint-schema';
import { createFeatureSchema } from './spec/create-feature-schema';
import { createStrategySchema } from './spec/create-strategy-schema';
import { emptySchema } from './spec/empty-schema';
import { featureEnvironmentSchema } from './spec/feature-environment-schema';
import { featureSchema } from './spec/feature-schema';
import { featureStrategySchema } from './spec/feature-strategy-schema';
import { featureVariantsSchema } from './spec/feature-variants-schema';
import { featuresSchema } from './spec/features-schema';
import { mapValues } from '../util/map-values';
import { omitKeys } from '../util/omit-keys';
import { overrideSchema } from './spec/override-schema';
import { parametersSchema } from './spec/parameters-schema';
import { patchSchema } from './spec/patch-schema';
import { patchesSchema } from './spec/patches-schema';
import { strategySchema } from './spec/strategy-schema';
import { tagSchema } from './spec/tag-schema';
import { tagsSchema } from './spec/tags-schema';
import { updateFeatureSchema } from './spec/update-feature-schema';
import { updateStrategySchema } from './spec/update-strategy-schema';
import { variantSchema } from './spec/variant-schema';
import { variantsSchema } from './spec/variants-schema';
// Schemas must have $id property on the form "#/components/schemas/mySchema".
export type SchemaId = typeof schemas[keyof typeof schemas]['$id'];
// Schemas must list all $ref schemas in "components", including nested schemas.
export type SchemaRef = typeof schemas[keyof typeof schemas]['components'];
export interface AdminApiOperation
extends Omit<OpenAPIV3.OperationObject, 'tags'> {
tags: ['admin'];
}
export interface ClientApiOperation
extends Omit<OpenAPIV3.OperationObject, 'tags'> {
tags: ['client'];
}
export const schemas = {
cloneFeatureSchema,
constraintSchema,
createFeatureSchema,
createStrategySchema,
emptySchema,
featureEnvironmentSchema,
featureSchema,
featureStrategySchema,
featureVariantsSchema,
featuresSchema,
overrideSchema,
parametersSchema,
patchSchema,
patchesSchema,
strategySchema,
tagSchema,
tagsSchema,
updateFeatureSchema,
updateStrategySchema,
variantSchema,
variantsSchema,
};
export const createRequestSchema = (
schemaName: string,
): OpenAPIV3.RequestBodyObject => {
return {
description: schemaName,
required: true,
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${schemaName}`,
},
},
},
};
};
export const createResponseSchema = (
schemaName: string,
): OpenAPIV3.ResponseObject => {
return {
description: schemaName,
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${schemaName}`,
},
},
},
};
};
export const createOpenApiSchema = (
serverUrl?: string,
@ -26,13 +102,9 @@ export const createOpenApiSchema = (
servers: serverUrl ? [{ url: serverUrl }] : [],
info: {
title: 'Unleash API',
version: process.env.npm_package_version,
version: process.env.npm_package_version!,
},
security: [
{
apiKey: [],
},
],
security: [{ apiKey: [] }],
components: {
securitySchemes: {
apiKey: {
@ -41,26 +113,9 @@ export const createOpenApiSchema = (
name: 'Authorization',
},
},
schemas: {
constraintSchema,
cloneFeatureSchema,
createFeatureSchema,
createStrategySchema,
featureSchema,
featuresSchema,
featureEnvironmentSchema,
featureStrategySchema,
emptyResponseSchema,
overrideSchema,
parametersSchema,
patchOperationSchema,
strategySchema,
updateStrategySchema,
updateFeatureSchema,
variantSchema,
tagSchema,
tagsResponseSchema,
},
schemas: mapValues(schemas, (schema) =>
omitKeys(schema, '$id', 'components'),
),
},
};
};

View File

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`featureSchema constraints 1`] = `
Object {
"data": Object {
"name": "a",
"strategies": Array [
Object {
"constraints": Array [
Object {
"contextName": "a",
},
],
"name": "a",
},
],
},
"errors": Array [
Object {
"instancePath": "/strategies/0/constraints/0",
"keyword": "required",
"message": "must have required property 'operator'",
"params": Object {
"missingProperty": "operator",
},
"schemaPath": "#/components/schemas/constraintSchema/required",
},
],
"schema": "#/components/schemas/featureSchema",
}
`;
exports[`featureSchema overrides 1`] = `
Object {
"data": Object {
"name": "a",
"variants": Array [
Object {
"name": "a",
"overrides": Array [
Object {
"contextName": "a",
"values": "b",
},
],
"payload": Object {
"type": "a",
"value": "b",
},
"stickiness": "a",
"weight": 1,
"weightType": "a",
},
],
},
"errors": Array [
Object {
"instancePath": "/variants/0/overrides/0/values",
"keyword": "type",
"message": "must be array",
"params": Object {
"type": "array",
},
"schemaPath": "#/components/schemas/overrideSchema/properties/values/type",
},
],
"schema": "#/components/schemas/featureSchema",
}
`;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const cloneFeatureRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/cloneFeatureSchema',
},
},
},
};

View File

@ -1,6 +1,7 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
const schema = {
export const cloneFeatureSchema = {
$id: '#/components/schemas/cloneFeatureSchema',
type: 'object',
required: ['name'],
properties: {
@ -11,9 +12,7 @@ const schema = {
type: 'boolean',
},
},
'components/schemas': {},
components: {},
} as const;
export type CloneFeatureSchema = CreateSchemaType<typeof schema>;
export const cloneFeatureSchema = createSchemaObject(schema);
export type CloneFeatureSchema = FromSchema<typeof cloneFeatureSchema>;

View File

@ -1,7 +1,8 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { ALL_OPERATORS } from '../../util/constants';
const schema = {
export const constraintSchema = {
$id: '#/components/schemas/constraintSchema',
type: 'object',
additionalProperties: false,
required: ['contextName', 'operator'],
@ -29,9 +30,7 @@ const schema = {
type: 'string',
},
},
'components/schemas': {},
components: {},
} as const;
export type ConstraintSchema = CreateSchemaType<typeof schema>;
export const constraintSchema = createSchemaObject(schema);
export type ConstraintSchema = FromSchema<typeof constraintSchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const createFeatureRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/createFeatureSchema',
},
},
},
};

View File

@ -1,6 +1,7 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
const schema = {
export const createFeatureSchema = {
$id: '#/components/schemas/createFeatureSchema',
type: 'object',
required: ['name'],
properties: {
@ -17,9 +18,7 @@ const schema = {
type: 'boolean',
},
},
'components/schemas': {},
components: {},
} as const;
export type CreateFeatureSchema = CreateSchemaType<typeof schema>;
export const createFeatureSchema = createSchemaObject(schema);
export type CreateFeatureSchema = FromSchema<typeof createFeatureSchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const createStrategyRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/createStrategySchema',
},
},
},
};

View File

@ -1,8 +1,9 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { parametersSchema } from './parameters-schema';
import { constraintSchema } from './constraint-schema';
const schema = {
export const createStrategySchema = {
$id: '#/components/schemas/createStrategySchema',
type: 'object',
additionalProperties: false,
required: ['name'],
@ -23,12 +24,12 @@ const schema = {
$ref: '#/components/schemas/parametersSchema',
},
},
'components/schemas': {
constraintSchema,
parametersSchema,
components: {
schemas: {
constraintSchema,
parametersSchema,
},
},
} as const;
export type CreateStrategySchema = CreateSchemaType<typeof schema>;
export const createStrategySchema = createSchemaObject(schema);
export type CreateStrategySchema = FromSchema<typeof createStrategySchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const createTagRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/tagSchema',
},
},
},
};

View File

@ -1,11 +0,0 @@
import { createSchemaObject, CreateSchemaType } from '../types';
const schema = {
type: 'object',
description: 'OK',
'components/schemas': {},
} as const;
export type EmptyResponseSchema = CreateSchemaType<typeof schema>;
export const emptyResponseSchema = createSchemaObject(schema);

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const emptyResponse: OpenAPIV3.ResponseObject = {
description: 'emptyResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/emptyResponseSchema',
},
},
},
};

View File

@ -0,0 +1,9 @@
import { FromSchema } from 'json-schema-to-ts';
export const emptySchema = {
$id: '#/components/schemas/emptySchema',
description: 'emptySchema',
components: {},
} as const;
export type EmptySchema = FromSchema<typeof emptySchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const featureEnvironmentResponse: OpenAPIV3.ResponseObject = {
description: 'featureEnvironmentResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/featureEnvironmentSchema',
},
},
},
};

View File

@ -1,9 +1,10 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { featureStrategySchema } from './feature-strategy-schema';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
let schema = {
export const featureEnvironmentSchema = {
$id: '#/components/schemas/featureEnvironmentSchema',
type: 'object',
additionalProperties: false,
required: ['name', 'enabled'],
@ -27,13 +28,15 @@ let schema = {
},
},
},
'components/schemas': {
featureStrategySchema,
constraintSchema,
parametersSchema,
components: {
schemas: {
featureStrategySchema,
constraintSchema,
parametersSchema,
},
},
} as const;
export type FeatureEnvironmentSchema = CreateSchemaType<typeof schema>;
export const featureEnvironmentSchema = createSchemaObject(schema);
export type FeatureEnvironmentSchema = FromSchema<
typeof featureEnvironmentSchema
>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const featureResponse: OpenAPIV3.ResponseObject = {
description: 'featureResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/featureSchema',
},
},
},
};

View File

@ -0,0 +1,64 @@
import { validateSchema } from '../validate';
import { FeatureSchema } from './feature-schema';
test('featureSchema', () => {
const data: FeatureSchema = {
name: 'a',
strategies: [
{
name: 'a',
constraints: [
{
contextName: 'a',
operator: 'IN',
},
],
},
],
variants: [
{
name: 'a',
weight: 1,
weightType: 'a',
stickiness: 'a',
overrides: [{ contextName: 'a', values: ['a'] }],
payload: { type: 'a', value: 'b' },
},
],
};
expect(
validateSchema('#/components/schemas/featureSchema', data),
).toBeUndefined();
});
test('featureSchema constraints', () => {
const data = {
name: 'a',
strategies: [{ name: 'a', constraints: [{ contextName: 'a' }] }],
};
expect(
validateSchema('#/components/schemas/featureSchema', data),
).toMatchSnapshot();
});
test('featureSchema overrides', () => {
const data = {
name: 'a',
variants: [
{
name: 'a',
weight: 1,
weightType: 'a',
stickiness: 'a',
overrides: [{ contextName: 'a', values: 'b' }],
payload: { type: 'a', value: 'b' },
},
],
};
expect(
validateSchema('#/components/schemas/featureSchema', data),
).toMatchSnapshot();
});

View File

@ -1,13 +1,12 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { strategySchema } from './strategy-schema';
import { FromSchema } from 'json-schema-to-ts';
import { variantSchema } from './variant-schema';
import { featureEnvironmentSchema } from './feature-environment-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { strategySchema } from './strategy-schema';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
import { overrideSchema } from './override-schema';
import { parametersSchema } from './parameters-schema';
const schema = {
export const featureSchema = {
$id: '#/components/schemas/featureSchema',
type: 'object',
additionalProperties: false,
required: ['name'],
@ -38,18 +37,18 @@ const schema = {
},
createdAt: {
type: 'string',
format: 'date',
format: 'date-time',
nullable: true,
},
lastSeenAt: {
type: 'string',
format: 'date',
format: 'date-time',
nullable: true,
},
environments: {
type: 'array',
items: {
$ref: '#/components/schemas/featureEnvironmentSchema',
type: 'object',
},
},
strategies: {
@ -65,17 +64,15 @@ const schema = {
},
},
},
'components/schemas': {
constraintSchema,
featureEnvironmentSchema,
featureStrategySchema,
overrideSchema,
parametersSchema,
strategySchema,
variantSchema,
components: {
schemas: {
constraintSchema,
overrideSchema,
parametersSchema,
strategySchema,
variantSchema,
},
},
} as const;
export type FeatureSchema = CreateSchemaType<typeof schema>;
export const featureSchema = createSchemaObject(schema);
export type FeatureSchema = FromSchema<typeof featureSchema>;

View File

@ -1,8 +1,9 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
export const schema = {
export const featureStrategySchema = {
$id: '#/components/schemas/featureStrategySchema',
type: 'object',
additionalProperties: false,
required: [
@ -22,7 +23,7 @@ export const schema = {
},
createdAt: {
type: 'string',
format: 'date',
format: 'date-time',
nullable: true,
},
featureName: {
@ -50,12 +51,12 @@ export const schema = {
$ref: '#/components/schemas/parametersSchema',
},
},
'components/schemas': {
constraintSchema,
parametersSchema,
components: {
schemas: {
constraintSchema,
parametersSchema,
},
},
} as const;
export type FeatureStrategySchema = CreateSchemaType<typeof schema>;
export const featureStrategySchema = createSchemaObject(schema);
export type FeatureStrategySchema = FromSchema<typeof featureStrategySchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const featureVariantsResponse: OpenAPIV3.ResponseObject = {
description: 'featureVariantResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/featureVariantsSchema',
},
},
},
};

View File

@ -1,7 +1,9 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { variantSchema } from './variant-schema';
import { FromSchema } from 'json-schema-to-ts';
import { overrideSchema } from './override-schema';
const schema = {
export const featureVariantsSchema = {
$id: '#/components/schemas/featureVariantsSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'variants'],
@ -16,11 +18,12 @@ const schema = {
},
},
},
'components/schemas': {
variantSchema,
components: {
schemas: {
variantSchema,
overrideSchema,
},
},
};
} as const;
export type FeatureVariantsSchema = CreateSchemaType<typeof schema>;
export const featureVariantsSchema = createSchemaObject(schema);
export type FeatureVariantsSchema = FromSchema<typeof featureVariantsSchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const featuresResponse: OpenAPIV3.ResponseObject = {
description: 'featuresResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/featuresSchema',
},
},
},
};

View File

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

View File

@ -1,14 +1,13 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { featureSchema } from './feature-schema';
import { parametersSchema } from './parameters-schema';
import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema';
import { featureEnvironmentSchema } from './feature-environment-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { constraintSchema } from './constraint-schema';
import { strategySchema } from './strategy-schema';
const schema = {
export const featuresSchema = {
$id: '#/components/schemas/featuresSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'features'],
@ -23,18 +22,16 @@ const schema = {
},
},
},
'components/schemas': {
featureSchema,
constraintSchema,
featureEnvironmentSchema,
featureStrategySchema,
overrideSchema,
parametersSchema,
strategySchema,
variantSchema,
components: {
schemas: {
constraintSchema,
featureSchema,
overrideSchema,
parametersSchema,
strategySchema,
variantSchema,
},
},
} as const;
export type FeaturesSchema = CreateSchemaType<typeof schema>;
export const featuresSchema = createSchemaObject(schema);
export type FeaturesSchema = FromSchema<typeof featuresSchema>;

View File

@ -1,6 +1,7 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
const schema = {
export const overrideSchema = {
$id: '#/components/schemas/overrideSchema',
type: 'object',
additionalProperties: false,
required: ['contextName', 'values'],
@ -15,9 +16,7 @@ const schema = {
},
},
},
'components/schemas': {},
components: {},
} as const;
export type OverrideSchema = CreateSchemaType<typeof schema>;
export const overrideSchema = createSchemaObject(schema);
export type OverrideSchema = FromSchema<typeof overrideSchema>;

View File

@ -1,13 +1,12 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
const schema = {
export const parametersSchema = {
$id: '#/components/schemas/parametersSchema',
type: 'object',
additionalProperties: {
type: 'string',
},
'components/schemas': {},
components: {},
} as const;
export type ParametersSchema = CreateSchemaType<typeof schema>;
export const parametersSchema = createSchemaObject(schema);
export type ParametersSchema = FromSchema<typeof parametersSchema>;

View File

@ -1,15 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const patchRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/patchOperationSchema',
},
},
},
},
};

View File

@ -1,6 +1,7 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
const schema = {
export const patchSchema = {
$id: '#/components/schemas/patchSchema',
type: 'object',
required: ['path', 'op'],
properties: {
@ -16,9 +17,7 @@ const schema = {
},
value: {},
},
'components/schemas': {},
components: {},
} as const;
export type PatchOperationSchema = CreateSchemaType<typeof schema>;
export const patchOperationSchema = createSchemaObject(schema);
export type PatchSchema = FromSchema<typeof patchSchema>;

View File

@ -0,0 +1,17 @@
import { FromSchema } from 'json-schema-to-ts';
import { patchSchema } from './patch-schema';
export const patchesSchema = {
$id: '#/components/schemas/patchesSchema',
type: 'array',
items: {
$ref: '#/components/schemas/patchSchema',
},
components: {
schemas: {
patchSchema,
},
},
} as const;
export type PatchesSchema = FromSchema<typeof patchesSchema>;

View File

@ -1,15 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const strategiesResponse: OpenAPIV3.ResponseObject = {
description: 'strategiesResponse',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/strategySchema',
},
},
},
},
};

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const strategyResponse: OpenAPIV3.ResponseObject = {
description: 'strategyResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/featureStrategySchema',
},
},
},
};

View File

@ -1,8 +1,9 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
export const strategySchemaDefinition = {
export const strategySchema = {
$id: '#/components/schemas/strategySchema',
type: 'object',
additionalProperties: false,
required: ['name'],
@ -26,12 +27,12 @@ export const strategySchemaDefinition = {
$ref: '#/components/schemas/parametersSchema',
},
},
'components/schemas': {
constraintSchema,
parametersSchema,
components: {
schemas: {
constraintSchema,
parametersSchema,
},
},
} as const;
export type StrategySchema = CreateSchemaType<typeof strategySchemaDefinition>;
export const strategySchema = createSchemaObject(strategySchemaDefinition);
export type StrategySchema = FromSchema<typeof strategySchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const tagResponse: OpenAPIV3.ResponseObject = {
description: 'tagResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/tagSchema',
},
},
},
};

View File

@ -1,6 +1,7 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
const schema = {
export const tagSchema = {
$id: '#/components/schemas/tagSchema',
type: 'object',
additionalProperties: false,
required: ['value', 'type'],
@ -12,9 +13,7 @@ const schema = {
type: 'string',
},
},
'components/schemas': {},
components: {},
} as const;
export type TagSchema = CreateSchemaType<typeof schema>;
export const tagSchema = createSchemaObject(schema);
export type TagSchema = FromSchema<typeof tagSchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const tagsResponse: OpenAPIV3.ResponseObject = {
description: 'tagsResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/tagsResponseSchema',
},
},
},
};

View File

@ -1,7 +1,8 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { tagSchema } from './tag-schema';
const schema = {
export const tagsSchema = {
$id: '#/components/schemas/tagsSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'tags'],
@ -16,11 +17,11 @@ const schema = {
},
},
},
'components/schemas': {
tagSchema,
components: {
schemas: {
tagSchema,
},
},
} as const;
export type TagsResponseSchema = CreateSchemaType<typeof schema>;
export const tagsResponseSchema = createSchemaObject(schema);
export type TagsSchema = FromSchema<typeof tagsSchema>;

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const updateFeatureRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/updateFeatureSchema',
},
},
},
};

View File

@ -1,7 +1,8 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { constraintSchema } from './constraint-schema';
const schema = {
export const updateFeatureSchema = {
$id: '#/components/schemas/updateFeatureSchema',
type: 'object',
required: ['name'],
properties: {
@ -22,7 +23,7 @@ const schema = {
},
createdAt: {
type: 'string',
format: 'date',
format: 'date-time',
},
impressionData: {
type: 'boolean',
@ -34,11 +35,11 @@ const schema = {
},
},
},
'components/schemas': {
constraintSchema,
components: {
schemas: {
constraintSchema,
},
},
} as const;
export type UpdateFeatureSchema = CreateSchemaType<typeof schema>;
export const updateFeatureSchema = createSchemaObject(schema);
export type UpdateFeatureSchema = FromSchema<typeof updateFeatureSchema>;

View File

@ -1,15 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const updateFeatureVariantsRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/variantSchema',
},
},
},
},
};

View File

@ -1,12 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
export const updateStrategyRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/updateStrategySchema',
},
},
},
};

View File

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

View File

@ -1,7 +1,8 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { FromSchema } from 'json-schema-to-ts';
import { overrideSchema } from './override-schema';
const schema = {
export const variantSchema = {
$id: '#/components/schemas/variantSchema',
type: 'object',
additionalProperties: false,
required: ['name', 'weight', 'weightType', 'stickiness'],
@ -37,11 +38,11 @@ const schema = {
},
},
},
'components/schemas': {
overrideSchema,
components: {
schemas: {
overrideSchema,
},
},
} as const;
export type VariantSchema = CreateSchemaType<typeof schema>;
export const variantSchema = createSchemaObject(schema);
export type VariantSchema = FromSchema<typeof variantSchema>;

View File

@ -0,0 +1,19 @@
import { variantSchema } from './variant-schema';
import { FromSchema } from 'json-schema-to-ts';
import { overrideSchema } from './override-schema';
export const variantsSchema = {
$id: '#/components/schemas/variantsSchema',
type: 'array',
items: {
$ref: '#/components/schemas/variantSchema',
},
components: {
schemas: {
variantSchema,
overrideSchema,
},
},
} as const;
export type VariantsSchema = FromSchema<typeof variantsSchema>;

View File

@ -1,38 +0,0 @@
import { OpenAPIV3 } from 'openapi-types';
import { FromSchema } from 'json-schema-to-ts';
import { DeepMutable } from '../types/mutable';
// Admin paths must have the "admin" tag.
export interface AdminApiOperation
extends Omit<OpenAPIV3.OperationObject, 'tags'> {
tags: ['admin'];
}
// Client paths must have the "client" tag.
export interface ClientApiOperation
extends Omit<OpenAPIV3.OperationObject, 'tags'> {
tags: ['client'];
}
// Create a type from a const schema object.
export type CreateSchemaType<T> = FromSchema<
T,
{
definitionsPath: 'components/schemas';
deserialize: [
{ pattern: { type: 'string'; format: 'date' }; output: Date },
];
}
>;
// Create an OpenAPIV3.SchemaObject from a const schema object.
// Make sure the schema contains an object of refs for type generation.
// Pass an empty 'components/schemas' object if there are no refs in the schema.
export const createSchemaObject = <
T extends { 'components/schemas': { [key: string]: object } },
>(
schema: T,
): DeepMutable<Omit<T, 'components/schemas'>> => {
const { 'components/schemas': schemas, ...rest } = schema;
return rest;
};

View File

@ -0,0 +1,31 @@
import Ajv, { ErrorObject } from 'ajv';
import addFormats from 'ajv-formats';
import { SchemaId, schemas } from './index';
import { omitKeys } from '../util/omit-keys';
interface ISchemaValidationErrors<T> {
schema: SchemaId;
data: T;
errors: ErrorObject[];
}
const ajv = new Ajv({
schemas: Object.values(schemas).map((schema) =>
omitKeys(schema, 'components'),
),
});
addFormats(ajv, ['date-time']);
export const validateSchema = <T>(
schema: SchemaId,
data: T,
): ISchemaValidationErrors<T> | undefined => {
if (!ajv.validate(schema, data)) {
return {
schema,
data: data,
errors: ajv.errors ?? [],
};
}
};

View File

@ -2,21 +2,26 @@ import { Request, Response } from 'express';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types';
import { Logger } from '../../logger';
import Controller from '../controller';
import { extractUsername } from '../../util/extract-user';
import { DELETE_FEATURE, NONE, UPDATE_FEATURE } from '../../types/permissions';
import FeatureToggleService from '../../services/feature-toggle-service';
import { IAuthRequest } from '../unleash-types';
import { featuresResponse } from '../../openapi/spec/features-response';
import { FeaturesSchema } from '../../openapi/spec/features-schema';
import {
featuresSchema,
FeaturesSchema,
} from '../../openapi/spec/features-schema';
import { serializeDates } from '../../types/serialize-dates';
import { OpenApiService } from '../../services/openapi-service';
import { createResponseSchema } from '../../openapi';
export default class ArchiveController extends Controller {
private readonly logger: Logger;
private featureService: FeatureToggleService;
private openApiService: OpenApiService;
constructor(
config: IUnleashConfig,
{
@ -27,6 +32,7 @@ export default class ArchiveController extends Controller {
super(config);
this.logger = config.getLogger('/admin-api/archive.js');
this.featureService = featureToggleServiceV2;
this.openApiService = openApiService;
this.route({
method: 'get',
@ -36,7 +42,7 @@ export default class ArchiveController extends Controller {
middleware: [
openApiService.validPath({
tags: ['admin'],
responses: { 200: featuresResponse },
responses: { 200: createResponseSchema('featuresSchema') },
deprecated: true,
}),
],
@ -50,7 +56,7 @@ export default class ArchiveController extends Controller {
middleware: [
openApiService.validPath({
tags: ['admin'],
responses: { 200: featuresResponse },
responses: { 200: createResponseSchema('featuresSchema') },
deprecated: true,
}),
],
@ -71,11 +77,12 @@ export default class ArchiveController extends Controller {
const features = await this.featureService.getMetadataForAllFeatures(
true,
);
res.json({
version: 2,
features: features,
});
this.openApiService.respondWithValidation(
200,
res,
featuresSchema.$id,
{ version: 2, features: serializeDates(features) },
);
}
async getArchivedFeaturesByProjectId(
@ -88,10 +95,12 @@ export default class ArchiveController extends Controller {
true,
projectId,
);
res.json({
version: 2,
features: features,
});
this.openApiService.respondWithValidation(
200,
res,
featuresSchema.$id,
{ version: 2, features: serializeDates(features) },
);
}
async deleteFeature(

View File

@ -1,8 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Request, Response } from 'express';
import Controller from '../controller';
import { extractUsername } from '../../util/extract-user';
import {
CREATE_FEATURE,
@ -18,20 +16,23 @@ import { IFeatureToggleQuery } from '../../types/model';
import FeatureTagService from '../../services/feature-tag-service';
import { IAuthRequest } from '../unleash-types';
import { DEFAULT_ENV } from '../../util/constants';
import { featuresResponse } from '../../openapi/spec/features-response';
import { FeaturesSchema } from '../../openapi/spec/features-schema';
import { tagsResponse } from '../../openapi/spec/tags-response';
import { tagResponse } from '../../openapi/spec/tag-response';
import { createTagRequest } from '../../openapi/spec/create-tag-request';
import { emptyResponse } from '../../openapi/spec/empty-response';
import {
featuresSchema,
FeaturesSchema,
} from '../../openapi/spec/features-schema';
import { TagSchema } from '../../openapi/spec/tag-schema';
import { TagsResponseSchema } from '../../openapi/spec/tags-response-schema';
import { TagsSchema } from '../../openapi/spec/tags-schema';
import { serializeDates } from '../../types/serialize-dates';
import { OpenApiService } from '../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../openapi';
const version = 1;
class FeatureController extends Controller {
private tagService: FeatureTagService;
private openApiService: OpenApiService;
private service: FeatureToggleService;
constructor(
@ -47,6 +48,7 @@ class FeatureController extends Controller {
) {
super(config);
this.tagService = featureTagService;
this.openApiService = openApiService;
this.service = featureToggleServiceV2;
if (!config.disableLegacyFeaturesApi) {
@ -75,7 +77,7 @@ class FeatureController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'getAllToggles',
responses: { 200: featuresResponse },
responses: { 200: createResponseSchema('featuresSchema') },
deprecated: true,
}),
],
@ -90,7 +92,7 @@ class FeatureController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'validateFeature',
responses: { 200: emptyResponse },
responses: { 200: createResponseSchema('emptySchema') },
}),
],
});
@ -104,7 +106,7 @@ class FeatureController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'listTags',
responses: { 200: tagsResponse },
responses: { 200: createResponseSchema('tagsSchema') },
}),
],
});
@ -118,8 +120,8 @@ class FeatureController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'addTag',
requestBody: createTagRequest,
responses: { 201: tagResponse },
requestBody: createRequestSchema('tagSchema'),
responses: { 201: createResponseSchema('tagSchema') },
}),
],
});
@ -134,7 +136,7 @@ class FeatureController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'removeTag',
responses: { 200: emptyResponse },
responses: { 200: createResponseSchema('emptySchema') },
}),
],
});
@ -175,10 +177,13 @@ class FeatureController extends Controller {
): Promise<void> {
const query = await this.prepQuery(req.query);
const features = await this.service.getFeatureToggles(query);
res.json({
version,
features: features,
});
this.openApiService.respondWithValidation(
200,
res,
featuresSchema.$id,
{ version, features: serializeDates(features) },
);
}
async getToggle(
@ -192,7 +197,7 @@ class FeatureController extends Controller {
async listTags(
req: Request<{ featureName: string }, any, any, any>,
res: Response<TagsResponseSchema>,
res: Response<TagsSchema>,
): Promise<void> {
const tags = await this.tagService.listTags(req.params.featureName);
res.json({ version, tags });

View File

@ -17,27 +17,24 @@ import {
} from '../../../types/permissions';
import { extractUsername } from '../../../util/extract-user';
import { IAuthRequest } from '../../unleash-types';
import { createFeatureRequest } from '../../../openapi/spec/create-feature-request';
import { featureResponse } from '../../../openapi/spec/feature-response';
import { CreateFeatureSchema } from '../../../openapi/spec/create-feature-schema';
import { FeatureSchema } from '../../../openapi/spec/feature-schema';
import { createStrategyRequest } from '../../../openapi/spec/create-strategy-request';
import {
featureSchema,
FeatureSchema,
} from '../../../openapi/spec/feature-schema';
import { StrategySchema } from '../../../openapi/spec/strategy-schema';
import { featuresResponse } from '../../../openapi/spec/features-response';
import { featureEnvironmentResponse } from '../../../openapi/spec/feature-environment-response';
import { strategiesResponse } from '../../../openapi/spec/strategies-response';
import { strategyResponse } from '../../../openapi/spec/strategy-response';
import { emptyResponse } from '../../../openapi/spec/empty-response';
import { updateFeatureRequest } from '../../../openapi/spec/update-feature-request';
import { patchRequest } from '../../../openapi/spec/patch-request';
import { updateStrategyRequest } from '../../../openapi/spec/update-strategy-request';
import { cloneFeatureRequest } from '../../../openapi/spec/clone-feature-request';
import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environment-schema';
import { ParametersSchema } from '../../../openapi/spec/parameters-schema';
import { FeaturesSchema } from '../../../openapi/spec/features-schema';
import { UpdateFeatureSchema } from '../../../openapi/spec/updateFeatureSchema';
import {
featuresSchema,
FeaturesSchema,
} from '../../../openapi/spec/features-schema';
import { UpdateFeatureSchema } from '../../../openapi/spec/update-feature-schema';
import { UpdateStrategySchema } from '../../../openapi/spec/update-strategy-schema';
import { CreateStrategySchema } from '../../../openapi/spec/create-strategy-schema';
import { serializeDates } from '../../../types/serialize-dates';
import { OpenApiService } from '../../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../../openapi';
import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environment-schema';
interface FeatureStrategyParams {
projectId: string;
@ -73,6 +70,8 @@ type ProjectFeaturesServices = Pick<
export default class ProjectFeaturesController extends Controller {
private featureService: FeatureToggleService;
private openApiService: OpenApiService;
private readonly logger: Logger;
constructor(
@ -81,6 +80,7 @@ export default class ProjectFeaturesController extends Controller {
) {
super(config);
this.featureService = featureToggleServiceV2;
this.openApiService = openApiService;
this.logger = config.getLogger('/admin-api/project/features.ts');
this.route({
@ -92,7 +92,9 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'getEnvironment',
responses: { 200: featureEnvironmentResponse },
responses: {
200: createResponseSchema('featureEnvironmentSchema'),
},
}),
],
});
@ -106,7 +108,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'toggleEnvironmentOff',
responses: { 200: featureResponse },
responses: { 200: createResponseSchema('featureSchema') },
}),
],
});
@ -120,7 +122,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'toggleEnvironmentOn',
responses: { 200: featureResponse },
responses: { 200: createResponseSchema('featureSchema') },
}),
],
});
@ -134,7 +136,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'getStrategies',
responses: { 200: strategiesResponse },
responses: { 200: createResponseSchema('strategySchema') },
}),
],
});
@ -148,8 +150,10 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'addStrategy',
requestBody: createStrategyRequest,
responses: { 200: strategyResponse },
requestBody: createRequestSchema('createStrategySchema'),
responses: {
200: createResponseSchema('featureStrategySchema'),
},
}),
],
});
@ -163,7 +167,9 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'getStrategy',
responses: { 200: strategyResponse },
responses: {
200: createResponseSchema('featureStrategySchema'),
},
}),
],
});
@ -177,8 +183,10 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'updateStrategy',
requestBody: updateStrategyRequest,
responses: { 200: strategyResponse },
requestBody: createRequestSchema('updateStrategySchema'),
responses: {
200: createResponseSchema('featureStrategySchema'),
},
}),
],
});
@ -191,8 +199,10 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'patchStrategy',
requestBody: patchRequest,
responses: { 200: strategyResponse },
requestBody: createRequestSchema('patchesSchema'),
responses: {
200: createResponseSchema('featureStrategySchema'),
},
}),
],
});
@ -206,7 +216,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
operationId: 'deleteStrategy',
tags: ['admin'],
responses: { 200: emptyResponse },
responses: { 200: createResponseSchema('emptySchema') },
}),
],
});
@ -220,7 +230,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'getFeatures',
responses: { 200: featuresResponse },
responses: { 200: createResponseSchema('featuresSchema') },
}),
],
});
@ -234,8 +244,8 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'createFeature',
requestBody: createFeatureRequest,
responses: { 200: featureResponse },
requestBody: createRequestSchema('createFeatureSchema'),
responses: { 200: createResponseSchema('featureSchema') },
}),
],
});
@ -250,8 +260,8 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'cloneFeature',
requestBody: cloneFeatureRequest,
responses: { 200: featureResponse },
requestBody: createRequestSchema('cloneFeatureSchema'),
responses: { 200: createResponseSchema('featureSchema') },
}),
],
});
@ -265,7 +275,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
operationId: 'getFeature',
tags: ['admin'],
responses: { 200: featureResponse },
responses: { 200: createResponseSchema('featureSchema') },
}),
],
});
@ -280,8 +290,8 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'updateFeature',
requestBody: updateFeatureRequest,
responses: { 200: featureResponse },
requestBody: createRequestSchema('updateFeatureSchema'),
responses: { 200: createResponseSchema('featureSchema') },
}),
],
});
@ -296,8 +306,8 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'patchFeature',
requestBody: patchRequest,
responses: { 200: featureResponse },
requestBody: createRequestSchema('patchesSchema'),
responses: { 200: createResponseSchema('featureSchema') },
}),
],
});
@ -312,7 +322,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'archiveFeature',
responses: { 200: emptyResponse },
responses: { 200: createResponseSchema('emptySchema') },
}),
],
});
@ -326,15 +336,19 @@ export default class ProjectFeaturesController extends Controller {
const features = await this.featureService.getFeatureOverview(
projectId,
);
res.json({ version: 1, features });
this.openApiService.respondWithValidation(
200,
res,
featuresSchema.$id,
{ version: 2, features: serializeDates(features) },
);
}
async cloneFeature(
req: IAuthRequest<
FeatureParams,
any,
{ name: string; replaceGroupId?: boolean },
any
{ name: string; replaceGroupId?: boolean }
>,
res: Response<FeatureSchema>,
): Promise<void> {
@ -348,7 +362,13 @@ export default class ProjectFeaturesController extends Controller {
replaceGroupId,
userName,
);
res.status(201).json(created);
this.openApiService.respondWithValidation(
201,
res,
featureSchema.$id,
serializeDates(created),
);
}
async createFeature(
@ -364,7 +384,12 @@ export default class ProjectFeaturesController extends Controller {
userName,
);
res.status(201).json(created);
this.openApiService.respondWithValidation(
201,
res,
featureSchema.$id,
serializeDates(created),
);
}
async getFeature(
@ -380,13 +405,12 @@ export default class ProjectFeaturesController extends Controller {
req: IAuthRequest<
{ projectId: string; featureName: string },
any,
UpdateFeatureSchema,
any
UpdateFeatureSchema
>,
res: Response<FeatureSchema>,
): Promise<void> {
const { projectId, featureName } = req.params;
const data = req.body;
const { createdAt, ...data } = req.body;
const userName = extractUsername(req);
const created = await this.featureService.updateFeatureToggle(
projectId,
@ -394,7 +418,13 @@ export default class ProjectFeaturesController extends Controller {
userName,
featureName,
);
res.status(200).json(created);
this.openApiService.respondWithValidation(
200,
res,
featureSchema.$id,
serializeDates(created),
);
}
async patchFeature(
@ -413,7 +443,12 @@ export default class ProjectFeaturesController extends Controller {
extractUsername(req),
req.body,
);
res.status(200).json(updated);
this.openApiService.respondWithValidation(
200,
res,
featureSchema.$id,
serializeDates(updated),
);
}
// TODO: validate projectId
@ -442,7 +477,12 @@ export default class ProjectFeaturesController extends Controller {
environment,
featureName,
);
res.status(200).json(environmentInfo);
this.openApiService.respondWithValidation(
200,
res,
featureSchema.$id,
serializeDates(environmentInfo),
);
}
async toggleEnvironmentOn(

View File

@ -9,10 +9,8 @@ import { NONE, UPDATE_FEATURE_VARIANTS } from '../../../types/permissions';
import { IVariant } from '../../../types/model';
import { extractUsername } from '../../../util/extract-user';
import { IAuthRequest } from '../../unleash-types';
import { featureVariantsResponse } from '../../../openapi/spec/feature-variants-response';
import { patchRequest } from '../../../openapi/spec/patch-request';
import { updateFeatureVariantsRequest } from '../../../openapi/spec/update-feature-variants-request';
import { FeatureVariantsSchema } from '../../../openapi/spec/feature-variants-schema';
import { createRequestSchema, createResponseSchema } from '../../../openapi';
const PREFIX = '/:projectId/features/:featureName/variants';
@ -47,7 +45,9 @@ export default class VariantsController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'getFeatureVariants',
responses: { 200: featureVariantsResponse },
responses: {
200: createResponseSchema('featureVariantsSchema'),
},
}),
],
});
@ -60,8 +60,10 @@ export default class VariantsController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'patchFeatureVariants',
requestBody: patchRequest,
responses: { 200: featureVariantsResponse },
requestBody: createRequestSchema('patchesSchema'),
responses: {
200: createResponseSchema('featureVariantsSchema'),
},
}),
],
});
@ -74,8 +76,10 @@ export default class VariantsController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'overwriteFeatureVariants',
requestBody: updateFeatureVariantsRequest,
responses: { 200: featureVariantsResponse },
requestBody: createRequestSchema('variantsSchema'),
responses: {
200: createResponseSchema('featureVariantsSchema'),
},
}),
],
});
@ -87,7 +91,7 @@ export default class VariantsController extends Controller {
): Promise<void> {
const { featureName } = req.params;
const variants = await this.featureService.getVariants(featureName);
res.status(200).json({ version: '1', variants: variants || [] });
res.status(200).json({ version: 1, variants: variants || [] });
}
async patchVariants(
@ -104,7 +108,7 @@ export default class VariantsController extends Controller {
userName,
);
res.status(200).json({
version: '1',
version: 1,
variants: updatedFeature.variants,
});
}
@ -122,7 +126,7 @@ export default class VariantsController extends Controller {
userName,
);
res.status(200).json({
version: '1',
version: 1,
variants: updatedFeature.variants,
});
}

View File

@ -1,17 +1,27 @@
import openapi, { IExpressOpenApi } from '@unleash/express-openapi';
import { Express, RequestHandler } from 'express';
import { OpenAPIV3 } from 'openapi-types';
import { Express, RequestHandler, Response } from 'express';
import { IUnleashConfig } from '../types/option';
import { createOpenApiSchema } from '../openapi';
import { AdminApiOperation, ClientApiOperation } from '../openapi/types';
import {
AdminApiOperation,
ClientApiOperation,
createOpenApiSchema,
SchemaId,
} from '../openapi';
import { Logger } from '../logger';
import { validateSchema } from '../openapi/validate';
import { omitKeys } from '../util/omit-keys';
export class OpenApiService {
private readonly config: IUnleashConfig;
private readonly logger: Logger;
private readonly api: IExpressOpenApi;
constructor(config: IUnleashConfig) {
this.config = config;
this.logger = config.getLogger('openapi-service.ts');
this.api = openapi(
this.docsPath(),
createOpenApiSchema(config.server?.unleashUrl),
@ -19,35 +29,28 @@ export class OpenApiService {
);
}
// Create request validation middleware for an admin or client path.
validPath(op: AdminApiOperation | ClientApiOperation): RequestHandler {
return this.api.validPath(op);
}
// Serve the OpenAPI JSON at `${baseUriPath}/docs/openapi.json`,
// and the OpenAPI SwaggerUI at `${baseUriPath}/docs/openapi`.
useDocs(app: Express): void {
app.use(this.api);
app.use(this.docsPath(), this.api.swaggerui);
}
// The OpenAPI docs live at `<baseUriPath>/docs/openapi{,.json}`.
docsPath(): string {
const { baseUriPath = '' } = this.config.server ?? {};
return `${baseUriPath}/docs/openapi`;
}
// Add custom schemas to the generated OpenAPI spec.
// Used by unleash-enterprise to add its own schemas.
registerCustomSchemas(schemas: {
[name: string]: OpenAPIV3.SchemaObject;
registerCustomSchemas<T extends object>(schemas: {
[name: string]: { $id: string; components: T };
}): void {
Object.entries(schemas).forEach(([name, schema]) => {
this.api.schema(name, schema);
this.api.schema(name, omitKeys(schema, '$id', 'components'));
});
}
// Catch and format Open API validation errors.
useErrorHandler(app: Express): void {
app.use((err, req, res, next) => {
if (err && err.status && err.validationErrors) {
@ -60,4 +63,19 @@ export class OpenApiService {
}
});
}
respondWithValidation<T>(
status: number,
res: Response<T>,
schema: SchemaId,
data: T,
): void {
const errors = validateSchema(schema, data);
if (errors) {
this.logger.warn('Invalid response:', errors);
}
res.status(status).json(data);
}
}

View File

@ -1,9 +1,9 @@
// Remove readonly modifiers from properties.
export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Recursively remove readonly modifiers from properties.
export type DeepMutable<T> = {
-readonly [P in keyof T]: DeepMutable<T[P]>;
};
// Recursively add readonly modifiers to properties.
export type DeepImmutable<T> = {
readonly [P in keyof T]: DeepImmutable<T[P]>;
};

View File

@ -0,0 +1,39 @@
import { serializeDates } from './serialize-dates';
test('serializeDates primitives', () => {
expect(serializeDates(undefined)).toEqual(undefined);
expect(serializeDates(null)).toEqual(null);
expect(serializeDates(1)).toEqual(1);
expect(serializeDates('a')).toEqual('a');
});
test('serializeDates arrays', () => {
const now = new Date();
const iso = now.toISOString();
expect(serializeDates([])).toEqual([]);
expect(serializeDates([1])).toEqual([1]);
expect(serializeDates(['2'])).toEqual(['2']);
expect(serializeDates([{ a: 1 }])).toEqual([{ a: 1 }]);
expect(serializeDates([{ a: now }])).toEqual([{ a: iso }]);
});
test('serializeDates object', () => {
const now = new Date();
const iso = now.toISOString();
const obj = {
a: 1,
b: '2',
c: now,
d: { e: now },
f: [{ g: now }],
};
expect(serializeDates({})).toEqual({});
expect(serializeDates(obj).a).toEqual(1);
expect(serializeDates(obj).b).toEqual('2');
expect(serializeDates(obj).c).toEqual(iso);
expect(serializeDates(obj).d.e).toEqual(iso);
expect(serializeDates(obj).f[0].g).toEqual(iso);
});

View File

@ -0,0 +1,26 @@
type SerializedDates<T> = T extends Date
? string
: T extends object
? { [P in keyof T]: SerializedDates<T[P]> }
: T;
// Convert Date objects to strings recursively.
export const serializeDates = <T>(obj: T): SerializedDates<T> => {
if (!obj || typeof obj !== 'object') {
return obj as SerializedDates<T>;
}
if (Array.isArray(obj)) {
return obj.map(serializeDates) as unknown as SerializedDates<T>;
}
const entries = Object.entries(obj).map(([k, v]) => {
if (v instanceof Date) {
return [k, v.toJSON()];
} else {
return [k, serializeDates(v)];
}
});
return Object.fromEntries(entries);
};

View File

@ -0,0 +1,18 @@
import { mapValues } from './map-values';
test('mapValues', () => {
expect(
mapValues(
{
a: 1,
b: 2,
c: 3,
},
(x) => x + 1,
),
).toEqual({
a: 2,
b: 3,
c: 4,
});
});

View File

@ -0,0 +1,11 @@
export const mapValues = <T extends object, U>(
object: T,
fn: (value: T[keyof T]) => U,
): Record<keyof T, U> => {
const entries = Object.entries(object).map(([key, value]) => [
key,
fn(value),
]);
return Object.fromEntries(entries);
};

View File

@ -0,0 +1,7 @@
import { omitKeys } from './omit-keys';
test('omitKeys', () => {
expect(omitKeys({ a: 1, b: 2, c: 3 }, 'a', 'b')).toEqual({
c: 3,
});
});

22
src/lib/util/omit-keys.ts Normal file
View File

@ -0,0 +1,22 @@
interface OmitKeys {
<T extends object, K extends [...(keyof T)[]]>(obj: T, ...keys: K): {
[K2 in Exclude<keyof T, K[number]>]: T[K2];
};
}
// https://stackoverflow.com/questions/53966509/typescript-type-safe-omit-function
export const omitKeys: OmitKeys = (obj, ...keys) => {
const ret = {} as {
[K in keyof typeof obj]: typeof obj[K];
};
let key: keyof typeof obj;
for (key in obj) {
if (!keys.includes(key)) {
ret[key] = obj[key];
}
}
return ret;
};

View File

@ -25,7 +25,7 @@ beforeAll(async () => {
app = await setupApp(db.stores);
const createToggle = async (
toggle: FeatureSchema,
toggle: Omit<FeatureSchema, 'createdAt'>,
strategy: Omit<StrategySchema, 'id'> = defaultStrategy,
projectId: string = 'default',
username: string = 'test',

View File

@ -33,7 +33,7 @@ test('Can get variants for a feature', async () => {
.get(`/api/admin/projects/default/features/${featureName}/variants`)
.expect(200)
.expect((res) => {
expect(res.body.version).toBe('1');
expect(res.body.version).toBe(1);
expect(res.body.variants).toHaveLength(1);
expect(res.body.variants[0].name).toBe(variantName);
});
@ -104,7 +104,7 @@ test('Can patch variants for a feature and get a response of new variant', async
.send(patch)
.expect(200)
.expect((res) => {
expect(res.body.version).toBe('1');
expect(res.body.version).toBe(1);
expect(res.body.variants).toHaveLength(1);
expect(res.body.variants[0].name).toBe(expectedVariantName);
});
@ -148,7 +148,7 @@ test('Can add variant for a feature', async () => {
await app.request
.get(`/api/admin/projects/default/features/${featureName}/variants`)
.expect((res) => {
expect(res.body.version).toBe('1');
expect(res.body.version).toBe(1);
expect(res.body.variants).toHaveLength(2);
expect(
res.body.variants.find((x) => x.name === expectedVariantName),
@ -192,7 +192,7 @@ test('Can remove variant for a feature', async () => {
await app.request
.get(`/api/admin/projects/default/features/${featureName}/variants`)
.expect((res) => {
expect(res.body.version).toBe('1');
expect(res.body.version).toBe(1);
expect(res.body.variants).toHaveLength(0);
});
});

View File

@ -157,9 +157,8 @@ Object {
],
"type": "object",
},
"emptyResponseSchema": Object {
"description": "OK",
"type": "object",
"emptySchema": Object {
"description": "emptySchema",
},
"featureEnvironmentSchema": Object {
"additionalProperties": false,
@ -196,7 +195,7 @@ Object {
"type": "boolean",
},
"createdAt": Object {
"format": "date",
"format": "date-time",
"nullable": true,
"type": "string",
},
@ -208,7 +207,7 @@ Object {
},
"environments": Object {
"items": Object {
"$ref": "#/components/schemas/featureEnvironmentSchema",
"type": "object",
},
"type": "array",
},
@ -216,7 +215,7 @@ Object {
"type": "boolean",
},
"lastSeenAt": Object {
"format": "date",
"format": "date-time",
"nullable": true,
"type": "string",
},
@ -260,7 +259,7 @@ Object {
"type": "array",
},
"createdAt": Object {
"format": "date",
"format": "date-time",
"nullable": true,
"type": "string",
},
@ -299,6 +298,25 @@ Object {
],
"type": "object",
},
"featureVariantsSchema": Object {
"additionalProperties": false,
"properties": Object {
"variants": Object {
"items": Object {
"$ref": "#/components/schemas/variantSchema",
},
"type": "array",
},
"version": Object {
"type": "integer",
},
},
"required": Array [
"version",
"variants",
],
"type": "object",
},
"featuresSchema": Object {
"additionalProperties": false,
"properties": Object {
@ -343,7 +361,7 @@ Object {
},
"type": "object",
},
"patchOperationSchema": Object {
"patchSchema": Object {
"properties": Object {
"from": Object {
"type": "string",
@ -369,6 +387,12 @@ Object {
],
"type": "object",
},
"patchesSchema": Object {
"items": Object {
"$ref": "#/components/schemas/patchSchema",
},
"type": "array",
},
"strategySchema": Object {
"additionalProperties": false,
"properties": Object {
@ -412,7 +436,7 @@ Object {
],
"type": "object",
},
"tagsResponseSchema": Object {
"tagsSchema": Object {
"additionalProperties": false,
"properties": Object {
"tags": Object {
@ -443,7 +467,7 @@ Object {
"type": "array",
},
"createdAt": Object {
"format": "date",
"format": "date-time",
"type": "string",
},
"description": Object {
@ -537,6 +561,12 @@ Object {
],
"type": "object",
},
"variantsSchema": Object {
"items": Object {
"$ref": "#/components/schemas/variantSchema",
},
"type": "array",
},
},
"securitySchemes": Object {
"apiKey": Object {
@ -563,7 +593,7 @@ Object {
},
},
},
"description": "featuresResponse",
"description": "featuresSchema",
},
},
"tags": Array [
@ -593,7 +623,7 @@ Object {
},
},
},
"description": "featuresResponse",
"description": "featuresSchema",
},
},
"tags": Array [
@ -614,7 +644,7 @@ Object {
},
},
},
"description": "featuresResponse",
"description": "featuresSchema",
},
},
"tags": Array [
@ -630,11 +660,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptyResponseSchema",
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptyResponse",
"description": "emptySchema",
},
},
"tags": Array [
@ -660,11 +690,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/tagsResponseSchema",
"$ref": "#/components/schemas/tagsSchema",
},
},
},
"description": "tagsResponse",
"description": "tagsSchema",
},
},
"tags": Array [
@ -691,6 +721,7 @@ Object {
},
},
},
"description": "tagSchema",
"required": true,
},
"responses": Object {
@ -702,7 +733,7 @@ Object {
},
},
},
"description": "tagResponse",
"description": "tagSchema",
},
},
"tags": Array [
@ -744,11 +775,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptyResponseSchema",
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptyResponse",
"description": "emptySchema",
},
},
"tags": Array [
@ -778,7 +809,7 @@ Object {
},
},
},
"description": "featuresResponse",
"description": "featuresSchema",
},
},
"tags": Array [
@ -805,6 +836,7 @@ Object {
},
},
},
"description": "createFeatureSchema",
"required": true,
},
"responses": Object {
@ -816,7 +848,7 @@ Object {
},
},
},
"description": "featureResponse",
"description": "featureSchema",
},
},
"tags": Array [
@ -850,11 +882,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptyResponseSchema",
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptyResponse",
"description": "emptySchema",
},
},
"tags": Array [
@ -890,7 +922,7 @@ Object {
},
},
},
"description": "featureResponse",
"description": "featureSchema",
},
},
"tags": Array [
@ -921,13 +953,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"items": Object {
"$ref": "#/components/schemas/patchOperationSchema",
},
"type": "array",
"$ref": "#/components/schemas/patchesSchema",
},
},
},
"description": "patchesSchema",
"required": true,
},
"responses": Object {
@ -939,7 +969,7 @@ Object {
},
},
},
"description": "featureResponse",
"description": "featureSchema",
},
},
"tags": Array [
@ -974,6 +1004,7 @@ Object {
},
},
},
"description": "updateFeatureSchema",
"required": true,
},
"responses": Object {
@ -985,7 +1016,7 @@ Object {
},
},
},
"description": "featureResponse",
"description": "featureSchema",
},
},
"tags": Array [
@ -1022,6 +1053,7 @@ Object {
},
},
},
"description": "cloneFeatureSchema",
"required": true,
},
"responses": Object {
@ -1033,7 +1065,7 @@ Object {
},
},
},
"description": "featureResponse",
"description": "featureSchema",
},
},
"tags": Array [
@ -1079,7 +1111,7 @@ Object {
},
},
},
"description": "featureEnvironmentResponse",
"description": "featureEnvironmentSchema",
},
},
"tags": Array [
@ -1125,7 +1157,7 @@ Object {
},
},
},
"description": "featureResponse",
"description": "featureSchema",
},
},
"tags": Array [
@ -1171,7 +1203,7 @@ Object {
},
},
},
"description": "featureResponse",
"description": "featureSchema",
},
},
"tags": Array [
@ -1213,14 +1245,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"items": Object {
"$ref": "#/components/schemas/strategySchema",
},
"type": "array",
"$ref": "#/components/schemas/strategySchema",
},
},
},
"description": "strategiesResponse",
"description": "strategySchema",
},
},
"tags": Array [
@ -1263,6 +1292,7 @@ Object {
},
},
},
"description": "createStrategySchema",
"required": true,
},
"responses": Object {
@ -1274,7 +1304,7 @@ Object {
},
},
},
"description": "strategyResponse",
"description": "featureStrategySchema",
},
},
"tags": Array [
@ -1324,11 +1354,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptyResponseSchema",
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptyResponse",
"description": "emptySchema",
},
},
"tags": Array [
@ -1380,7 +1410,7 @@ Object {
},
},
},
"description": "strategyResponse",
"description": "featureStrategySchema",
},
},
"tags": Array [
@ -1427,13 +1457,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"items": Object {
"$ref": "#/components/schemas/patchOperationSchema",
},
"type": "array",
"$ref": "#/components/schemas/patchesSchema",
},
},
},
"description": "patchesSchema",
"required": true,
},
"responses": Object {
@ -1445,7 +1473,7 @@ Object {
},
},
},
"description": "strategyResponse",
"description": "featureStrategySchema",
},
},
"tags": Array [
@ -1496,6 +1524,7 @@ Object {
},
},
},
"description": "updateStrategySchema",
"required": true,
},
"responses": Object {
@ -1507,7 +1536,7 @@ Object {
},
},
},
"description": "strategyResponse",
"description": "featureStrategySchema",
},
},
"tags": Array [
@ -1545,7 +1574,7 @@ Object {
},
},
},
"description": "featureVariantResponse",
"description": "featureVariantsSchema",
},
},
"tags": Array [
@ -1576,13 +1605,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"items": Object {
"$ref": "#/components/schemas/patchOperationSchema",
},
"type": "array",
"$ref": "#/components/schemas/patchesSchema",
},
},
},
"description": "patchesSchema",
"required": true,
},
"responses": Object {
@ -1594,7 +1621,7 @@ Object {
},
},
},
"description": "featureVariantResponse",
"description": "featureVariantsSchema",
},
},
"tags": Array [
@ -1625,13 +1652,11 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
"items": Object {
"$ref": "#/components/schemas/variantSchema",
},
"type": "array",
"$ref": "#/components/schemas/variantsSchema",
},
},
},
"description": "variantsSchema",
"required": true,
},
"responses": Object {
@ -1643,7 +1668,7 @@ Object {
},
},
},
"description": "featureVariantResponse",
"description": "featureVariantsSchema",
},
},
"tags": Array [

View File

@ -1356,6 +1356,13 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
dependencies:
ajv "^8.0.0"
ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
@ -1366,6 +1373,16 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.0.0, ajv@^8.11.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
version "4.3.2"
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz"
@ -4789,10 +4806,10 @@ json-parse-even-better-errors@^2.3.0:
resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json-schema-to-ts@^2.0.0:
version "2.3.0"
resolved "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-2.3.0.tgz"
integrity sha512-qBE94lvOfcVmedIgHkKNhDxTG1gPZW8pPIUpRtbPee54jGF2RZnyEOpDdowCU219sXCJ8SDVEMUCG4oMFw7pgA==
json-schema-to-ts@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/json-schema-to-ts/-/json-schema-to-ts-2.5.3.tgz#10a1ad27a3cc6117ae9c652cc583a9e0ed10f0c8"
integrity sha512-2vABI+1IZNkChaPfLu7PG192ZY9gvRY00RbuN3VGlNNZkvYRpIECdBZPBVMe41r3wX0sl9emjRyhHT3gTm7HIg==
dependencies:
"@types/json-schema" "^7.0.9"
ts-algebra "^1.1.1"
@ -4803,6 +4820,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-schema-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
json-schema@0.2.3, json-schema@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
@ -6453,6 +6475,11 @@ require-directory@^2.1.1:
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz"