From a8df59b6cff8a4799220de7e157f305d6e088821 Mon Sep 17 00:00:00 2001 From: Tamir Gershberg <47638346+tamirGer@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:00:09 +0300 Subject: [PATCH] feat(swagger): add examples to all api params (#280) --- src/app.controller.ts | 16 ++++ src/auth/api/login.request.ts | 5 +- src/file/file.controller.ts | 91 ++++++++++++++++--- src/products/api/ProductDto.ts | 14 ++- src/products/products.controller.ts | 2 + src/subscriptions/subscriptions.controller.ts | 12 ++- src/testimonials/api/TestimonialDto.ts | 6 +- src/testimonials/testimonials.controller.ts | 10 ++ src/users/api/UserDto.ts | 13 ++- src/users/users.controller.ts | 17 ++++ 10 files changed, 156 insertions(+), 30 deletions(-) diff --git a/src/app.controller.ts b/src/app.controller.ts index f1cc5894..29514c5f 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -25,6 +25,7 @@ import { ApiOkResponse, ApiOperation, ApiProduces, + ApiQuery, ApiTags, } from '@nestjs/swagger'; import * as dotT from 'dot'; @@ -73,6 +74,7 @@ export class AppController { } @Get('goto') + @ApiQuery({ name: 'url', example: 'https://google.com', required: true }) @ApiOperation({ description: API_DESC_REDIRECT_REQUEST, }) @@ -85,6 +87,17 @@ export class AppController { } @Post('metadata') + @ApiProduces('text/plain') + @ApiConsumes('text/plain') + @ApiBody({ + type: String, + examples: { + xml_doc: { + summary: 'XML doc', + value: ``, + }, + }, + }) @ApiOperation({ description: API_DESC_XML_METADATA, }) @@ -120,6 +133,7 @@ export class AppController { } @Get('spawn') + @ApiQuery({ name: 'command', example: 'ls -la', required: true }) @ApiOperation({ description: API_DESC_LAUNCH_COMMAND, }) @@ -191,6 +205,7 @@ export class AppController { } @Get('/v1/userinfo/:email') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseInterceptors(ClassSerializerInterceptor) @SerializeOptions({ groups: [BASIC_USER_INFO] }) @ApiOperation({ @@ -219,6 +234,7 @@ export class AppController { } @Get('/v2/userinfo/:email') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @UseInterceptors(ClassSerializerInterceptor) diff --git a/src/auth/api/login.request.ts b/src/auth/api/login.request.ts index 314f35a7..cf45ad27 100644 --- a/src/auth/api/login.request.ts +++ b/src/auth/api/login.request.ts @@ -9,14 +9,15 @@ export enum FormMode { } export class LoginRequest { - @ApiProperty() + @ApiProperty({ example: 'john', required: true }) user: string; - @ApiProperty() + @ApiProperty({ example: 'Pa55w0rd', required: true }) password: string; @ApiProperty({ enum: FormMode, + required: true, }) op?: string; diff --git a/src/file/file.controller.ts b/src/file/file.controller.ts index 9ca0cb04..d63e3622 100644 --- a/src/file/file.controller.ts +++ b/src/file/file.controller.ts @@ -12,10 +12,12 @@ import { Res, } from '@nestjs/common'; import { + ApiHeader, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, } from '@nestjs/swagger'; import { W_OK } from 'constants'; @@ -49,17 +51,24 @@ export class FileController { } } - private async loadCPFile(cpBaseUrl: string, path: string){ - if (!path.startsWith(cpBaseUrl)){ - throw new BadRequestException(`Invalid paramater 'path' ${path}`) + private async loadCPFile(cpBaseUrl: string, path: string) { + if (!path.startsWith(cpBaseUrl)) { + throw new BadRequestException(`Invalid paramater 'path' ${path}`); } - + const file: Stream = await this.fileService.getFile(path); return file; } @Get() + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -81,15 +90,21 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.fileService.getFile(path); - const type = this.getContentType(contentType, acceptHeader) + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Get('/google') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -111,14 +126,24 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.loadCPFile(CloudProvidersMetaData.GOOGLE, path); - const type = this.getContentType(contentType, acceptHeader) + const file: Stream = await this.loadCPFile( + CloudProvidersMetaData.GOOGLE, + path, + ); + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Get('/aws') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -140,14 +165,24 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.loadCPFile(CloudProvidersMetaData.AWS, path); - const type = this.getContentType(contentType, acceptHeader) + const file: Stream = await this.loadCPFile( + CloudProvidersMetaData.AWS, + path, + ); + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Get('/azure') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -169,14 +204,24 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.loadCPFile(CloudProvidersMetaData.AZURE, path); - const type = this.getContentType(contentType, acceptHeader) + const file: Stream = await this.loadCPFile( + CloudProvidersMetaData.AZURE, + path, + ); + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Get('/digital_ocean') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -198,14 +243,22 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.loadCPFile(CloudProvidersMetaData.DIGITAL_OCEAN, path); - const type = this.getContentType(contentType, acceptHeader) + const file: Stream = await this.loadCPFile( + CloudProvidersMetaData.DIGITAL_OCEAN, + path, + ); + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Delete() + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/some_file.jpg', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_DELETE_FILE, }) @@ -226,6 +279,11 @@ export class FileController { } @Put('raw') + @ApiQuery({ + name: 'path', + example: 'some/path/to/file.png', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_SAVE_RAW_CONTENT, }) @@ -247,6 +305,11 @@ export class FileController { } @Get('raw') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_READ_FILE_ON_SERVER, }) diff --git a/src/products/api/ProductDto.ts b/src/products/api/ProductDto.ts index db459399..d5dc4ba4 100644 --- a/src/products/api/ProductDto.ts +++ b/src/products/api/ProductDto.ts @@ -1,19 +1,23 @@ import { ApiProperty } from '@nestjs/swagger'; export class ProductDto { - @ApiProperty() + @ApiProperty({ example: 'Amethyst', required: true }) name: string; - @ApiProperty() + @ApiProperty({ example: 'Healing', required: true }) category: string; - @ApiProperty() + @ApiProperty({ + default: + '/api/file?path=config/products/crystals/amethyst.jpg&type=image/jpg', + required: true, + }) photoUrl: string; - @ApiProperty() + @ApiProperty({ example: 'a violet variety of quartz', required: true }) description: string; - @ApiProperty() + @ApiProperty({ example: 1, required: true }) viewsCount: number; constructor(params: { diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index b95c7bfe..04fa8c66 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -13,6 +13,7 @@ import { ApiTags, ApiForbiddenResponse, ApiInternalServerErrorResponse, + ApiHeader, } from '@nestjs/swagger'; import { AuthGuard } from '../auth/auth.guard'; import { JwtProcessorType } from '../auth/auth.service'; @@ -74,6 +75,7 @@ export class ProductsController { } @Get('views') + @ApiHeader({ name: 'x-product-name', example: 'Amethyst' }) @ApiOperation({ description: API_DESC_GET_VIEW_PRODUCT, }) diff --git a/src/subscriptions/subscriptions.controller.ts b/src/subscriptions/subscriptions.controller.ts index 8269ad4e..24e68312 100644 --- a/src/subscriptions/subscriptions.controller.ts +++ b/src/subscriptions/subscriptions.controller.ts @@ -1,5 +1,10 @@ import { Controller, Logger, Post, Query } from '@nestjs/common'; -import { ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + ApiCreatedResponse, + ApiOperation, + ApiQuery, + ApiTags, +} from '@nestjs/swagger'; import { SWAGGER_DESC_CREATE_SUBSCRIPTION } from './subscriptions.controller.swagger.desc'; @Controller('/api/subscriptions') @@ -8,6 +13,11 @@ export class SubscriptionsController { private readonly logger = new Logger(SubscriptionsController.name); @Post() + @ApiQuery({ + name: 'email', + example: 'john.doe@example.com', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_CREATE_SUBSCRIPTION, }) diff --git a/src/testimonials/api/TestimonialDto.ts b/src/testimonials/api/TestimonialDto.ts index 89d3cf73..981312c7 100644 --- a/src/testimonials/api/TestimonialDto.ts +++ b/src/testimonials/api/TestimonialDto.ts @@ -2,13 +2,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { Testimonial } from '../../model/testimonial.entity'; export class TestimonialDto { - @ApiProperty() + @ApiProperty({ example: 'John', required: true }) name: string; - @ApiProperty() + @ApiProperty({ example: 'Doe', required: true }) title: string; - @ApiProperty() + @ApiProperty({ example: "I've broken all the crystals", required: true }) message: string; public static covertToApi(t: Testimonial): TestimonialDto { diff --git a/src/testimonials/testimonials.controller.ts b/src/testimonials/testimonials.controller.ts index f28f7ea2..90749ce0 100644 --- a/src/testimonials/testimonials.controller.ts +++ b/src/testimonials/testimonials.controller.ts @@ -9,9 +9,11 @@ import { UseGuards, } from '@nestjs/common'; import { + ApiBody, ApiForbiddenResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, } from '@nestjs/swagger'; import { AuthGuard } from '../auth/auth.guard'; @@ -34,6 +36,9 @@ export class TestimonialsController { constructor(private readonly testimonialsService: TestimonialsService) {} @Post() + @ApiBody({ + type: TestimonialDto, + }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -81,6 +86,11 @@ export class TestimonialsController { } @Get('count') + @ApiQuery({ + name: 'query', + example: 'select count(*) as count from testimonial', + required: true, + }) @Header('content-type', 'text/html') @ApiOperation({ description: API_DESC_GET_TESTIMONIALS_ON_SQL_QUERY, diff --git a/src/users/api/UserDto.ts b/src/users/api/UserDto.ts index f01e3f79..1d7fd74c 100644 --- a/src/users/api/UserDto.ts +++ b/src/users/api/UserDto.ts @@ -6,29 +6,31 @@ export const FULL_USER_INFO = 'fullUserInfo'; export class UserDto { @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 'john.doe@examle.com', required: true }) email: string; @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 'John', required: true }) firstName: string; @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 'Doe', required: true }) lastName: string; @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 'Bright Security', required: true }) company: string; @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 1 }) id: number; @Expose({ groups: [FULL_USER_INFO] }) + @ApiProperty({ example: '4263982640269299' }) cardNumber: string; @Expose({ groups: [FULL_USER_INFO] }) + @ApiProperty({ example: '12065550100' }) phoneNumber: string; @Exclude() @@ -37,6 +39,7 @@ export class UserDto { @Exclude() @ApiHideProperty() + @ApiProperty({ example: 'Pa55w0rd' }) password?: string; @Exclude() diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index d0f103e1..6bea6e75 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -31,6 +31,7 @@ import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; @@ -86,6 +87,7 @@ export class UsersController { } @Get('/one/:email') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @SerializeOptions({ groups: [BASIC_USER_INFO] }) @ApiOperation({ description: SWAGGER_DESC_FIND_USER, @@ -114,6 +116,7 @@ export class UsersController { } @Get('/id/:id') + @ApiQuery({ name: 'id', example: 1, required: true }) @SerializeOptions({ groups: [BASIC_USER_INFO] }) @ApiOperation({ description: SWAGGER_DESC_FIND_USER, @@ -142,6 +145,7 @@ export class UsersController { } @Get('/fullinfo/:email') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @SerializeOptions({ groups: [FULL_USER_INFO] }) @ApiOperation({ description: SWAGGER_DESC_FIND_FULL_USER_INFO, @@ -170,6 +174,7 @@ export class UsersController { } @Get('/search/:name') + @ApiQuery({ name: 'name', example: 'john', required: true }) @SerializeOptions({ groups: [FULL_USER_INFO] }) @ApiOperation({ description: SWAGGER_DESC_FIND_USERS, @@ -189,6 +194,7 @@ export class UsersController { } @Get('/one/:email/photo') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -232,6 +238,7 @@ export class UsersController { } @Delete('/one/:id/photo') + @ApiQuery({ name: 'id', example: 1, required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -273,6 +280,12 @@ export class UsersController { } @Get('/ldap') + @ApiQuery({ + name: 'query', + example: + '(&(objectClass=person)(objectClass=user)(email=john.doe@example.com))', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_LDAP_SEARCH, }) @@ -385,6 +398,7 @@ export class UsersController { } @Put('/one/:email/info') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -427,6 +441,7 @@ export class UsersController { } @Get('/one/:email/info') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -470,6 +485,7 @@ export class UsersController { } @Get('/one/:email/adminpermission') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard, AdminGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -493,6 +509,7 @@ export class UsersController { } @Put('/one/:email/photo') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({