diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 4c1503e74c..3084503d78 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -52,6 +52,11 @@ import { versionSchema } from './spec/version-schema'; import { applicationSchema } from './spec/application-schema'; import { applicationsSchema } from './spec/applications-schema'; import { tagWithVersionSchema } from './spec/tag-with-version-schema'; +import { featureStrategySegmentSchema } from './spec/feature-strategy-segment-schema'; +import { segmentSchema } from './spec/segment-schema'; +import { stateSchema } from './spec/state-schema'; +import { featureTagSchema } from './spec/feature-tag-schema'; +import { exportParametersSchema } from './spec/export-parameters-schema'; // All schemas in `openapi/spec` should be listed here. export const schemas = { @@ -68,9 +73,12 @@ export const schemas = { createStrategySchema, environmentSchema, environmentsSchema, + exportParametersSchema, featureEnvironmentSchema, featureSchema, featureStrategySchema, + featureStrategySegmentSchema, + featureTagSchema, featureTypeSchema, featureTypesSchema, featureVariantsSchema, @@ -88,8 +96,10 @@ export const schemas = { projectEnvironmentSchema, projectSchema, projectsSchema, + segmentSchema, sortOrderSchema, splashSchema, + stateSchema, strategySchema, tagSchema, tagWithVersionSchema, diff --git a/src/lib/openapi/spec/export-parameters-schema.ts b/src/lib/openapi/spec/export-parameters-schema.ts new file mode 100644 index 0000000000..0dceaada20 --- /dev/null +++ b/src/lib/openapi/spec/export-parameters-schema.ts @@ -0,0 +1,32 @@ +import { FromSchema } from 'json-schema-to-ts'; + +export const exportParametersSchema = { + $id: '#/components/schemas/exportParametersSchema', + type: 'object', + properties: { + format: { + type: 'string', + }, + download: { + type: 'boolean', + }, + strategies: { + type: 'boolean', + }, + featureToggles: { + type: 'boolean', + }, + projects: { + type: 'boolean', + }, + tags: { + type: 'boolean', + }, + environments: { + type: 'boolean', + }, + }, + components: {}, +} as const; + +export type ExportParametersSchema = FromSchema; diff --git a/src/lib/openapi/spec/feature-strategy-segment-schema.ts b/src/lib/openapi/spec/feature-strategy-segment-schema.ts new file mode 100644 index 0000000000..ae473c1a0f --- /dev/null +++ b/src/lib/openapi/spec/feature-strategy-segment-schema.ts @@ -0,0 +1,21 @@ +import { FromSchema } from 'json-schema-to-ts'; + +export const featureStrategySegmentSchema = { + $id: '#/components/schemas/featureStrategySegmentSchema', + type: 'object', + additionalProperties: false, + required: ['segmentId', 'featureStrategyId'], + properties: { + segmentId: { + type: 'integer', + }, + featureStrategyId: { + type: 'string', + }, + }, + components: {}, +} as const; + +export type FeatureStrategySegmentSchema = FromSchema< + typeof featureStrategySegmentSchema +>; diff --git a/src/lib/openapi/spec/feature-tag-schema.ts b/src/lib/openapi/spec/feature-tag-schema.ts new file mode 100644 index 0000000000..1b4e0f924b --- /dev/null +++ b/src/lib/openapi/spec/feature-tag-schema.ts @@ -0,0 +1,28 @@ +import { FromSchema } from 'json-schema-to-ts'; + +export const featureTagSchema = { + $id: '#/components/schemas/featureTagSchema', + type: 'object', + additionalProperties: false, + required: ['featureName', 'tagValue'], + properties: { + featureName: { + type: 'string', + }, + tagType: { + type: 'string', + }, + tagValue: { + type: 'string', + }, + type: { + type: 'string', + }, + value: { + type: 'string', + }, + }, + components: {}, +} as const; + +export type FeatureTagSchema = FromSchema; diff --git a/src/lib/openapi/spec/segment-schema.ts b/src/lib/openapi/spec/segment-schema.ts new file mode 100644 index 0000000000..98311e1131 --- /dev/null +++ b/src/lib/openapi/spec/segment-schema.ts @@ -0,0 +1,30 @@ +import { FromSchema } from 'json-schema-to-ts'; +import { constraintSchema } from './constraint-schema'; + +export const segmentSchema = { + $id: '#/components/schemas/segmentSchema', + type: 'object', + additionalProperties: false, + required: ['name', 'constraints'], + properties: { + name: { + type: 'string', + }, + description: { + type: 'string', + }, + constraints: { + type: 'array', + items: { + $ref: '#/components/schemas/constraintSchema', + }, + }, + }, + components: { + schemas: { + constraintSchema, + }, + }, +} as const; + +export type SegmentSchema = FromSchema; diff --git a/src/lib/openapi/spec/state-schema.ts b/src/lib/openapi/spec/state-schema.ts new file mode 100644 index 0000000000..5292271be9 --- /dev/null +++ b/src/lib/openapi/spec/state-schema.ts @@ -0,0 +1,107 @@ +import { FromSchema } from 'json-schema-to-ts'; +import { featureSchema } from './feature-schema'; +import { strategySchema } from './strategy-schema'; +import { tagSchema } from './tag-schema'; +import { tagTypeSchema } from './tag-type-schema'; +import { featureTagSchema } from './feature-tag-schema'; +import { projectSchema } from './project-schema'; +import { featureStrategySchema } from './feature-strategy-schema'; +import { featureEnvironmentSchema } from './feature-environment-schema'; +import { environmentSchema } from './environment-schema'; +import { segmentSchema } from './segment-schema'; +import { featureStrategySegmentSchema } from './feature-strategy-segment-schema'; + +export const stateSchema = { + $id: '#/components/schemas/stateSchema', + type: 'object', + additionalProperties: true, + required: ['version'], + properties: { + version: { + type: 'integer', + }, + features: { + type: 'array', + items: { + $ref: '#/components/schemas/featureSchema', + }, + }, + strategies: { + type: 'array', + items: { + $ref: '#/components/schemas/strategySchema', + }, + }, + tags: { + type: 'array', + items: { + $ref: '#/components/schemas/tagSchema', + }, + }, + tagTypes: { + type: 'array', + items: { + $ref: '#/components/schemas/tagTypeSchema', + }, + }, + featureTags: { + type: 'array', + items: { + $ref: '#/components/schemas/featureTagSchema', + }, + }, + projects: { + type: 'array', + items: { + $ref: '#/components/schemas/projectSchema', + }, + }, + featureStrategies: { + type: 'array', + items: { + $ref: '#/components/schemas/featureStrategySchema', + }, + }, + featureEnvironments: { + type: 'array', + items: { + $ref: '#/components/schemas/featureEnvironmentSchema', + }, + }, + environments: { + type: 'array', + items: { + $ref: '#/components/schemas/environmentSchema', + }, + }, + segments: { + type: 'array', + items: { + $ref: '#/components/schemas/segmentSchema', + }, + }, + featureStrategySegments: { + type: 'array', + items: { + $ref: '#/components/schemas/featureStrategySegmentSchema', + }, + }, + }, + components: { + schemas: { + featureSchema, + strategySchema, + tagSchema, + tagTypeSchema, + featureTagSchema, + projectSchema, + featureStrategySchema, + featureEnvironmentSchema, + environmentSchema, + segmentSchema, + featureStrategySegmentSchema, + }, + }, +} as const; + +export type StateSchema = FromSchema; diff --git a/src/lib/routes/admin-api/state.ts b/src/lib/routes/admin-api/state.ts index 27dcd804f4..36b264f95a 100644 --- a/src/lib/routes/admin-api/state.ts +++ b/src/lib/routes/admin-api/state.ts @@ -11,6 +11,10 @@ import { IUnleashServices } from '../../types/services'; import { Logger } from '../../logger'; import StateService from '../../services/state-service'; import { IAuthRequest } from '../unleash-types'; +import { OpenApiService } from '../../services/openapi-service'; +import { createRequestSchema, createResponseSchema } from '../../openapi'; +import { emptyResponse } from '../../openapi/spec/empty-response'; +import { ExportParametersSchema } from '../../openapi/spec/export-parameters-schema'; const upload = multer({ limits: { fileSize: 5242880 } }); const paramToBool = (param, def) => { @@ -28,16 +32,56 @@ class StateController extends Controller { private stateService: StateService; + private openApiService: OpenApiService; + constructor( config: IUnleashConfig, - { stateService }: Pick, + { + stateService, + openApiService, + }: Pick, ) { super(config); this.logger = config.getLogger('/admin-api/state.ts'); this.stateService = stateService; + this.openApiService = openApiService; this.fileupload('/import', upload.single('file'), this.import, ADMIN); - this.post('/import', this.import, ADMIN); - this.get('/export', this.export, ADMIN); + this.route({ + method: 'post', + path: '/import', + permission: ADMIN, + handler: this.import, + middleware: [ + this.openApiService.validPath({ + tags: ['admin'], + operationId: 'import', + responses: { + 202: emptyResponse, + }, + requestBody: createRequestSchema('stateSchema'), + }), + ], + }); + this.route({ + method: 'get', + path: '/export', + permission: ADMIN, + handler: this.export, + middleware: [ + this.openApiService.validPath({ + tags: ['admin'], + operationId: 'export', + responses: { + 200: createResponseSchema('stateSchema'), + }, + parameters: [ + { + $ref: '#/components/schema/exportParametersSchema', + }, + ], + }), + ], + }); } async import(req: IAuthRequest, res: Response): Promise { @@ -68,7 +112,10 @@ class StateController extends Controller { res.sendStatus(202); } - async export(req: Request, res: Response): Promise { + async export( + req: Request, + res: Response, + ): Promise { const { format } = req.query; const downloadFile = paramToBool(req.query.download, false); diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 9c6dd8f606..906abc5dbb 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -384,6 +384,32 @@ Object { ], "type": "object", }, + "exportParametersSchema": Object { + "properties": Object { + "download": Object { + "type": "boolean", + }, + "environments": Object { + "type": "boolean", + }, + "featureToggles": Object { + "type": "boolean", + }, + "format": Object { + "type": "string", + }, + "projects": Object { + "type": "boolean", + }, + "strategies": Object { + "type": "boolean", + }, + "tags": Object { + "type": "boolean", + }, + }, + "type": "object", + }, "featureEnvironmentSchema": Object { "additionalProperties": false, "properties": Object { @@ -522,6 +548,47 @@ Object { ], "type": "object", }, + "featureStrategySegmentSchema": Object { + "additionalProperties": false, + "properties": Object { + "featureStrategyId": Object { + "type": "string", + }, + "segmentId": Object { + "type": "integer", + }, + }, + "required": Array [ + "segmentId", + "featureStrategyId", + ], + "type": "object", + }, + "featureTagSchema": Object { + "additionalProperties": false, + "properties": Object { + "featureName": Object { + "type": "string", + }, + "tagType": Object { + "type": "string", + }, + "tagValue": Object { + "type": "string", + }, + "type": Object { + "type": "string", + }, + "value": Object { + "type": "string", + }, + }, + "required": Array [ + "featureName", + "tagValue", + ], + "type": "object", + }, "featureTypeSchema": Object { "additionalProperties": false, "properties": Object { @@ -889,6 +956,28 @@ Object { ], "type": "object", }, + "segmentSchema": Object { + "additionalProperties": false, + "properties": Object { + "constraints": Object { + "items": Object { + "$ref": "#/components/schemas/constraintSchema", + }, + "type": "array", + }, + "description": Object { + "type": "string", + }, + "name": Object { + "type": "string", + }, + }, + "required": Array [ + "name", + "constraints", + ], + "type": "object", + }, "sortOrderSchema": Object { "additionalProperties": Object { "type": "number", @@ -915,6 +1004,84 @@ Object { ], "type": "object", }, + "stateSchema": Object { + "additionalProperties": true, + "properties": Object { + "environments": Object { + "items": Object { + "$ref": "#/components/schemas/environmentSchema", + }, + "type": "array", + }, + "featureEnvironments": Object { + "items": Object { + "$ref": "#/components/schemas/featureEnvironmentSchema", + }, + "type": "array", + }, + "featureStrategies": Object { + "items": Object { + "$ref": "#/components/schemas/featureStrategySchema", + }, + "type": "array", + }, + "featureStrategySegments": Object { + "items": Object { + "$ref": "#/components/schemas/featureStrategySegmentSchema", + }, + "type": "array", + }, + "featureTags": Object { + "items": Object { + "$ref": "#/components/schemas/featureTagSchema", + }, + "type": "array", + }, + "features": Object { + "items": Object { + "$ref": "#/components/schemas/featureSchema", + }, + "type": "array", + }, + "projects": Object { + "items": Object { + "$ref": "#/components/schemas/projectSchema", + }, + "type": "array", + }, + "segments": Object { + "items": Object { + "$ref": "#/components/schemas/segmentSchema", + }, + "type": "array", + }, + "strategies": Object { + "items": Object { + "$ref": "#/components/schemas/strategySchema", + }, + "type": "array", + }, + "tagTypes": Object { + "items": Object { + "$ref": "#/components/schemas/tagTypeSchema", + }, + "type": "array", + }, + "tags": Object { + "items": Object { + "$ref": "#/components/schemas/tagSchema", + }, + "type": "array", + }, + "version": Object { + "type": "integer", + }, + }, + "required": Array [ + "version", + ], + "type": "object", + }, "strategySchema": Object { "additionalProperties": false, "properties": Object { @@ -3208,6 +3375,55 @@ Object { ], }, }, + "/api/admin/state/export": Object { + "get": Object { + "operationId": "export", + "parameters": Array [ + Object { + "$ref": "#/components/schema/exportParametersSchema", + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/stateSchema", + }, + }, + }, + "description": "stateSchema", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, + "/api/admin/state/import": Object { + "post": Object { + "operationId": "import", + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/stateSchema", + }, + }, + }, + "description": "stateSchema", + "required": true, + }, + "responses": Object { + "202": Object { + "description": "emptyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, "/api/admin/tag-types": Object { "get": Object { "operationId": "getTagTypes",