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:
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