1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-11-01 19:07:38 +01:00
unleash.unleash/src/lib/routes/admin-api/state.ts
Thomas Heartman 6afc0a6954
fix: fix broken OpenAPI spec (#1846)
* Wip: fix openapi spec

* Feat: add openapi enforcer for enforcing the generated schema

* Chore: Allow the example keyword in params

* Feat: add validator tests and fix some errors

* Use @apidevtools/swagger-parser for schema validation

* Wip: refactor tests for updated schema name

* Feat: update request params creation method

* Feat: add query params to state

* Refactor: move mapping test into separate function

* Refactor: rename request-parameters -> query-parameters

* Refactor: expose only finished query parameters

* Wip: fixup param types

* Refactor: remove unused types

* Chore: rename and cleanup

* Chore: cleanup

* Fix: Update snapshot

* Fix: use ?? Instead of paramToBool to get defaults

* Wip: generate query param object type from openapi params list

* Wip: use generated types for export query params

* Revert "Fix: use ?? Instead of paramToBool to get defaults"

This reverts commit 842567500b.

Because we accept bools, strings, and numbers, this is the only way to
do it.

* Chore: update and pin json-schema-to-ts

* Fix: use `&` to merge types

* Update snapshot

* Chore: rename export-parameters-schema -> export-query-parameters

When it ends in `schema`, the tests expect it to be included in the
openapi index file.
2022-07-28 09:19:58 +02:00

155 lines
5.2 KiB
TypeScript

import * as mime from 'mime';
import YAML from 'js-yaml';
import multer from 'multer';
import { format as formatDate } from 'date-fns';
import { Request, Response } from 'express';
import Controller from '../controller';
import { ADMIN } from '../../types/permissions';
import { extractUsername } from '../../util/extract-user';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import { Logger } from '../../logger';
import StateService from '../../services/state-service';
import { IAuthRequest } from '../unleash-types';
import { OpenApiService } from '../../services/openapi-service';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema';
import {
exportQueryParameters,
ExportQueryParameters,
} from '../../openapi/spec/export-query-parameters';
import { emptyResponse } from '../../openapi/util/standard-responses';
import { OpenAPIV3 } from 'openapi-types';
const upload = multer({ limits: { fileSize: 5242880 } });
const paramToBool = (param, def) => {
if (param === null || param === undefined) {
return def;
}
const nu = Number.parseInt(param, 10);
if (Number.isNaN(nu)) {
return param.toLowerCase() === 'true';
}
return Boolean(nu);
};
class StateController extends Controller {
private logger: Logger;
private stateService: StateService;
private openApiService: OpenApiService;
constructor(
config: IUnleashConfig,
{
stateService,
openApiService,
}: Pick<IUnleashServices, 'stateService' | 'openApiService'>,
) {
super(config);
this.logger = config.getLogger('/admin-api/state.ts');
this.stateService = stateService;
this.openApiService = openApiService;
this.fileupload('/import', upload.single('file'), this.import, ADMIN);
this.route({
method: 'post',
path: '/import',
permission: ADMIN,
handler: this.import,
middleware: [
this.openApiService.validPath({
tags: ['admin'],
operationId: 'import',
responses: {
202: emptyResponse,
},
requestBody: createRequestSchema('stateSchema'),
}),
],
});
this.route({
method: 'get',
path: '/export',
permission: ADMIN,
handler: this.export,
middleware: [
this.openApiService.validPath({
tags: ['admin'],
operationId: 'export',
responses: {
200: createResponseSchema('stateSchema'),
},
parameters:
exportQueryParameters as unknown as OpenAPIV3.ParameterObject[],
}),
],
});
}
async import(req: IAuthRequest, res: Response): Promise<void> {
const userName = extractUsername(req);
const { drop, keep } = req.query;
// TODO: Should override request type so file is a type on request
let data;
// @ts-expect-error
if (req.file) {
// @ts-expect-error
if (mime.getType(req.file.originalname) === 'text/yaml') {
// @ts-expect-error
data = YAML.load(req.file.buffer);
} else {
// @ts-expect-error
data = JSON.parse(req.file.buffer);
}
} else {
data = req.body;
}
await this.stateService.import({
data,
userName,
dropBeforeImport: paramToBool(drop, false),
keepExisting: paramToBool(keep, true),
});
res.sendStatus(202);
}
async export(
req: Request<unknown, unknown, unknown, ExportQueryParameters>,
res: Response,
): Promise<void> {
const { format } = req.query;
const downloadFile = paramToBool(req.query.download, false);
const includeStrategies = paramToBool(req.query.strategies, true);
const includeFeatureToggles = paramToBool(
req.query.featureToggles,
true,
);
const includeProjects = paramToBool(req.query.projects, true);
const includeTags = paramToBool(req.query.tags, true);
const includeEnvironments = paramToBool(req.query.environments, true);
const data = await this.stateService.export({
includeStrategies,
includeFeatureToggles,
includeProjects,
includeTags,
includeEnvironments,
});
const timestamp = formatDate(Date.now(), 'yyyy-MM-dd_HH-mm-ss');
if (format === 'yaml') {
if (downloadFile) {
res.attachment(`export-${timestamp}.yml`);
}
res.type('yaml').send(YAML.dump(data, { skipInvalid: true }));
} else {
if (downloadFile) {
res.attachment(`export-${timestamp}.json`);
}
res.json(data);
}
}
}
export default StateController;