feat: swagger annotations (#713)
This commit is contained in:
parent
fe357574bd
commit
8eede4b3c4
37
README.md
37
README.md
@ -458,6 +458,43 @@ is resolved to:
|
|||||||
|
|
||||||
`WHERE ... AND (id = 5 OR id = 7) AND name = 'Milo' AND ...`
|
`WHERE ... AND (id = 5 OR id = 7) AND name = 'Milo' AND ...`
|
||||||
|
|
||||||
|
## Swagger
|
||||||
|
|
||||||
|
You can use two default decorators @ApiOkResponsePaginated and @ApiPagination to generate swagger documentation for your endpoints
|
||||||
|
|
||||||
|
`@ApiOkPaginatedResponse` is for response body, return http[](https://) status is 200
|
||||||
|
|
||||||
|
`@ApiPaginationQuery` is for query params
|
||||||
|
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Get()
|
||||||
|
@ApiOkPaginatedResponse(
|
||||||
|
UserDto,
|
||||||
|
USER_PAGINATION_CONFIG,
|
||||||
|
)
|
||||||
|
@ApiPaginationQuery(USER_PAGINATION_CONFIG)
|
||||||
|
async findAll(
|
||||||
|
@Paginate()
|
||||||
|
query: PaginateQuery,
|
||||||
|
): Promise<Paginated<UserEntity>> {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There is also some syntax sugar for this, and you can use only one decorator `@PaginatedSwaggerDocs` for both response body and query params
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Get()
|
||||||
|
@PaginatedSwaggerDocs(UserDto, USER_PAGINATION_CONFIG)
|
||||||
|
async findAll(
|
||||||
|
@Paginate()
|
||||||
|
query: PaginateQuery,
|
||||||
|
): Promise<Paginated<UserEntity>> {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
The package does not report error reasons in the response bodies. They are instead
|
The package does not report error reasons in the response bodies. They are instead
|
||||||
|
964
package-lock.json
generated
964
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,8 @@
|
|||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nestjs/testing": "^10.2.1",
|
||||||
|
"@nestjs/platform-express": "^10.2.1",
|
||||||
"@nestjs/common": "^10.2.1",
|
"@nestjs/common": "^10.2.1",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.4",
|
"@types/jest": "^29.5.4",
|
||||||
@ -59,6 +61,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@nestjs/common": "^10.2.1",
|
"@nestjs/common": "^10.2.1",
|
||||||
|
"@nestjs/swagger": "^7.1.8",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fastify": "^4.21.0",
|
"fastify": "^4.21.0",
|
||||||
"typeorm": "^0.3.17"
|
"typeorm": "^0.3.17"
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './decorator'
|
export * from './decorator'
|
||||||
export * from './paginate'
|
export * from './paginate'
|
||||||
|
export * from './swagger'
|
||||||
|
65
src/swagger/api-ok-paginated-response.decorator.ts
Normal file
65
src/swagger/api-ok-paginated-response.decorator.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { applyDecorators, Type } from '@nestjs/common'
|
||||||
|
import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger'
|
||||||
|
import { ReferenceObject, SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'
|
||||||
|
import { PaginateConfig } from '../paginate'
|
||||||
|
import { PaginatedDocumented } from './paginated-swagger.type'
|
||||||
|
|
||||||
|
export const ApiOkPaginatedResponse = <DTO extends Type<unknown>>(
|
||||||
|
dataDto: DTO,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
paginatedConfig: PaginateConfig<any>
|
||||||
|
) => {
|
||||||
|
const cols = paginatedConfig?.filterableColumns || {}
|
||||||
|
|
||||||
|
return applyDecorators(
|
||||||
|
ApiExtraModels(PaginatedDocumented, dataDto),
|
||||||
|
ApiOkResponse({
|
||||||
|
schema: {
|
||||||
|
allOf: [
|
||||||
|
{ $ref: getSchemaPath(PaginatedDocumented) },
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: { $ref: getSchemaPath(dataDto) },
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
properties: {
|
||||||
|
select: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
enum: paginatedConfig?.select,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: 'object',
|
||||||
|
properties: Object.keys(cols).reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
acc[key] = {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, SchemaObject | ReferenceObject>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
158
src/swagger/api-paginated-query.decorator.ts
Normal file
158
src/swagger/api-paginated-query.decorator.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, FilterOperator, FilterSuffix, PaginateConfig } from '../paginate'
|
||||||
|
import { ApiQuery } from '@nestjs/swagger'
|
||||||
|
import { FilterComparator } from '../filter'
|
||||||
|
import { applyDecorators } from '@nestjs/common'
|
||||||
|
|
||||||
|
const DEFAULT_VALUE_KEY = 'Default Value'
|
||||||
|
|
||||||
|
function p(key: string | 'Format' | 'Example' | 'Default Value' | 'Max Value', value: string) {
|
||||||
|
return `<p>
|
||||||
|
<b>${key}: </b> ${value}
|
||||||
|
</p>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function li(key: string | 'Available Fields', values: string[]) {
|
||||||
|
return `<h4>${key}</h4><ul>${values.map((v) => `<li>${v}</li>`).join('\n')}</ul>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortBy(paginationConfig: PaginateConfig<any>) {
|
||||||
|
const defaultSortMessage = paginationConfig.defaultSortBy
|
||||||
|
? paginationConfig.defaultSortBy.map(([col, order]) => `${col}:${order}`).join(',')
|
||||||
|
: 'No default sorting specified, the result order is not guaranteed'
|
||||||
|
|
||||||
|
return ApiQuery({
|
||||||
|
name: 'sortBy',
|
||||||
|
isArray: true,
|
||||||
|
description: `Parameter to sort by.
|
||||||
|
<p>To sort by multiple fields, just provide query param multiple types. The order in url defines an order of sorting</p>
|
||||||
|
${p('Format', 'fieldName:DIRECTION')}
|
||||||
|
${p('Example', 'sortBy=id:DESC&sortBy=createdAt:ASC')}
|
||||||
|
${p('Default Value', defaultSortMessage)}
|
||||||
|
${li('Available Fields', paginationConfig.sortableColumns)}
|
||||||
|
`,
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function Limit(paginationConfig: PaginateConfig<any>) {
|
||||||
|
return ApiQuery({
|
||||||
|
name: 'limit',
|
||||||
|
description: `Number of records per page.
|
||||||
|
${p('Example', '20')}
|
||||||
|
${p(DEFAULT_VALUE_KEY, paginationConfig?.defaultLimit?.toString() || DEFAULT_LIMIT.toString())}
|
||||||
|
${p('Max Value', paginationConfig.maxLimit?.toString() || DEFAULT_MAX_LIMIT.toString())}
|
||||||
|
|
||||||
|
If provided value is greater than max value, max value will be applied.
|
||||||
|
`,
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function Select(paginationConfig: PaginateConfig<any>) {
|
||||||
|
if (!paginationConfig.select) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiQuery({
|
||||||
|
name: 'select',
|
||||||
|
description: `List of fields to select.
|
||||||
|
${p('Example', paginationConfig.select.slice(0, Math.min(5, paginationConfig.select.length)).join(','))}
|
||||||
|
${p(
|
||||||
|
DEFAULT_VALUE_KEY,
|
||||||
|
'By default all fields returns. If you want to select only some fields, provide them in query param'
|
||||||
|
)}
|
||||||
|
`,
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function Where(paginationConfig: PaginateConfig<any>) {
|
||||||
|
if (!paginationConfig.filterableColumns) return
|
||||||
|
|
||||||
|
const allColumnsDecorators = Object.entries(paginationConfig.filterableColumns)
|
||||||
|
.map(([fieldName, filterOperations]) => {
|
||||||
|
const operations =
|
||||||
|
filterOperations === true || filterOperations === undefined
|
||||||
|
? [
|
||||||
|
...Object.values(FilterComparator),
|
||||||
|
...Object.values(FilterSuffix),
|
||||||
|
...Object.values(FilterOperator),
|
||||||
|
]
|
||||||
|
: filterOperations.map((fo) => fo.toString())
|
||||||
|
|
||||||
|
return ApiQuery({
|
||||||
|
name: `filter.${fieldName}`,
|
||||||
|
description: `Filter by ${fieldName} query param.
|
||||||
|
${p('Format', `filter.${fieldName}={$not}:OPERATION:VALUE`)}
|
||||||
|
${p('Example', `filter.${fieldName}=$not:$like:John Doe&filter.${fieldName}=like:John`)}
|
||||||
|
${li('Available Operations', operations)}`,
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
isArray: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.filter((v) => v !== undefined)
|
||||||
|
|
||||||
|
return applyDecorators(...allColumnsDecorators)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Page() {
|
||||||
|
return ApiQuery({
|
||||||
|
name: 'page',
|
||||||
|
description: `Page number to retrieve.If you provide invalid value the default page number will applied
|
||||||
|
${p('Example', '1')}
|
||||||
|
${p(DEFAULT_VALUE_KEY, '1')}
|
||||||
|
`,
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function Search(paginateConfig: PaginateConfig<any>) {
|
||||||
|
if (!paginateConfig.searchableColumns) return
|
||||||
|
|
||||||
|
return ApiQuery({
|
||||||
|
name: 'search',
|
||||||
|
description: `Search term to filter result values
|
||||||
|
${p('Example', 'John')}
|
||||||
|
${p(DEFAULT_VALUE_KEY, 'No default value')}
|
||||||
|
`,
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchBy(paginateConfig: PaginateConfig<any>) {
|
||||||
|
if (!paginateConfig.searchableColumns) return
|
||||||
|
|
||||||
|
return ApiQuery({
|
||||||
|
name: 'searchBy',
|
||||||
|
description: `List of fields to search by term to filter result values
|
||||||
|
${p(
|
||||||
|
'Example',
|
||||||
|
paginateConfig.searchableColumns.slice(0, Math.min(5, paginateConfig.searchableColumns.length)).join(',')
|
||||||
|
)}
|
||||||
|
${p(DEFAULT_VALUE_KEY, 'By default all fields mentioned below will be used to search by term')}
|
||||||
|
${li('Available Fields', paginateConfig.searchableColumns)}
|
||||||
|
`,
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiPaginationQuery = (paginationConfig: PaginateConfig<any>) => {
|
||||||
|
return applyDecorators(
|
||||||
|
...[
|
||||||
|
Page(),
|
||||||
|
Limit(paginationConfig),
|
||||||
|
Where(paginationConfig),
|
||||||
|
SortBy(paginationConfig),
|
||||||
|
Search(paginationConfig),
|
||||||
|
SearchBy(paginationConfig),
|
||||||
|
Select(paginationConfig),
|
||||||
|
].filter((v): v is MethodDecorator => v !== undefined)
|
||||||
|
)
|
||||||
|
}
|
8
src/swagger/api-paginated-swagger-docs.decorator.ts
Normal file
8
src/swagger/api-paginated-swagger-docs.decorator.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { applyDecorators, Type } from '@nestjs/common'
|
||||||
|
import { PaginateConfig } from '../paginate'
|
||||||
|
import { ApiPaginationQuery } from './api-paginated-query.decorator'
|
||||||
|
import { ApiOkPaginatedResponse } from './api-ok-paginated-response.decorator'
|
||||||
|
|
||||||
|
export function PaginatedSwaggerDocs<DTO extends Type<unknown>>(dto: DTO, paginatedConfig: PaginateConfig<any>) {
|
||||||
|
return applyDecorators(ApiOkPaginatedResponse(dto, paginatedConfig), ApiPaginationQuery(paginatedConfig))
|
||||||
|
}
|
4
src/swagger/index.ts
Normal file
4
src/swagger/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './api-paginated-query.decorator'
|
||||||
|
export * from './api-ok-paginated-response.decorator'
|
||||||
|
export * from './paginated-swagger.type'
|
||||||
|
export * from './api-paginated-swagger-docs.decorator'
|
145
src/swagger/paginated-swagger.type.ts
Normal file
145
src/swagger/paginated-swagger.type.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger'
|
||||||
|
import { Column, SortBy } from '../helper'
|
||||||
|
import { Paginated } from '../paginate'
|
||||||
|
|
||||||
|
class PaginatedLinksDocumented {
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Link to first page',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
first?: string
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Link to previous page',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
previous?: string
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Link to current page',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
current!: string
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Link to next page',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
next?: string
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Link to last page',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
last?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaginatedMetaDocumented<T> {
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Number of items per page',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
itemsPerPage!: number
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Total number of items',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
totalItems!: number
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Current requested page',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
currentPage!: number
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Total number of pages',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
totalPages!: number
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Sorting by columns',
|
||||||
|
required: false,
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
enum: ['ASC', 'DESC'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
sortBy!: SortBy<T>
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Search by fields',
|
||||||
|
required: false,
|
||||||
|
isArray: true,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
searchBy!: Column<T>[]
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Search term',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
search!: string
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'List of selected fields',
|
||||||
|
required: false,
|
||||||
|
isArray: true,
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
select!: string[]
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Filters that applied to the query',
|
||||||
|
required: false,
|
||||||
|
isArray: false,
|
||||||
|
type: 'object',
|
||||||
|
})
|
||||||
|
filter?: {
|
||||||
|
[p: string]: string | string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaginatedDocumented<T> extends Paginated<T> {
|
||||||
|
@ApiProperty({
|
||||||
|
isArray: true,
|
||||||
|
required: true,
|
||||||
|
title: 'Array of entities',
|
||||||
|
})
|
||||||
|
override data!: T[]
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Pagination Metadata',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
override meta!: PaginatedMetaDocumented<T>
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Links to pages',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
override links!: PaginatedLinksDocumented
|
||||||
|
}
|
310
src/swagger/pagination-docs.spec.ts
Normal file
310
src/swagger/pagination-docs.spec.ts
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
import { Get, Post, Type } from '@nestjs/common'
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
||||||
|
import { FilterOperator, FilterSuffix, PaginateConfig } from '../paginate'
|
||||||
|
import { Test } from '@nestjs/testing'
|
||||||
|
import { PaginatedSwaggerDocs } from './api-paginated-swagger-docs.decorator'
|
||||||
|
import { ApiPaginationQuery } from './api-paginated-query.decorator'
|
||||||
|
import { ApiOkPaginatedResponse } from './api-ok-paginated-response.decorator'
|
||||||
|
|
||||||
|
const BASE_PAGINATION_CONFIG = {
|
||||||
|
sortableColumns: ['id'],
|
||||||
|
} satisfies PaginateConfig<TestDto>
|
||||||
|
|
||||||
|
const FULL_CONFIG = {
|
||||||
|
...BASE_PAGINATION_CONFIG,
|
||||||
|
defaultSortBy: [['id', 'DESC']],
|
||||||
|
defaultLimit: 20,
|
||||||
|
maxLimit: 100,
|
||||||
|
filterableColumns: {
|
||||||
|
id: true,
|
||||||
|
name: [FilterOperator.EQ, FilterSuffix.NOT],
|
||||||
|
},
|
||||||
|
searchableColumns: ['name'],
|
||||||
|
select: ['id', 'name'],
|
||||||
|
} satisfies PaginateConfig<TestDto>
|
||||||
|
|
||||||
|
class TestDto {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
async function getSwaggerDefinitionForEndpoint<T>(entityType: Type<T>, config: PaginateConfig<T>) {
|
||||||
|
class TestController {
|
||||||
|
@PaginatedSwaggerDocs(entityType, config)
|
||||||
|
@Get('/test')
|
||||||
|
public test(): void {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiPaginationQuery(config)
|
||||||
|
@ApiOkPaginatedResponse(entityType, config)
|
||||||
|
@Post('/test')
|
||||||
|
public testPost(): void {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fakeAppModule = await Test.createTestingModule({
|
||||||
|
controllers: [TestController],
|
||||||
|
}).compile()
|
||||||
|
const fakeApp = fakeAppModule.createNestApplication()
|
||||||
|
|
||||||
|
return SwaggerModule.createDocument(fakeApp, new DocumentBuilder().build())
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PaginatedEndpoint decorator', () => {
|
||||||
|
it('post and get definition should be the same', async () => {
|
||||||
|
const openApiDefinition = await getSwaggerDefinitionForEndpoint(TestDto, BASE_PAGINATION_CONFIG)
|
||||||
|
|
||||||
|
expect(openApiDefinition.paths['/test'].get.parameters).toStrictEqual(
|
||||||
|
openApiDefinition.paths['/test'].post.parameters
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should annotate endpoint with OpenApi documentation with limited config', async () => {
|
||||||
|
const openApiDefinition = await getSwaggerDefinitionForEndpoint(TestDto, BASE_PAGINATION_CONFIG)
|
||||||
|
|
||||||
|
const params = openApiDefinition.paths['/test'].get.parameters
|
||||||
|
expect(params).toStrictEqual([
|
||||||
|
{
|
||||||
|
name: 'page',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Page number to retrieve.If you provide invalid value the default page number will applied\n <p>\n <b>Example: </b> 1\n </p>\n <p>\n <b>Default Value: </b> 1\n </p>\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'limit',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Number of records per page.\n <p>\n <b>Example: </b> 20\n </p>\n <p>\n <b>Default Value: </b> 20\n </p>\n <p>\n <b>Max Value: </b> 100\n </p>\n\n If provided value is greater than max value, max value will be applied.\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortBy',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Parameter to sort by.\n <p>To sort by multiple fields, just provide query param multiple types. The order in url defines an order of sorting</p>\n <p>\n <b>Format: </b> fieldName:DIRECTION\n </p>\n <p>\n <b>Example: </b> sortBy=id:DESC&sortBy=createdAt:ASC\n </p>\n <p>\n <b>Default Value: </b> No default sorting specified, the result order is not guaranteed\n </p>\n <h4>Available Fields</h4><ul><li>id</li></ul>\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(openApiDefinition.paths['/test'].get.responses).toEqual({
|
||||||
|
'200': {
|
||||||
|
description: '',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
allOf: [
|
||||||
|
{
|
||||||
|
$ref: '#/components/schemas/PaginatedDocumented',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/TestDto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
properties: {
|
||||||
|
select: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should annotate endpoint with OpenApi documentation with full config', async () => {
|
||||||
|
const openApiDefinition = await getSwaggerDefinitionForEndpoint(TestDto, FULL_CONFIG)
|
||||||
|
|
||||||
|
const params = openApiDefinition.paths['/test'].get.parameters
|
||||||
|
expect(params).toStrictEqual([
|
||||||
|
{
|
||||||
|
name: 'page',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Page number to retrieve.If you provide invalid value the default page number will applied\n <p>\n <b>Example: </b> 1\n </p>\n <p>\n <b>Default Value: </b> 1\n </p>\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'limit',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Number of records per page.\n <p>\n <b>Example: </b> 20\n </p>\n <p>\n <b>Default Value: </b> 20\n </p>\n <p>\n <b>Max Value: </b> 100\n </p>\n\n If provided value is greater than max value, max value will be applied.\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'filter.id',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Filter by id query param.\n <p>\n <b>Format: </b> filter.id={$not}:OPERATION:VALUE\n </p>\n <p>\n <b>Example: </b> filter.id=$not:$like:John Doe&filter.id=like:John\n </p>\n <h4>Available Operations</h4><ul><li>$and</li>\n<li>$or</li>\n<li>$not</li>\n<li>$eq</li>\n<li>$gt</li>\n<li>$gte</li>\n<li>$in</li>\n<li>$null</li>\n<li>$lt</li>\n<li>$lte</li>\n<li>$btw</li>\n<li>$ilike</li>\n<li>$sw</li>\n<li>$contains</li></ul>',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'filter.name',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Filter by name query param.\n <p>\n <b>Format: </b> filter.name={$not}:OPERATION:VALUE\n </p>\n <p>\n <b>Example: </b> filter.name=$not:$like:John Doe&filter.name=like:John\n </p>\n <h4>Available Operations</h4><ul><li>$eq</li>\n<li>$not</li></ul>',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortBy',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Parameter to sort by.\n <p>To sort by multiple fields, just provide query param multiple types. The order in url defines an order of sorting</p>\n <p>\n <b>Format: </b> fieldName:DIRECTION\n </p>\n <p>\n <b>Example: </b> sortBy=id:DESC&sortBy=createdAt:ASC\n </p>\n <p>\n <b>Default Value: </b> id:DESC\n </p>\n <h4>Available Fields</h4><ul><li>id</li></ul>\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'search',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'Search term to filter result values\n <p>\n <b>Example: </b> John\n </p>\n <p>\n <b>Default Value: </b> No default value\n </p>\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'searchBy',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'List of fields to search by term to filter result values\n <p>\n <b>Example: </b> name\n </p>\n <p>\n <b>Default Value: </b> By default all fields mentioned below will be used to search by term\n </p>\n <h4>Available Fields</h4><ul><li>name</li></ul>\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'select',
|
||||||
|
required: false,
|
||||||
|
in: 'query',
|
||||||
|
description:
|
||||||
|
'List of fields to select.\n <p>\n <b>Example: </b> id,name\n </p>\n <p>\n <b>Default Value: </b> By default all fields returns. If you want to select only some fields, provide them in query param\n </p>\n ',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(openApiDefinition.paths['/test'].get.responses).toEqual({
|
||||||
|
'200': {
|
||||||
|
description: '',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
allOf: [
|
||||||
|
{
|
||||||
|
$ref: '#/components/schemas/PaginatedDocumented',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/TestDto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
properties: {
|
||||||
|
select: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['id', 'name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user