From 32399291e0e8997a975b2f4f7562593d18946f5a Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 21 Jun 2022 09:12:40 +0200 Subject: [PATCH] task: add OpenApi spec to metrics route (#1725) * task: add OpenApi spec to metrics route --- src/lib/openapi/index.ts | 4 + src/lib/openapi/spec/application-schema.ts | 40 +++++ src/lib/openapi/spec/applications-schema.ts | 22 +++ src/lib/routes/admin-api/metrics.test.ts | 2 +- src/lib/routes/admin-api/metrics.ts | 109 ++++++++++--- src/lib/services/client-metrics/models.ts | 6 +- .../__snapshots__/openapi.e2e.test.ts.snap | 150 ++++++++++++++++++ 7 files changed, 309 insertions(+), 24 deletions(-) create mode 100644 src/lib/openapi/spec/application-schema.ts create mode 100644 src/lib/openapi/spec/applications-schema.ts diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index d31b73e55b..4c1503e74c 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -49,12 +49,16 @@ import { validateTagTypeSchema } from './spec/validate-tag-type-schema'; import { variantSchema } from './spec/variant-schema'; import { variantsSchema } from './spec/variants-schema'; 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'; // All schemas in `openapi/spec` should be listed here. export const schemas = { apiTokenSchema, apiTokensSchema, + applicationSchema, + applicationsSchema, cloneFeatureSchema, constraintSchema, contextFieldSchema, diff --git a/src/lib/openapi/spec/application-schema.ts b/src/lib/openapi/spec/application-schema.ts new file mode 100644 index 0000000000..db4f50a122 --- /dev/null +++ b/src/lib/openapi/spec/application-schema.ts @@ -0,0 +1,40 @@ +import { FromSchema } from 'json-schema-to-ts'; + +export const applicationSchema = { + $id: '#/components/schemas/applicationSchema', + type: 'object', + additionalProperties: false, + required: ['appName'], + properties: { + appName: { + type: 'string', + }, + sdkVersion: { + type: 'string', + }, + strategies: { + type: 'array', + items: { + type: 'string', + }, + }, + description: { + type: 'string', + }, + url: { + type: 'string', + }, + color: { + type: 'string', + }, + icon: { + type: 'string', + }, + announced: { + type: 'boolean', + }, + }, + components: {}, +} as const; + +export type ApplicationSchema = FromSchema; diff --git a/src/lib/openapi/spec/applications-schema.ts b/src/lib/openapi/spec/applications-schema.ts new file mode 100644 index 0000000000..efda2f935a --- /dev/null +++ b/src/lib/openapi/spec/applications-schema.ts @@ -0,0 +1,22 @@ +import { applicationSchema } from './application-schema'; +import { FromSchema } from 'json-schema-to-ts'; + +export const applicationsSchema = { + $id: '#/components/schemas/applicationsSchema', + type: 'object', + properties: { + applications: { + type: 'array', + items: { + $ref: '#/components/schemas/applicationSchema', + }, + }, + }, + components: { + schemas: { + applicationSchema, + }, + }, +} as const; + +export type ApplicationsSchema = FromSchema; diff --git a/src/lib/routes/admin-api/metrics.test.ts b/src/lib/routes/admin-api/metrics.test.ts index eff34c4af7..27063b6283 100644 --- a/src/lib/routes/admin-api/metrics.test.ts +++ b/src/lib/routes/admin-api/metrics.test.ts @@ -84,7 +84,7 @@ test('should store application', () => { .expect(202); }); -test('should store application details wihtout strategies', () => { +test('should store application details without strategies', () => { expect.assertions(0); const appName = '123!23'; diff --git a/src/lib/routes/admin-api/metrics.ts b/src/lib/routes/admin-api/metrics.ts index 4d7c959912..28b32497b9 100644 --- a/src/lib/routes/admin-api/metrics.ts +++ b/src/lib/routes/admin-api/metrics.ts @@ -1,10 +1,14 @@ import { Request, Response } from 'express'; import Controller from '../controller'; -import { UPDATE_APPLICATION } from '../../types/permissions'; +import { NONE, UPDATE_APPLICATION } from '../../types/permissions'; import { IUnleashConfig } from '../../types/option'; import { IUnleashServices } from '../../types/services'; import { Logger } from '../../logger'; import ClientInstanceService from '../../services/client-metrics/instance-service'; +import { emptyResponse } from '../../openapi/spec/empty-response'; +import { createRequestSchema, createResponseSchema } from '../../openapi'; +import { ApplicationSchema } from '../../openapi/spec/application-schema'; +import { ApplicationsSchema } from '../../openapi/spec/applications-schema'; class MetricsController extends Controller { private logger: Logger; @@ -15,7 +19,8 @@ class MetricsController extends Controller { config: IUnleashConfig, { clientInstanceService, - }: Pick, + openApiService, + }: Pick, ) { super(config); this.logger = config.getLogger('/admin-api/metrics.ts'); @@ -28,19 +33,68 @@ class MetricsController extends Controller { this.get('/feature-toggles', this.deprecated); this.get('/feature-toggles/:name', this.deprecated); - // in use - this.post( - '/applications/:appName', - this.createApplication, - UPDATE_APPLICATION, - ); - this.delete( - '/applications/:appName', - this.deleteApplication, - UPDATE_APPLICATION, - ); - this.get('/applications/', this.getApplications); - this.get('/applications/:appName', this.getApplication); + this.route({ + method: 'post', + path: '/applications/:appName', + handler: this.createApplication, + permission: UPDATE_APPLICATION, + middleware: [ + openApiService.validPath({ + tags: ['admin'], + operationId: 'createApplication', + responses: { + 202: emptyResponse, + }, + requestBody: createRequestSchema('applicationSchema'), + }), + ], + }); + this.route({ + method: 'delete', + path: '/applications/:appName', + handler: this.deleteApplication, + permission: UPDATE_APPLICATION, + acceptAnyContentType: true, + middleware: [ + openApiService.validPath({ + tags: ['admin'], + operationId: 'deleteApplication', + responses: { + 200: emptyResponse, + }, + }), + ], + }); + this.route({ + method: 'get', + path: '/applications', + handler: this.getApplications, + permission: NONE, + middleware: [ + openApiService.validPath({ + tags: ['admin'], + operationId: 'getApplications', + responses: { + 200: createResponseSchema('applicationsSchema'), + }, + }), + ], + }); + this.route({ + method: 'get', + path: '/applications/:appName', + handler: this.getApplication, + permission: NONE, + middleware: [ + openApiService.validPath({ + tags: ['admin'], + operationId: 'getApplication', + responses: { + 200: createResponseSchema('applicationSchema'), + }, + }), + ], + }); } async deprecated(req: Request, res: Response): Promise { @@ -51,20 +105,32 @@ class MetricsController extends Controller { }); } - async deleteApplication(req: Request, res: Response): Promise { + async deleteApplication( + req: Request<{ appName: string }>, + res: Response, + ): Promise { const { appName } = req.params; await this.clientInstanceService.deleteApplication(appName); res.status(200).end(); } - async createApplication(req: Request, res: Response): Promise { - const input = { ...req.body, appName: req.params.appName }; + async createApplication( + req: Request<{ appName: string }, unknown, ApplicationSchema>, + res: Response, + ): Promise { + const input = { + ...req.body, + appName: req.params.appName, + }; await this.clientInstanceService.createApplication(input); res.status(202).end(); } - async getApplications(req: Request, res: Response): Promise { + async getApplications( + req: Request, + res: Response, + ): Promise { const query = req.query.strategyName ? { strategyName: req.query.strategyName as string } : {}; @@ -74,7 +140,10 @@ class MetricsController extends Controller { res.json({ applications }); } - async getApplication(req: Request, res: Response): Promise { + async getApplication( + req: Request, + res: Response, + ): Promise { const { appName } = req.params; const appDetails = await this.clientInstanceService.getApplication( diff --git a/src/lib/services/client-metrics/models.ts b/src/lib/services/client-metrics/models.ts index bff026260c..854a05e71e 100644 --- a/src/lib/services/client-metrics/models.ts +++ b/src/lib/services/client-metrics/models.ts @@ -22,8 +22,8 @@ export interface IApplication { url?: string; color?: string; icon?: string; - createdAt: Date; + createdAt?: Date; instances?: IClientInstance[]; - seenToggles: Record; - links: Record; + seenToggles?: Record; + links?: Record; } 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 bcf71a9826..402ea2d255 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 @@ -113,6 +113,53 @@ Object { ], "type": "object", }, + "applicationSchema": Object { + "additionalProperties": false, + "properties": Object { + "announced": Object { + "type": "boolean", + }, + "appName": Object { + "type": "string", + }, + "color": Object { + "type": "string", + }, + "description": Object { + "type": "string", + }, + "icon": Object { + "type": "string", + }, + "sdkVersion": Object { + "type": "string", + }, + "strategies": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + "url": Object { + "type": "string", + }, + }, + "required": Array [ + "appName", + ], + "type": "object", + }, + "applicationsSchema": Object { + "properties": Object { + "applications": Object { + "items": Object { + "$ref": "#/components/schemas/applicationSchema", + }, + "type": "array", + }, + }, + "type": "object", + }, "cloneFeatureSchema": Object { "properties": Object { "name": Object { @@ -2011,6 +2058,109 @@ Object { ], }, }, + "/api/admin/metrics/applications": Object { + "get": Object { + "operationId": "getApplications", + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/applicationsSchema", + }, + }, + }, + "description": "applicationsSchema", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, + "/api/admin/metrics/applications/{appName}": Object { + "delete": Object { + "operationId": "deleteApplication", + "parameters": Array [ + Object { + "in": "path", + "name": "appName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "description": "emptyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + "get": Object { + "operationId": "getApplication", + "parameters": Array [ + Object { + "in": "path", + "name": "appName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/applicationSchema", + }, + }, + }, + "description": "applicationSchema", + }, + }, + "tags": Array [ + "admin", + ], + }, + "post": Object { + "operationId": "createApplication", + "parameters": Array [ + Object { + "in": "path", + "name": "appName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/applicationSchema", + }, + }, + }, + "description": "applicationSchema", + "required": true, + }, + "responses": Object { + "202": Object { + "description": "emptyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, "/api/admin/projects": Object { "get": Object { "operationId": "getProjects",