Merge pull request #17875 from sennyeya/validation
feat(openapi-tooling): Add support for request validation.
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-openapi-utils": "workspace:^",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
@@ -45,7 +46,6 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-openapi-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^1.0.0",
|
||||
|
||||
@@ -16,23 +16,6 @@
|
||||
|
||||
import { InputError } from '@backstage/errors';
|
||||
|
||||
export const parseIntegerParam = (
|
||||
str: unknown,
|
||||
ctx: string,
|
||||
): number | undefined => {
|
||||
if (str === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof str !== 'string') {
|
||||
throw new InputError(`invalid ${ctx}, must be a string`);
|
||||
}
|
||||
const parsed = parseInt(str, 10);
|
||||
if (!Number.isInteger(parsed) || String(parsed) !== str) {
|
||||
throw new InputError(`invalid ${ctx}, not an integer`);
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const parseOrderByParam = <T extends readonly string[]>(
|
||||
str: unknown,
|
||||
allowedFields: T,
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { createValidatedOpenApiRouter } from '@backstage/backend-openapi-utils';
|
||||
|
||||
export default {
|
||||
export const spec = {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: '@backstage/plugin-todo-backend',
|
||||
@@ -149,7 +150,8 @@ export default {
|
||||
{
|
||||
name: 'entity',
|
||||
in: 'query',
|
||||
required: true,
|
||||
required: false,
|
||||
allowReserved: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
@@ -206,3 +208,6 @@ export default {
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
export const createOpenApiRouter = async (
|
||||
options?: Parameters<typeof createValidatedOpenApiRouter>['1'],
|
||||
) => createValidatedOpenApiRouter<typeof spec>(spec, options);
|
||||
|
||||
@@ -95,7 +95,8 @@ paths:
|
||||
parameters:
|
||||
- name: entity
|
||||
in: query
|
||||
required: true
|
||||
required: false
|
||||
allowReserved: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 1
|
||||
|
||||
@@ -19,11 +19,7 @@ import request from 'supertest';
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
|
||||
import { createRouter } from './router';
|
||||
import {
|
||||
parseFilterParam,
|
||||
parseIntegerParam,
|
||||
parseOrderByParam,
|
||||
} from '../lib/utils';
|
||||
import { parseFilterParam, parseOrderByParam } from '../lib/utils';
|
||||
import { TodoService } from './types';
|
||||
|
||||
const mockListBody = {
|
||||
@@ -136,58 +132,44 @@ describe('createRouter', () => {
|
||||
});
|
||||
|
||||
it('rejects invalid queries', async () => {
|
||||
await expect(
|
||||
request(app).get('/v1/todos?entity=k:n&entity=k:n'),
|
||||
).resolves.toMatchObject(
|
||||
matchErrorResponse(400, 'InputError', 'entity query must be a string'),
|
||||
);
|
||||
request(app)
|
||||
.get('/v1/todos?entity=k:n&entity=k:n')
|
||||
.expect(400)
|
||||
.expect(
|
||||
matchErrorResponse(
|
||||
400,
|
||||
'InputError',
|
||||
'entity query must be a string',
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
request(app).get('/v1/todos?entity=:n'),
|
||||
).resolves.toMatchObject(
|
||||
matchErrorResponse(
|
||||
400,
|
||||
'InputError',
|
||||
'Invalid entity ref, TypeError: Entity reference ":n" was not on the form [<kind>:][<namespace>/]<name>',
|
||||
),
|
||||
);
|
||||
request(app)
|
||||
.get('/v1/todos?entity=:n')
|
||||
.expect(400)
|
||||
.expect(
|
||||
matchErrorResponse(
|
||||
400,
|
||||
'InputError',
|
||||
'Invalid entity ref, TypeError: Entity reference ":n" was not on the form [<kind>:][<namespace>/]<name>',
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
request(app).get('/v1/todos?offset=1.5'),
|
||||
).resolves.toMatchObject(
|
||||
matchErrorResponse(
|
||||
400,
|
||||
'InputError',
|
||||
'invalid offset query, not an integer',
|
||||
),
|
||||
);
|
||||
request(app)
|
||||
.get('/v1/todos?offset=1.5')
|
||||
.expect(400)
|
||||
.expect(
|
||||
matchErrorResponse(
|
||||
400,
|
||||
'InputError',
|
||||
'invalid offset query, not an integer',
|
||||
),
|
||||
);
|
||||
|
||||
expect(mockService.listTodos).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIntegerParam', () => {
|
||||
it('should parse a param', () => {
|
||||
expect(parseIntegerParam('1', 'ctx')).toBe(1);
|
||||
});
|
||||
|
||||
it('should reject invalid params', () => {
|
||||
expect(() => parseIntegerParam(['1'], 'ctx')).toThrow(
|
||||
'invalid ctx, must be a string',
|
||||
);
|
||||
expect(() => parseIntegerParam('1.5', 'ctx')).toThrow(
|
||||
'invalid ctx, not an integer',
|
||||
);
|
||||
expect(() => parseIntegerParam('foo', 'ctx')).toThrow(
|
||||
'invalid ctx, not an integer',
|
||||
);
|
||||
expect(() => parseIntegerParam('1foo', 'ctx')).toThrow(
|
||||
'invalid ctx, not an integer',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseOrderByParam', () => {
|
||||
it('should parse a param', () => {
|
||||
expect(parseOrderByParam('a=asc', ['a'])).toEqual({
|
||||
|
||||
@@ -17,16 +17,13 @@
|
||||
import { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { type TodoService, TODO_FIELDS } from './types';
|
||||
import {
|
||||
getBearerToken,
|
||||
parseFilterParam,
|
||||
parseIntegerParam,
|
||||
parseOrderByParam,
|
||||
} from '../lib/utils';
|
||||
import spec from '../schema/openapi.generated';
|
||||
import type { ApiRouter } from '@backstage/backend-openapi-utils';
|
||||
import { createOpenApiRouter } from '../schema/openapi.generated';
|
||||
|
||||
/** @public */
|
||||
export interface RouterOptions {
|
||||
@@ -37,14 +34,12 @@ export interface RouterOptions {
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const router = await createOpenApiRouter();
|
||||
router.use(express.json());
|
||||
const { todoService } = options;
|
||||
|
||||
const router = Router() as ApiRouter<typeof spec>;
|
||||
router.use(express.json());
|
||||
|
||||
router.get('/v1/todos', async (req, res) => {
|
||||
const offset = parseIntegerParam(req.query.offset, 'offset query');
|
||||
const limit = parseIntegerParam(req.query.limit, 'limit query');
|
||||
const { offset, limit } = req.query;
|
||||
const orderBy = parseOrderByParam(req.query.orderBy, TODO_FIELDS);
|
||||
const filters = parseFilterParam(req.query.filter, TODO_FIELDS);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user