Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QueryParams are parsed as separate params #49

Merged
merged 6 commits into from
Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ npm-debug*
build
node_modules
coverage
.idea/
.vscode/
*.key
*.secret
Expand Down
38 changes: 19 additions & 19 deletions __tests__/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,46 +192,46 @@ describe('decorators', () => {
})

it('merges keywords defined in @OpenAPI decorator into operation', () => {
const operation = getOperation(routes.listUsers)
const operation = getOperation(routes.listUsers, {})
expect(operation.description).toEqual('List all users')
})

it('applies @OpenAPI decorator function parameter to operation', () => {
const operation = getOperation(routes.getUser)
const operation = getOperation(routes.getUser, {})
expect(operation.tags).toEqual(['Users', 'custom-tag'])
})

it('merges consecutive @OpenAPI object parameters top-down', () => {
const operation = getOperation(routes.multipleOpenAPIsWithObjectParam)
const operation = getOperation(routes.multipleOpenAPIsWithObjectParam, {})
expect(operation.summary).toEqual('Some summary')
expect(operation.description).toEqual('Some description')
expect(operation['x-custom-key']).toEqual('Custom value')
})

it('applies consecutive @OpenAPI function parameters top-down', () => {
const operation = getOperation(routes.multipleOpenAPIsWithFunctionParam)
const operation = getOperation(routes.multipleOpenAPIsWithFunctionParam, {})
expect(operation.summary).toEqual('Some summary')
expect(operation.description).toEqual('Some description')
expect(operation['x-custom-key']).toEqual(20)
})

it('merges and applies consecutive @OpenAPI object and function parameters top-down', () => {
const operation = getOperation(routes.multipleOpenAPIsWithMixedParam)
const operation = getOperation(routes.multipleOpenAPIsWithMixedParam, {})
expect(operation.summary).toEqual('Some summary')
expect(operation.description).toEqual('Some description')
expect(operation['x-custom-key']).toEqual(20)
})

it('applies @ResponseSchema merging in response schema into source metadata', () => {
const operation = getOperation(routes.responseSchemaDefaults)
const operation = getOperation(routes.responseSchemaDefaults, {})
// ensure other metadata doesnt get overwritten by decorator
expect(operation.operationId).toEqual(
'UsersController.responseSchemaDefaults'
)
})

