1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +02: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:
olav 2022-06-09 13:17:13 +02:00 committed by GitHub
parent 3d84668ba2
commit 138300ab22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 224 additions and 29 deletions

View File

@ -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],

View File

@ -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,

View File

@ -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",
}
`;

View 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();
});

View 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>;

View 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>;

View File

@ -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'>,

View File

@ -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;

View File

@ -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';

View File

@ -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);

View File

@ -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>;
}

View File

@ -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();
});
});

View File

@ -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,

View File

@ -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 () => {

View File

@ -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;