mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: add OpenAPI schema to feature types controller (#1684)
* refactor: fix feature types id type * refactor: fix error-hiding Controller imports * refactor: add OpenAPI schema to feature types controller
This commit is contained in:
		
							parent
							
								
									3d84668ba2
								
							
						
					
					
						commit
						138300ab22
					
				| @ -9,7 +9,7 @@ const COLUMNS = ['id', 'name', 'description', 'lifetime_days']; | ||||
| const TABLE = 'feature_types'; | ||||
| 
 | ||||
| interface IFeatureTypeRow { | ||||
|     id: number; | ||||
|     id: string; | ||||
|     name: string; | ||||
|     description: string; | ||||
|     lifetime_days: number; | ||||
| @ -39,7 +39,7 @@ class FeatureTypeStore implements IFeatureTypeStore { | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     async get(id: number): Promise<IFeatureType | undefined> { | ||||
|     async get(id: string): Promise<IFeatureType | undefined> { | ||||
|         const row = await this.db(TABLE).where({ id }).first(); | ||||
|         return this.rowToFeatureType(row); | ||||
|     } | ||||
| @ -49,7 +49,7 @@ class FeatureTypeStore implements IFeatureTypeStore { | ||||
|         return this.rowToFeatureType(row); | ||||
|     } | ||||
| 
 | ||||
|     async delete(key: number): Promise<void> { | ||||
|     async delete(key: string): Promise<void> { | ||||
|         await this.db(TABLE).where({ id: key }).del(); | ||||
|     } | ||||
| 
 | ||||
| @ -59,7 +59,7 @@ class FeatureTypeStore implements IFeatureTypeStore { | ||||
| 
 | ||||
|     destroy(): void {} | ||||
| 
 | ||||
|     async exists(key: number): Promise<boolean> { | ||||
|     async exists(key: string): Promise<boolean> { | ||||
|         const result = await this.db.raw( | ||||
|             `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, | ||||
|             [key], | ||||
|  | ||||
| @ -8,6 +8,8 @@ import { environmentSchema } from './spec/environment-schema'; | ||||
| import { featureEnvironmentSchema } from './spec/feature-environment-schema'; | ||||
| import { featureSchema } from './spec/feature-schema'; | ||||
| import { featureStrategySchema } from './spec/feature-strategy-schema'; | ||||
| import { featureTypeSchema } from './spec/feature-type-schema'; | ||||
| import { featureTypesSchema } from './spec/feature-types-schema'; | ||||
| import { featureVariantsSchema } from './spec/feature-variants-schema'; | ||||
| import { featuresSchema } from './spec/features-schema'; | ||||
| import { healthOverviewSchema } from './spec/health-overview-schema'; | ||||
| @ -59,6 +61,8 @@ export const schemas = { | ||||
|     featureEnvironmentSchema, | ||||
|     featureSchema, | ||||
|     featureStrategySchema, | ||||
|     featureTypeSchema, | ||||
|     featureTypesSchema, | ||||
|     featureVariantsSchema, | ||||
|     featuresSchema, | ||||
|     healthOverviewSchema, | ||||
|  | ||||
| @ -0,0 +1,19 @@ | ||||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
| 
 | ||||
| exports[`featureTypeSchema empty 1`] = ` | ||||
| Object { | ||||
|   "data": Object {}, | ||||
|   "errors": Array [ | ||||
|     Object { | ||||
|       "instancePath": "", | ||||
|       "keyword": "required", | ||||
|       "message": "must have required property 'id'", | ||||
|       "params": Object { | ||||
|         "missingProperty": "id", | ||||
|       }, | ||||
|       "schemaPath": "#/required", | ||||
|     }, | ||||
|   ], | ||||
|   "schema": "#/components/schemas/featureTypeSchema", | ||||
| } | ||||
| `; | ||||
							
								
								
									
										21
									
								
								src/lib/openapi/spec/feature-type-schema.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/lib/openapi/spec/feature-type-schema.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| import { validateSchema } from '../validate'; | ||||
| import { FeatureTypeSchema } from './feature-type-schema'; | ||||
| 
 | ||||
| test('featureTypeSchema', () => { | ||||
|     const data: FeatureTypeSchema = { | ||||
|         description: '', | ||||
|         id: '', | ||||
|         name: '', | ||||
|         lifetimeDays: 0, | ||||
|     }; | ||||
| 
 | ||||
|     expect( | ||||
|         validateSchema('#/components/schemas/featureTypeSchema', data), | ||||
|     ).toBeUndefined(); | ||||
| }); | ||||
| 
 | ||||
| test('featureTypeSchema empty', () => { | ||||
|     expect( | ||||
|         validateSchema('#/components/schemas/featureTypeSchema', {}), | ||||
|     ).toMatchSnapshot(); | ||||
| }); | ||||
							
								
								
									
										26
									
								
								src/lib/openapi/spec/feature-type-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/lib/openapi/spec/feature-type-schema.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| import { FromSchema } from 'json-schema-to-ts'; | ||||
| 
 | ||||
| export const featureTypeSchema = { | ||||
|     $id: '#/components/schemas/featureTypeSchema', | ||||
|     type: 'object', | ||||
|     additionalProperties: false, | ||||
|     required: ['id', 'name', 'description', 'lifetimeDays'], | ||||
|     properties: { | ||||
|         id: { | ||||
|             type: 'string', | ||||
|         }, | ||||
|         name: { | ||||
|             type: 'string', | ||||
|         }, | ||||
|         description: { | ||||
|             type: 'string', | ||||
|         }, | ||||
|         lifetimeDays: { | ||||
|             type: 'number', | ||||
|             nullable: true, | ||||
|         }, | ||||
|     }, | ||||
|     components: {}, | ||||
| } as const; | ||||
| 
 | ||||
| export type FeatureTypeSchema = FromSchema<typeof featureTypeSchema>; | ||||
							
								
								
									
										27
									
								
								src/lib/openapi/spec/feature-types-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/lib/openapi/spec/feature-types-schema.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| import { FromSchema } from 'json-schema-to-ts'; | ||||
| import { featureTypeSchema } from './feature-type-schema'; | ||||
| 
 | ||||
| export const featureTypesSchema = { | ||||
|     $id: '#/components/schemas/featureTypesSchema', | ||||
|     type: 'object', | ||||
|     additionalProperties: false, | ||||
|     required: ['version', 'types'], | ||||
|     properties: { | ||||
|         version: { | ||||
|             type: 'integer', | ||||
|         }, | ||||
|         types: { | ||||
|             type: 'array', | ||||
|             items: { | ||||
|                 $ref: '#/components/schemas/featureTypeSchema', | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     components: { | ||||
|         schemas: { | ||||
|             featureTypeSchema, | ||||
|         }, | ||||
|     }, | ||||
| } as const; | ||||
| 
 | ||||
| export type FeatureTypesSchema = FromSchema<typeof featureTypesSchema>; | ||||
| @ -1,12 +1,16 @@ | ||||
| import { ADMIN } from '../../types/permissions'; | ||||
| import { TemplateFormat } from '../../services/email-service'; | ||||
| import { EmailService, TemplateFormat } from '../../services/email-service'; | ||||
| import { IUnleashConfig } from '../../types/option'; | ||||
| import { IUnleashServices } from '../../types/services'; | ||||
| import { Request, Response } from 'express'; | ||||
| 
 | ||||
| const Controller = require('../controller'); | ||||
| import Controller from '../controller'; | ||||
| import { Logger } from '../../logger'; | ||||
| 
 | ||||
| export default class EmailController extends Controller { | ||||
|     private emailService: EmailService; | ||||
| 
 | ||||
|     private logger: Logger; | ||||
| 
 | ||||
|     constructor( | ||||
|         config: IUnleashConfig, | ||||
|         { emailService }: Pick<IUnleashServices, 'emailService'>, | ||||
|  | ||||
| @ -3,31 +3,57 @@ import { IUnleashServices } from '../../types/services'; | ||||
| import FeatureTypeService from '../../services/feature-type-service'; | ||||
| import { Logger } from '../../logger'; | ||||
| import { IUnleashConfig } from '../../types/option'; | ||||
| 
 | ||||
| const Controller = require('../controller'); | ||||
| import { OpenApiService } from '../../services/openapi-service'; | ||||
| import { NONE } from '../../types/permissions'; | ||||
| import { FeatureTypesSchema } from '../../openapi/spec/feature-types-schema'; | ||||
| import { createResponseSchema } from '../../openapi'; | ||||
| import Controller from '../controller'; | ||||
| 
 | ||||
| const version = 1; | ||||
| 
 | ||||
| export default class FeatureTypeController extends Controller { | ||||
| export class FeatureTypeController extends Controller { | ||||
|     private featureTypeService: FeatureTypeService; | ||||
| 
 | ||||
|     private openApiService: OpenApiService; | ||||
| 
 | ||||
|     private logger: Logger; | ||||
| 
 | ||||
|     constructor( | ||||
|         config: IUnleashConfig, | ||||
|         { featureTypeService }: Pick<IUnleashServices, 'featureTypeService'>, | ||||
|         { | ||||
|             featureTypeService, | ||||
|             openApiService, | ||||
|         }: Pick<IUnleashServices, 'featureTypeService' | 'openApiService'>, | ||||
|     ) { | ||||
|         super(config); | ||||
|         this.featureTypeService = featureTypeService; | ||||
|         this.openApiService = openApiService; | ||||
|         this.logger = config.getLogger('/admin-api/feature-type.js'); | ||||
| 
 | ||||
|         this.get('/', this.getAllFeatureTypes); | ||||
|         this.route({ | ||||
|             method: 'get', | ||||
|             path: '', | ||||
|             handler: this.getAllFeatureTypes, | ||||
|             permission: NONE, | ||||
|             middleware: [ | ||||
|                 openApiService.validPath({ | ||||
|                     tags: ['admin'], | ||||
|                     operationId: 'getAllFeatureTypes', | ||||
|                     responses: { | ||||
|                         200: createResponseSchema('featureTypesSchema'), | ||||
|                     }, | ||||
|                 }), | ||||
|             ], | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     async getAllFeatureTypes(req: Request, res: Response): Promise<void> { | ||||
|         const types = await this.featureTypeService.getAll(); | ||||
|         res.json({ version, types }); | ||||
|     async getAllFeatureTypes( | ||||
|         req: Request, | ||||
|         res: Response<FeatureTypesSchema>, | ||||
|     ): Promise<void> { | ||||
|         res.json({ | ||||
|             version, | ||||
|             types: await this.featureTypeService.getAll(), | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = FeatureTypeController; | ||||
|  | ||||
| @ -3,7 +3,7 @@ import Controller from '../controller'; | ||||
| import { IUnleashServices } from '../../types/services'; | ||||
| import { IUnleashConfig } from '../../types/option'; | ||||
| import FeatureController from './feature'; | ||||
| import FeatureTypeController from './feature-type'; | ||||
| import { FeatureTypeController } from './feature-type'; | ||||
| import ArchiveController from './archive'; | ||||
| import StrategyController from './strategy'; | ||||
| import EventController from './event'; | ||||
|  | ||||
| @ -189,10 +189,10 @@ export class EmailService { | ||||
|         return this.mailer !== undefined; | ||||
|     } | ||||
| 
 | ||||
|     private async compileTemplate( | ||||
|     async compileTemplate( | ||||
|         templateName: string, | ||||
|         format: TemplateFormat, | ||||
|         context: any, | ||||
|         context: unknown, | ||||
|     ): Promise<string> { | ||||
|         try { | ||||
|             const template = this.resolveTemplate(templateName, format); | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| import { Store } from './store'; | ||||
| 
 | ||||
| export interface IFeatureType { | ||||
|     id: number; | ||||
|     id: string; | ||||
|     name: string; | ||||
|     description: string; | ||||
|     lifetimeDays: number; | ||||
|     lifetimeDays: number | null; | ||||
| } | ||||
| 
 | ||||
| export interface IFeatureTypeStore extends Store<IFeatureType, number> { | ||||
| export interface IFeatureTypeStore extends Store<IFeatureType, string> { | ||||
|     getByName(name: string): Promise<IFeatureType>; | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| import dbInit from '../../helpers/database-init'; | ||||
| import getLogger from '../../../fixtures/no-logger'; | ||||
| import { setupApp } from '../../helpers/test-helper'; | ||||
| import { validateSchema } from '../../../../lib/openapi/validate'; | ||||
| import { featureTypesSchema } from '../../../../lib/openapi/spec/feature-types-schema'; | ||||
| 
 | ||||
| let app; | ||||
| let db; | ||||
| @ -22,9 +24,11 @@ test('Should get all defined feature types', async () => { | ||||
|         .expect('Content-Type', /json/) | ||||
|         .expect((res) => { | ||||
|             const { version, types } = res.body; | ||||
| 
 | ||||
|             expect(version).toBe(1); | ||||
|             expect(types.length).toBe(5); | ||||
|             expect(types[0].name).toBe('Release'); | ||||
|             expect( | ||||
|                 validateSchema(featureTypesSchema.$id, res.body), | ||||
|             ).toBeUndefined(); | ||||
|         }); | ||||
| }); | ||||
|  | ||||
| @ -321,6 +321,50 @@ Object { | ||||
|         ], | ||||
|         "type": "object", | ||||
|       }, | ||||
|       "featureTypeSchema": Object { | ||||
|         "additionalProperties": false, | ||||
|         "properties": Object { | ||||
|           "description": Object { | ||||
|             "type": "string", | ||||
|           }, | ||||
|           "id": Object { | ||||
|             "type": "string", | ||||
|           }, | ||||
|           "lifetimeDays": Object { | ||||
|             "nullable": true, | ||||
|             "type": "number", | ||||
|           }, | ||||
|           "name": Object { | ||||
|             "type": "string", | ||||
|           }, | ||||
|         }, | ||||
|         "required": Array [ | ||||
|           "id", | ||||
|           "name", | ||||
|           "description", | ||||
|           "lifetimeDays", | ||||
|         ], | ||||
|         "type": "object", | ||||
|       }, | ||||
|       "featureTypesSchema": Object { | ||||
|         "additionalProperties": false, | ||||
|         "properties": Object { | ||||
|           "types": Object { | ||||
|             "items": Object { | ||||
|               "$ref": "#/components/schemas/featureTypeSchema", | ||||
|             }, | ||||
|             "type": "array", | ||||
|           }, | ||||
|           "version": Object { | ||||
|             "type": "integer", | ||||
|           }, | ||||
|         }, | ||||
|         "required": Array [ | ||||
|           "version", | ||||
|           "types", | ||||
|         ], | ||||
|         "type": "object", | ||||
|       }, | ||||
|       "featureVariantsSchema": Object { | ||||
|         "additionalProperties": false, | ||||
|         "properties": Object { | ||||
| @ -1014,6 +1058,26 @@ Object { | ||||
|         ], | ||||
|       }, | ||||
|     }, | ||||
|     "/api/admin/feature-types": Object { | ||||
|       "get": Object { | ||||
|         "operationId": "getAllFeatureTypes", | ||||
|         "responses": Object { | ||||
|           "200": Object { | ||||
|             "content": Object { | ||||
|               "application/json": Object { | ||||
|                 "schema": Object { | ||||
|                   "$ref": "#/components/schemas/featureTypesSchema", | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|             "description": "featureTypesSchema", | ||||
|           }, | ||||
|         }, | ||||
|         "tags": Array [ | ||||
|           "admin", | ||||
|         ], | ||||
|       }, | ||||
|     }, | ||||
|     "/api/admin/features": Object { | ||||
|       "get": Object { | ||||
|         "deprecated": true, | ||||
|  | ||||
| @ -28,8 +28,8 @@ test('should be possible to get by name', async () => { | ||||
| }); | ||||
| 
 | ||||
| test('should be possible to get by id', async () => { | ||||
|     const type = await featureTypeStore.exists(0); | ||||
|     expect(type).toBeDefined(); | ||||
|     expect(await featureTypeStore.exists('unknown')).toEqual(false); | ||||
|     expect(await featureTypeStore.exists('operational')).toEqual(true); | ||||
| }); | ||||
| 
 | ||||
| test('should be possible to delete by id', async () => { | ||||
|  | ||||
							
								
								
									
										6
									
								
								src/test/fixtures/fake-feature-type-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/test/fixtures/fake-feature-type-store.ts
									
									
									
									
										vendored
									
									
								
							| @ -7,7 +7,7 @@ import NotFoundError from '../../lib/error/notfound-error'; | ||||
| export default class FakeFeatureTypeStore implements IFeatureTypeStore { | ||||
|     featureTypes: IFeatureType[] = []; | ||||
| 
 | ||||
|     async delete(key: number): Promise<void> { | ||||
|     async delete(key: string): Promise<void> { | ||||
|         this.featureTypes.splice( | ||||
|             this.featureTypes.findIndex((type) => type.id === key), | ||||
|             1, | ||||
| @ -20,11 +20,11 @@ export default class FakeFeatureTypeStore implements IFeatureTypeStore { | ||||
| 
 | ||||
|     destroy(): void {} | ||||
| 
 | ||||
|     async exists(key: number): Promise<boolean> { | ||||
|     async exists(key: string): Promise<boolean> { | ||||
|         return this.featureTypes.some((fT) => fT.id === key); | ||||
|     } | ||||
| 
 | ||||
|     async get(key: number): Promise<IFeatureType> { | ||||
|     async get(key: string): Promise<IFeatureType> { | ||||
|         const type = this.featureTypes.find((fT) => fT.id === key); | ||||
|         if (type) { | ||||
|             return type; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user