it('applies @ResponseSchema using default contentType and statusCode', () => {
const operation = getOperation(routes.responseSchemaDefaults)
const operation = getOperation(routes.responseSchemaDefaults, {})
expect(operation.responses).toEqual({
'200': {
content: {
Expand All @@ -247,7 +247,7 @@ describe('decorators', () => {
})

it('applies @ResponseSchema using contentType and statusCode from options object', () => {
const operation = getOperation(routes.responseSchemaOptions)
const operation = getOperation(routes.responseSchemaOptions, {})
expect(operation.responses).toEqual({
'200': {
content: {
Expand All @@ -269,14 +269,14 @@ describe('decorators', () => {
})

it('applies @ResponseSchema using contentType and statusCode from decorators', () => {
const operation = getOperation(routes.responseSchemaDecorators)
const operation = getOperation(routes.responseSchemaDecorators, {})
expect(operation.responses['201'].content['application/pdf']).toEqual({
schema: { $ref: '#/components/schemas/ModelDto' }
})
})

it('applies @ResponseSchema using isArray flag set to true', () => {
const operation = getOperation(routes.responseSchemaArray)
const operation = getOperation(routes.responseSchemaArray, {})
expect(operation.responses['200'].content['application/json']).toEqual({
schema: {
items: {
Expand All @@ -288,7 +288,7 @@ describe('decorators', () => {
})

it('applies @ResponseSchema using contentType and statusCode from options object, overruling options from RC decorators', () => {
const operation = getOperation(routes.responseSchemaDecoratorAndSchema)
const operation = getOperation(routes.responseSchemaDecoratorAndSchema, {})
expect(operation.responses).toEqual({
'201': {
content: {
Expand All @@ -308,7 +308,7 @@ describe('decorators', () => {
})

it('applies @ResponseSchema using a string as ModelName', () => {
const operation = getOperation(routes.responseSchemaModelAsString)
const operation = getOperation(routes.responseSchemaModelAsString, {})
expect(operation.responses).toEqual({
'200': {
content: {
Expand All @@ -329,7 +329,7 @@ describe('decorators', () => {

it('applies @ResponseSchema while retaining inner OpenAPI decorator', () => {
const operation = getOperation(
routes.responseSchemaNotOverwritingInnerOpenApiDecorator
routes.responseSchemaNotOverwritingInnerOpenApiDecorator, {}
)
expect(operation.description).toEqual('somedescription')
expect(operation.responses).toEqual({
Expand All @@ -352,7 +352,7 @@ describe('decorators', () => {

it('applies @ResponseSchema while retaining outer OpenAPI decorator', () => {
const operation = getOperation(
routes.responseSchemaNotOverwritingOuterOpenApiDecorator
routes.responseSchemaNotOverwritingOuterOpenApiDecorator, {}
)
expect(operation.description).toEqual('somedescription')
expect(operation.responses).toEqual({
Expand All @@ -374,7 +374,7 @@ describe('decorators', () => {
})

it('does not apply @ResponseSchema if empty ModelName is passed', () => {
const operation = getOperation(routes.responseSchemaNoNoModel)
const operation = getOperation(routes.responseSchemaNoNoModel, {})
expect(operation.responses).toEqual({
'200': {
content: {
Expand All @@ -386,7 +386,7 @@ describe('decorators', () => {
})

it('applies @ResponseSchema using default contentType and statusCode from @Controller (non-json)', () => {
const operation = getOperation(routes.responseSchemaDefaultsHtml)
const operation = getOperation(routes.responseSchemaDefaultsHtml, {})
expect(operation.responses).toEqual({
'200': {
content: {
Expand All @@ -402,7 +402,7 @@ describe('decorators', () => {
})

it('applies multiple @ResponseSchema on a single handler', () => {
const operation = getOperation(routes.multipleResponseSchemas)
const operation = getOperation(routes.multipleResponseSchemas, {})
expect(operation.responses).toEqual({
'200': {
content: {
Expand Down Expand Up @@ -474,7 +474,7 @@ describe('@OpenAPI-decorated class', () => {
})

it('applies controller OpenAPI props to each method with method-specific props taking precedence', () => {
expect(getOperation(routes.listItems)).toEqual(
expect(getOperation(routes.listItems, {})).toEqual(
expect.objectContaining({
description: 'List all items',
externalDocs: { url: 'http://docs.com' },
Expand All @@ -483,7 +483,7 @@ describe('@OpenAPI-decorated class', () => {
})
)

expect(getOperation(routes.getItem)).toEqual(
expect(getOperation(routes.getItem, {})).toEqual(
expect.objectContaining({
description: 'Common description',
externalDocs: { url: 'http://docs.com' },
Expand Down
16 changes: 14 additions & 2 deletions __tests__/fixtures/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,22 @@
},
{
"in": "query",
"name": "ListUsersQueryParams",
"name": "email",
"required": false,
"schema": {
"$ref": "#/components/schemas/ListUsersQueryParams"
"format": "email",
"type": "string"
}
},
{
"in": "query",
"name": "types",
"required": true,
"schema": {
"items": {
"type": "string"
},
"type": "array"
}
}
],
Expand Down
8 changes: 4 additions & 4 deletions __tests__/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,24 @@ describe('options', () => {

it('sets query parameter optional by default', () => {
const route = routes[0]
expect(getQueryParams(route)[0].required).toEqual(false)
expect(getQueryParams(route, {})[0].required).toEqual(false)
})

it('sets query parameter required as per global options', () => {
const route = routes[0]
route.options.defaults = { paramOptions: { required: true } }
expect(getQueryParams(route)[0].required).toEqual(true)
expect(getQueryParams(route, {})[0].required).toEqual(true)
})

it('uses local required option over the global one', () => {
const route = routes[0]
route.options.defaults = { paramOptions: { required: true } }
expect(getQueryParams(route)[1].required).toEqual(false)
expect(getQueryParams(route, {})[1].required).toEqual(false)
})

it('uses the explicit `type` parameter to override request query type', () => {
const route = routes[1]
expect(getQueryParams(route)[0]).toEqual({
expect(getQueryParams(route, {})[0]).toEqual({
in: "query",
name: "param",
required: false,
Expand Down
71 changes: 60 additions & 11 deletions __tests__/parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,33 @@ import {
IRoute,
parseRoutes
} from '../src'
import { SchemaObject } from 'openapi3-ts'
import { validationMetadatasToSchemas } from 'class-validator-jsonschema'
import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator'
import { defaultMetadataStorage } from 'class-transformer/storage'

describe('parameters', () => {
let route: IRoute
let schemas: { [p: string]: SchemaObject }

beforeAll(() => {
class ListUsersHeaderParams {}
class ListUsersQueryParams {}
class ListUsersHeaderParams {
}

class ListUsersQueryParams {
@IsNumber()
genderId: number

@IsBoolean()
@IsOptional()
isPretty: boolean

@IsString({ each: true })
types: string[]
}

@JsonController('/users')
// @ts-ignore: not referenced
// @ts-ignore: not referenced
class UsersController {
@Get('/:string/:regex(\\d{6})/:optional?/:number/:boolean/:any')
getPost(
Expand All @@ -36,7 +53,7 @@ describe('parameters', () => {
@Param('any') _anyParam: any,
@QueryParam('limit') _limit: number,
@HeaderParam('Authorization', { required: true })
_authorization: string,
_authorization: string,
@QueryParams() _queryRef?: ListUsersQueryParams,
@HeaderParams() _headerParams?: ListUsersHeaderParams
) {
Expand All @@ -45,6 +62,10 @@ describe('parameters', () => {
}

route = parseRoutes(getMetadataArgsStorage())[0]
schemas = validationMetadatasToSchemas({
classTransformerMetadataStorage: defaultMetadataStorage,
refPointerPrefix: '#/components/schemas/'
})
})

it('parses path parameter from path strings', () => {
Expand Down Expand Up @@ -134,7 +155,7 @@ describe('parameters', () => {
})

it('parses query param from @QueryParam decorator', () => {
expect(getQueryParams(route)[0]).toEqual({
expect(getQueryParams(route, schemas)[0]).toEqual({
in: 'query',
name: 'limit',
required: false,
Expand All @@ -143,12 +164,40 @@ describe('parameters', () => {
})

it('parses query param ref from @QueryParams decorator', () => {
expect(getQueryParams(route)[1]).toEqual({
in: 'query',
name: 'ListUsersQueryParams',
required: false,
schema: { $ref: '#/components/schemas/ListUsersQueryParams' }
})
expect(getQueryParams(route, schemas)).toEqual([
// limit comes from @QueryParam
{
in: 'query',
name: 'limit',
required: false,
schema: { type: 'number' }
},
{
in: 'query',
name: 'genderId',
required: true,
schema: { type: 'number' }
},
{
in: 'query',
name: 'isPretty',
required: false,
schema: {
type: 'boolean'
}
},
{
in: 'query',
name: 'types',
required: true,
schema: {
items: {
type: 'string'
},
type: 'array'
}
}
])
})

it('parses header param from @HeaderParam decorator', () => {
Expand Down
17 changes: 14 additions & 3 deletions sample/01-basic/UsersController.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { IsOptional, IsString, MaxLength } from 'class-validator'
import { IsOptional, IsString, MaxLength, IsNumber, IsPositive } from 'class-validator'
import {
Body,
Get,
JsonController,
Param,
Post,
Put
Put,
QueryParams
} from 'routing-controllers'
import { OpenAPI } from 'routing-controllers-openapi'

Expand All @@ -18,14 +19,24 @@ class CreateUserBody {
hobbies: string[]
}

class PaginationQuery {
@IsNumber()
@IsPositive()
public limit: number;

@IsNumber()
@IsOptional()
public offset?: number;
}

@OpenAPI({
security: [{ basicAuth: [] }]
})
@JsonController('/users')
export class UsersController {
@Get('/')
@OpenAPI({ summary: 'Return a list of users' })
getAll() {
getAll(@QueryParams() query: PaginationQuery) {
return [
{ id: 1, name: 'First user!', hobbies: [] },
{ id: 2, name: 'Second user!', hobbies: ['fishing', 'cycling'] }
Expand Down
3 changes: 3 additions & 0 deletions sample/01-basic/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getMetadataArgsStorage
} from 'routing-controllers'
import { routingControllersToSpec } from 'routing-controllers-openapi'
import * as swaggerUiExpress from 'swagger-ui-express';

import { UsersController } from './UsersController'

Expand Down Expand Up @@ -39,6 +40,8 @@ const spec = routingControllersToSpec(storage, routingControllersOptions, {
}
})

app.use('/docs', swaggerUiExpress.serve, swaggerUiExpress.setup(spec));

// Render spec on root:
app.get('/', (_req, res) => {
res.json(spec)
Expand Down
Loading