diff --git a/src/lib/db/feature-type-store.ts b/src/lib/db/feature-type-store.ts index b7c8119985..10c696d6bd 100644 --- a/src/lib/db/feature-type-store.ts +++ b/src/lib/db/feature-type-store.ts @@ -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 { + async get(id: string): Promise { 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 { + async delete(key: string): Promise { await this.db(TABLE).where({ id: key }).del(); } @@ -59,7 +59,7 @@ class FeatureTypeStore implements IFeatureTypeStore { destroy(): void {} - async exists(key: number): Promise { + async exists(key: string): Promise { const result = await this.db.raw( `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, [key], diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index f6bd2bcbed..855f034eff 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -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, diff --git a/src/lib/openapi/spec/__snapshots__/feature-type-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/feature-type-schema.test.ts.snap new file mode 100644 index 0000000000..1fd9d01904 --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/feature-type-schema.test.ts.snap @@ -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", +} +`; diff --git a/src/lib/openapi/spec/feature-type-schema.test.ts b/src/lib/openapi/spec/feature-type-schema.test.ts new file mode 100644 index 0000000000..57219dc6b4 --- /dev/null +++ b/src/lib/openapi/spec/feature-type-schema.test.ts @@ -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(); +}); diff --git a/src/lib/openapi/spec/feature-type-schema.ts b/src/lib/openapi/spec/feature-type-schema.ts new file mode 100644 index 0000000000..c7a8fed031 --- /dev/null +++ b/src/lib/openapi/spec/feature-type-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/feature-types-schema.ts b/src/lib/openapi/spec/feature-types-schema.ts new file mode 100644 index 0000000000..33ce80980a --- /dev/null +++ b/src/lib/openapi/spec/feature-types-schema.ts @@ -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; diff --git a/src/lib/routes/admin-api/email.ts b/src/lib/routes/admin-api/email.ts index 8fb851ea05..5a9822900c 100644 --- a/src/lib/routes/admin-api/email.ts +++ b/src/lib/routes/admin-api/email.ts @@ -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, diff --git a/src/lib/routes/admin-api/feature-type.ts b/src/lib/routes/admin-api/feature-type.ts index cb7a16f210..4aa847b336 100644 --- a/src/lib/routes/admin-api/feature-type.ts +++ b/src/lib/routes/admin-api/feature-type.ts @@ -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, + { + featureTypeService, + openApiService, + }: Pick, ) { 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 { - const types = await this.featureTypeService.getAll(); - res.json({ version, types }); + async getAllFeatureTypes( + req: Request, + res: Response, + ): Promise { + res.json({ + version, + types: await this.featureTypeService.getAll(), + }); } } - -module.exports = FeatureTypeController; diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 79e7b415d0..69493f7de8 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -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'; diff --git a/src/lib/services/email-service.ts b/src/lib/services/email-service.ts index 022f60840d..f2307a50ec 100644 --- a/src/lib/services/email-service.ts +++ b/src/lib/services/email-service.ts @@ -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 { try { const template = this.resolveTemplate(templateName, format); diff --git a/src/lib/types/stores/feature-type-store.ts b/src/lib/types/stores/feature-type-store.ts index 4776eb1fff..dfc34e43e3 100644 --- a/src/lib/types/stores/feature-type-store.ts +++ b/src/lib/types/stores/feature-type-store.ts @@ -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 { +export interface IFeatureTypeStore extends Store { getByName(name: string): Promise; } diff --git a/src/test/e2e/api/admin/feature-type.test.ts b/src/test/e2e/api/admin/feature-type.test.ts index beebe404a3..4ceb529a9a 100644 --- a/src/test/e2e/api/admin/feature-type.test.ts +++ b/src/test/e2e/api/admin/feature-type.test.ts @@ -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(); }); }); 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 59d5f6051a..c7ffc6ef38 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 @@ -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, diff --git a/src/test/e2e/stores/feature-type-store.e2e.test.ts b/src/test/e2e/stores/feature-type-store.e2e.test.ts index dceebde442..e7da2e08d3 100644 --- a/src/test/e2e/stores/feature-type-store.e2e.test.ts +++ b/src/test/e2e/stores/feature-type-store.e2e.test.ts @@ -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 () => { diff --git a/src/test/fixtures/fake-feature-type-store.ts b/src/test/fixtures/fake-feature-type-store.ts index d9c95ac175..028f700cb3 100644 --- a/src/test/fixtures/fake-feature-type-store.ts +++ b/src/test/fixtures/fake-feature-type-store.ts @@ -7,7 +7,7 @@ import NotFoundError from '../../lib/error/notfound-error'; export default class FakeFeatureTypeStore implements IFeatureTypeStore { featureTypes: IFeatureType[] = []; - async delete(key: number): Promise { + async delete(key: string): Promise { 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 { + async exists(key: string): Promise { return this.featureTypes.some((fT) => fT.id === key); } - async get(key: number): Promise { + async get(key: string): Promise { const type = this.featureTypes.find((fT) => fT.id === key); if (type) { return type;