Skip to content

Commit 2d71292

Browse files
authored
feat: replaced HTML formatting with markdown as new versions of swagger wrapping HTML into code that looks weird, and added documentation for customisations (#1093)
1 parent b9748a6 commit 2d71292

File tree

3 files changed

+135
-61
lines changed

3 files changed

+135
-61
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,50 @@ There is also some syntax sugar for this, and you can use only one decorator `@P
680680
}
681681
```
682682

683+
It is also possible to customize a swagger UI completely or partially, by following the default implementation and creating your own version of PaginatedSwaggerDocs decorator
684+
685+
Let's say you want some custom appearance for SortBy, you need to create a decorator for it
686+
687+
```typescript
688+
export function CustomSortBy(paginationConfig: PaginateConfig<any>) {
689+
return ApiQuery({
690+
name: 'sortBy',
691+
isArray: true,
692+
description: `My custom sort by description`,
693+
required: false,
694+
type: 'string',
695+
})
696+
}
697+
```
698+
699+
Now you can create your version of the whole docs decorator and use it
700+
701+
```typescript
702+
703+
const CustomApiPaginationQuery = (paginationConfig: PaginateConfig<any>) => {
704+
return applyDecorators(
705+
...[
706+
Page(),
707+
Limit(paginationConfig),
708+
Where(paginationConfig),
709+
CustomSortBy(paginationConfig),
710+
Search(paginationConfig),
711+
SearchBy(paginationConfig),
712+
Select(paginationConfig),
713+
].filter((v): v is MethodDecorator => v !== undefined)
714+
)
715+
}
716+
717+
function CustomPaginatedSwaggerDocs<DTO extends Type<unknown>>(dto: DTO, paginatedConfig: PaginateConfig<any>) {
718+
return applyDecorators(ApiOkPaginatedResponse(dto, paginatedConfig), CustomApiPaginationQuery(paginatedConfig))
719+
}
720+
721+
```
722+
723+
You can use CustomPaginatedSwaggerDocs instead of default PaginatedSwaggerDocs
724+
725+
726+
683727
## Troubleshooting
684728

685729
The package does not report error reasons in the response bodies. They are instead

src/swagger/api-paginated-query.decorator.ts

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,98 +3,128 @@ import { ApiQuery } from '@nestjs/swagger'
33
import { FilterComparator } from '../filter'
44
import { FilterOperator, FilterSuffix, PaginateConfig } from '../paginate'
55
import globalConfig from '../global-config'
6+
import { isNil } from '../helper'
67

78
const DEFAULT_VALUE_KEY = 'Default Value'
89

10+
const allFilterSuffixes = Object.values(FilterSuffix).map((v) => v.toString())
11+
912
function p(key: string | 'Format' | 'Example' | 'Default Value' | 'Max Value', value: string) {
10-
return `<p>
11-
<b>${key}: </b> ${value}
12-
</p>`
13+
return `
14+
**${key}:** ${value}
15+
`
1316
}
1417

1518
function li(key: string | 'Available Fields', values: string[]) {
16-
return `<h4>${key}</h4><ul>${values.map((v) => `<li>${v}</li>`).join('\n')}</ul>`
19+
return `**${key}**
20+
${values.map((v) => `- ${v}`).join('\n\n')}`
1721
}
1822

1923
export function SortBy(paginationConfig: PaginateConfig<any>) {
24+
const sortableColumnNotAvailable =
25+
isNil(paginationConfig.sortableColumns) || paginationConfig.sortableColumns.length === 0
26+
27+
if (isNil(paginationConfig.defaultSortBy) && sortableColumnNotAvailable) {
28+
// no sorting allowed or predefined
29+
return undefined
30+
}
31+
2032
const defaultSortMessage = paginationConfig.defaultSortBy
2133
? paginationConfig.defaultSortBy.map(([col, order]) => `${col}:${order}`).join(',')
22-
: 'No default sorting specified, the result order is not guaranteed'
34+
: 'No default sorting specified, the result order is not guaranteed if not provided'
2335

2436
const sortBy = paginationConfig.sortableColumns.reduce((prev, curr) => {
2537
return [...prev, `${curr}:ASC`, `${curr}:DESC`]
2638
}, [])
2739

40+
const exampleValue = sortableColumnNotAvailable
41+
? 'Allowed sortable columns are not provided, only default sorting will be used'
42+
: paginationConfig.sortableColumns
43+
.slice(0, 2)
44+
.map((col) => `sortBy=${col}:DESC`)
45+
.join('&')
46+
2847
return ApiQuery({
2948
name: 'sortBy',
3049
isArray: true,
3150
enum: sortBy,
3251
description: `Parameter to sort by.
33-
<p>To sort by multiple fields, just provide query param multiple types. The order in url defines an order of sorting</p>
34-
${p('Format', 'fieldName:DIRECTION')}
35-
${p('Example', 'sortBy=id:DESC&sortBy=createdAt:ASC')}
36-
${p('Default Value', defaultSortMessage)}
37-
${li('Available Fields', paginationConfig.sortableColumns)}
38-
`,
52+
To sort by multiple fields, just provide query param multiple types. The order in url defines an order of sorting
53+
${p('Format', '{fieldName}:{DIRECTION}')}
54+
${p('Example', exampleValue)}
55+
${p('Default Value', defaultSortMessage)}
56+
${li('Available Fields', paginationConfig.sortableColumns)}
57+
`,
3958
required: false,
4059
type: 'string',
4160
})
4261
}
4362

44-
function Limit(paginationConfig: PaginateConfig<any>) {
63+
export function Limit(paginationConfig: PaginateConfig<any>) {
4564
return ApiQuery({
4665
name: 'limit',
4766
description: `Number of records per page.
48-
${p('Example', '20')}
49-
${p(DEFAULT_VALUE_KEY, paginationConfig?.defaultLimit?.toString() || globalConfig.defaultLimit.toString())}
50-
${p('Max Value', paginationConfig.maxLimit?.toString() || globalConfig.defaultMaxLimit.toString())}
5167
52-
If provided value is greater than max value, max value will be applied.
53-
`,
68+
${p('Example', globalConfig.defaultLimit.toString())}
69+
70+
${p(DEFAULT_VALUE_KEY, paginationConfig?.defaultLimit?.toString() || globalConfig.defaultLimit.toString())}
71+
72+
${p('Max Value', paginationConfig.maxLimit?.toString() || globalConfig.defaultMaxLimit.toString())}
73+
74+
If provided value is greater than max value, max value will be applied.
75+
`,
5476
required: false,
5577
type: 'number',
5678
})
5779
}
5880

59-
function Select(paginationConfig: PaginateConfig<any>) {
81+
export function Select(paginationConfig: PaginateConfig<any>) {
6082
if (!paginationConfig.select) {
6183
return
6284
}
6385

6486
return ApiQuery({
6587
name: 'select',
6688
description: `List of fields to select.
67-
${p('Example', paginationConfig.select.slice(0, Math.min(5, paginationConfig.select.length)).join(','))}
68-
${p(
69-
DEFAULT_VALUE_KEY,
70-
'By default all fields returns. If you want to select only some fields, provide them in query param'
71-
)}
72-
`,
89+
${p('Example', paginationConfig.select.slice(0, 5).join(','))}
90+
${p(
91+
DEFAULT_VALUE_KEY,
92+
'By default all fields returns. If you want to select only some fields, provide them in query param'
93+
)}
94+
`,
7395
required: false,
7496
type: 'string',
7597
})
7698
}
7799

78-
function Where(paginationConfig: PaginateConfig<any>) {
100+
export function Where(paginationConfig: PaginateConfig<any>) {
79101
if (!paginationConfig.filterableColumns) return
80102

81103
const allColumnsDecorators = Object.entries(paginationConfig.filterableColumns)
82104
.map(([fieldName, filterOperations]) => {
83105
const operations =
84106
filterOperations === true || filterOperations === undefined
85-
? [
86-
...Object.values(FilterComparator),
87-
...Object.values(FilterSuffix),
88-
...Object.values(FilterOperator),
89-
]
107+
? [...Object.values(FilterOperator), ...Object.values(FilterSuffix)]
90108
: filterOperations.map((fo) => fo.toString())
91109

110+
const operationsForExample =
111+
operations
112+
.filter((v) => !allFilterSuffixes.includes(v))
113+
.sort()
114+
.slice(0, 2) || []
115+
92116
return ApiQuery({
93117
name: `filter.${fieldName}`,
94118
description: `Filter by ${fieldName} query param.
95-
${p('Format', `filter.${fieldName}={$not}:OPERATION:VALUE`)}
96-
${p('Example', `filter.${fieldName}=$not:$like:John Doe&filter.${fieldName}=like:John`)}
97-
${li('Available Operations', operations)}`,
119+
${p('Format', `filter.${fieldName}={$not}:OPERATION:VALUE`)}
120+
121+
${p(
122+
'Example',
123+
operationsForExample.length === 0
124+
? 'No filtering allowed'
125+
: operationsForExample.map((v) => `filter.${fieldName}=${v}:John Doe`).join('&')
126+
)}
127+
${li('Available Operations', [...operations, ...Object.values(FilterComparator)])}`,
98128
required: false,
99129
type: 'string',
100130
isArray: true,
@@ -105,45 +135,45 @@ function Where(paginationConfig: PaginateConfig<any>) {
105135
return applyDecorators(...allColumnsDecorators)
106136
}
107137

108-
function Page() {
138+
export function Page() {
109139
return ApiQuery({
110140
name: 'page',
111-
description: `Page number to retrieve.If you provide invalid value the default page number will applied
112-
${p('Example', '1')}
113-
${p(DEFAULT_VALUE_KEY, '1')}
114-
`,
141+
description: `Page number to retrieve. If you provide invalid value the default page number will applied
142+
${p('Example', '1')}
143+
${p(DEFAULT_VALUE_KEY, '1')}
144+
`,
115145
required: false,
116146
type: 'number',
117147
})
118148
}
119149

120-
function Search(paginateConfig: PaginateConfig<any>) {
150+
export function Search(paginateConfig: PaginateConfig<any>) {
121151
if (!paginateConfig.searchableColumns) return
122152

123153
return ApiQuery({
124154
name: 'search',
125155
description: `Search term to filter result values
126-
${p('Example', 'John')}
127-
${p(DEFAULT_VALUE_KEY, 'No default value')}
128-
`,
156+
${p('Example', 'John')}
157+
${p(DEFAULT_VALUE_KEY, 'No default value')}
158+
`,
129159
required: false,
130160
type: 'string',
131161
})
132162
}
133163

134-
function SearchBy(paginateConfig: PaginateConfig<any>) {
164+
export function SearchBy(paginateConfig: PaginateConfig<any>) {
135165
if (!paginateConfig.searchableColumns) return
136166

137167
return ApiQuery({
138168
name: 'searchBy',
139169
description: `List of fields to search by term to filter result values
140-
${p(
141-
'Example',
142-
paginateConfig.searchableColumns.slice(0, Math.min(5, paginateConfig.searchableColumns.length)).join(',')
143-
)}
144-
${p(DEFAULT_VALUE_KEY, 'By default all fields mentioned below will be used to search by term')}
145-
${li('Available Fields', paginateConfig.searchableColumns)}
146-
`,
170+
${p(
171+
'Example',
172+
paginateConfig.searchableColumns.slice(0, Math.min(5, paginateConfig.searchableColumns.length)).join(',')
173+
)}
174+
${p(DEFAULT_VALUE_KEY, 'By default all fields mentioned below will be used to search by term')}
175+
${li('Available Fields', paginateConfig.searchableColumns)}
176+
`,
147177
required: false,
148178
isArray: true,
149179
type: 'string',

src/swagger/pagination-docs.spec.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe('PaginatedEndpoint decorator', () => {
7272
required: false,
7373
in: 'query',
7474
description:
75-
'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 ',
75+
'Page number to retrieve. If you provide invalid value the default page number will applied\n\n**Example:** 1\n\n\n**Default Value:** 1\n\n',
7676
schema: {
7777
type: 'number',
7878
},
@@ -82,7 +82,7 @@ describe('PaginatedEndpoint decorator', () => {
8282
required: false,
8383
in: 'query',
8484
description:
85-
'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 ',
85+
'Number of records per page.\n\n\n**Example:** 20\n\n\n\n**Default Value:** 20\n\n\n\n**Max Value:** 100\n\n\nIf provided value is greater than max value, max value will be applied.\n',
8686
schema: {
8787
type: 'number',
8888
},
@@ -92,7 +92,7 @@ describe('PaginatedEndpoint decorator', () => {
9292
required: false,
9393
in: 'query',
9494
description:
95-
'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 ',
95+
'Parameter to sort by.\nTo sort by multiple fields, just provide query param multiple types. The order in url defines an order of sorting\n\n**Format:** {fieldName}:{DIRECTION}\n\n\n**Example:** sortBy=id:DESC\n\n\n**Default Value:** No default sorting specified, the result order is not guaranteed if not provided\n\n**Available Fields**\n- id\n',
9696
schema: {
9797
type: 'array',
9898
items: {
@@ -154,7 +154,7 @@ describe('PaginatedEndpoint decorator', () => {
154154
required: false,
155155
in: 'query',
156156
description:
157-
'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 ',
157+
'Page number to retrieve. If you provide invalid value the default page number will applied\n\n**Example:** 1\n\n\n**Default Value:** 1\n\n',
158158
schema: {
159159
type: 'number',
160160
},
@@ -164,7 +164,7 @@ describe('PaginatedEndpoint decorator', () => {
164164
required: false,
165165
in: 'query',
166166
description:
167-
'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 ',
167+
'Number of records per page.\n\n\n**Example:** 20\n\n\n\n**Default Value:** 20\n\n\n\n**Max Value:** 100\n\n\nIf provided value is greater than max value, max value will be applied.\n',
168168
schema: {
169169
type: 'number',
170170
},
@@ -174,7 +174,7 @@ describe('PaginatedEndpoint decorator', () => {
174174
required: false,
175175
in: 'query',
176176
description:
177-
'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>',
177+
'Filter by id query param.\n\n**Format:** filter.id={$not}:OPERATION:VALUE\n\n\n\n**Example:** filter.id=$btw:John Doe&filter.id=$contains:John Doe\n\n**Available Operations**\n- $eq\n\n- $gt\n\n- $gte\n\n- $in\n\n- $null\n\n- $lt\n\n- $lte\n\n- $btw\n\n- $ilike\n\n- $sw\n\n- $contains\n\n- $not\n\n- $and\n\n- $or',
178178
schema: {
179179
type: 'array',
180180
items: {
@@ -187,7 +187,7 @@ describe('PaginatedEndpoint decorator', () => {
187187
required: false,
188188
in: 'query',
189189
description:
190-
'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>',
190+
'Filter by name query param.\n\n**Format:** filter.name={$not}:OPERATION:VALUE\n\n\n\n**Example:** filter.name=$eq:John Doe\n\n**Available Operations**\n- $eq\n\n- $not\n\n- $and\n\n- $or',
191191
schema: {
192192
type: 'array',
193193
items: {
@@ -200,7 +200,7 @@ describe('PaginatedEndpoint decorator', () => {
200200
required: false,
201201
in: 'query',
202202
description:
203-
'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 ',
203+
'Parameter to sort by.\nTo sort by multiple fields, just provide query param multiple types. The order in url defines an order of sorting\n\n**Format:** {fieldName}:{DIRECTION}\n\n\n**Example:** sortBy=id:DESC\n\n\n**Default Value:** id:DESC\n\n**Available Fields**\n- id\n',
204204
schema: {
205205
type: 'array',
206206
items: {
@@ -214,7 +214,7 @@ describe('PaginatedEndpoint decorator', () => {
214214
required: false,
215215
in: 'query',
216216
description:
217-
'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 ',
217+
'Search term to filter result values\n\n**Example:** John\n\n\n**Default Value:** No default value\n\n',
218218
schema: {
219219
type: 'string',
220220
},
@@ -224,7 +224,7 @@ describe('PaginatedEndpoint decorator', () => {
224224
required: false,
225225
in: 'query',
226226
description:
227-
'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 ',
227+
'List of fields to search by term to filter result values\n\n**Example:** name\n\n\n**Default Value:** By default all fields mentioned below will be used to search by term\n\n**Available Fields**\n- name\n',
228228
schema: {
229229
type: 'array',
230230
items: {
@@ -237,7 +237,7 @@ describe('PaginatedEndpoint decorator', () => {
237237
required: false,
238238
in: 'query',
239239
description:
240-
'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 ',
240+
'List of fields to select.\n\n**Example:** id,name\n\n\n**Default Value:** By default all fields returns. If you want to select only some fields, provide them in query param\n\n',
241241
schema: {
242242
type: 'string',
243243
},

0 commit comments

Comments
 (0)