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:
parent
dadbc3addc
commit
13ef025fab
@ -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",
|
||||
|
@ -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'),
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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",
|
||||
}
|
||||
`;
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const cloneFeatureRequest: OpenAPIV3.RequestBodyObject = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/cloneFeatureSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const createFeatureRequest: OpenAPIV3.RequestBodyObject = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/createFeatureSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const createStrategyRequest: OpenAPIV3.RequestBodyObject = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/createStrategySchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const createTagRequest: OpenAPIV3.RequestBodyObject = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/tagSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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);
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const emptyResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'emptyResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/emptyResponseSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
9
src/lib/openapi/spec/empty-schema.ts
Normal file
9
src/lib/openapi/spec/empty-schema.ts
Normal 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>;
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const featureEnvironmentResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'featureEnvironmentResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/featureEnvironmentSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
>;
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const featureResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'featureResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/featureSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
64
src/lib/openapi/spec/feature-schema.test.ts
Normal file
64
src/lib/openapi/spec/feature-schema.test.ts
Normal 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();
|
||||
});
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const featureVariantsResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'featureVariantResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/featureVariantsSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const featuresResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'featuresResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/featuresSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
13
src/lib/openapi/spec/features-schema.test.ts
Normal file
13
src/lib/openapi/spec/features-schema.test.ts
Normal 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();
|
||||
});
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
17
src/lib/openapi/spec/patches-schema.ts
Normal file
17
src/lib/openapi/spec/patches-schema.ts
Normal 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>;
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const strategyResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'strategyResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/featureStrategySchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const tagResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'tagResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/tagSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const tagsResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'tagsResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/tagsResponseSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const updateFeatureRequest: OpenAPIV3.RequestBodyObject = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/updateFeatureSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const updateStrategyRequest: OpenAPIV3.RequestBodyObject = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/updateStrategySchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
19
src/lib/openapi/spec/variants-schema.ts
Normal file
19
src/lib/openapi/spec/variants-schema.ts
Normal 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>;
|
@ -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;
|
||||
};
|
31
src/lib/openapi/validate.ts
Normal file
31
src/lib/openapi/validate.ts
Normal 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 ?? [],
|
||||
};
|
||||
}
|
||||
};
|
@ -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(
|
||||
|
@ -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 });
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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]>;
|
||||
};
|
||||
|
39
src/lib/types/serialize-dates.test.ts
Normal file
39
src/lib/types/serialize-dates.test.ts
Normal 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);
|
||||
});
|
26
src/lib/types/serialize-dates.ts
Normal file
26
src/lib/types/serialize-dates.ts
Normal 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);
|
||||
};
|
18
src/lib/util/map-values.test.ts
Normal file
18
src/lib/util/map-values.test.ts
Normal 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,
|
||||
});
|
||||
});
|
11
src/lib/util/map-values.ts
Normal file
11
src/lib/util/map-values.ts
Normal 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);
|
||||
};
|
7
src/lib/util/omit-keys.test.ts
Normal file
7
src/lib/util/omit-keys.test.ts
Normal 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
22
src/lib/util/omit-keys.ts
Normal 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;
|
||||
};
|
@ -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',
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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 [
|
||||
|
35
yarn.lock
35
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user