From f3c5673c75208d88a2413b30f1a181314cf28aef Mon Sep 17 00:00:00 2001 From: M1000fr Date: Mon, 2 Dec 2024 15:25:36 +0100 Subject: [PATCH] feat: add swagger --- package.json | 1 + src/main.ts | 25 ++++-- src/modules/user/dto/create-user.dto.ts | 2 + src/modules/user/user.controller.ts | 113 +++++++++++++++++++++--- src/modules/user/user.entity.ts | 5 ++ src/modules/user/user.service.ts | 19 +++- yarn.lock | 33 ++++++- 7 files changed, 178 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 43800a5..da1d85d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@nestjs/mapped-types": "^2.0.6", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.11", + "@nestjs/swagger": "^8.0.7", "@prisma/client": "5.22.0", "axios": "^1.7.7", "class-transformer": "^0.5.1", diff --git a/src/main.ts b/src/main.ts index 7009b44..fd98f36 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ -import { NestFactory, Reflector } from "@nestjs/core"; -import { AppModule } from "./app.module"; import { ClassSerializerInterceptor, ValidationPipe } from "@nestjs/common"; +import { NestFactory, Reflector } from "@nestjs/core"; +import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; +import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -11,9 +12,23 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); - app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector), { - excludeExtraneousValues: true, - })); + app.useGlobalInterceptors( + new ClassSerializerInterceptor(app.get(Reflector), { + excludeExtraneousValues: true, + }), + ); + + const config = new DocumentBuilder() + .setTitle("Toogether API") + .addBearerAuth() + .build(); + const documentFactory = () => SwaggerModule.createDocument(app, config); + SwaggerModule.setup("documentation", app, documentFactory, { + swaggerOptions: { + tryItOutEnabled: true, + persistAuthorization: true, + } + }); await app.listen(process.env.PORT ?? 3000, "0.0.0.0"); } diff --git a/src/modules/user/dto/create-user.dto.ts b/src/modules/user/dto/create-user.dto.ts index 201604c..e21cbd1 100644 --- a/src/modules/user/dto/create-user.dto.ts +++ b/src/modules/user/dto/create-user.dto.ts @@ -1,6 +1,8 @@ +import { ApiProperty } from "@nestjs/swagger"; import { IsString } from "class-validator"; export class CreateUserDTO { @IsString() + @ApiProperty() username: string; } diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index c282dcb..943eff6 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -7,6 +7,13 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { + ApiBearerAuth, + ApiBody, + ApiOkResponse, + ApiQuery, + ApiUnauthorizedResponse, +} from "@nestjs/swagger"; import { Role } from "@Modules/auth/Decorators/roles.decorator"; import { JwtAuthGuard } from "@Modules/auth/Guards/jwt.guard"; @@ -21,32 +28,118 @@ import { UserService } from "./user.service"; @Controller("user") @UseGuards(RolesGuard) @UseGuards(JwtAuthGuard) +@ApiBearerAuth() +@ApiUnauthorizedResponse({ + description: "Unauthorized", + example: { + message: "Unauthorized", + statusCode: 401, + }, +}) export class UserController { constructor(private readonly userService: UserService) {} - + @Get() @Role("ADMIN") + @ApiOkResponse({ + type: UserEntity, + isArray: true, + examples: { + example: { + summary: "A list of users", + value: [ + { id: "1", role: "ADMIN", username: "admin" }, + { id: "2", role: "STUDENT", username: "student" }, + ] as UserEntity[], + }, + }, + }) async findAll(): Promise { - const users = await this.userService.findAll(); - - return users.map((user) => new UserEntity(user)); + return await this.userService + .findAll() + .then((users) => users.map((user) => new UserEntity(user))); } - + @Post() @Role("ADMIN") - create(@Body() createUserInput: CreateUserDTO) { - return this.userService.create(createUserInput); + @ApiOkResponse({ + type: UserEntity, + description: "The user has been successfully created.", + examples: { + example: { + summary: "A user example", + value: { + id: "1", + role: "ADMIN", + username: "admin", + } as UserEntity, + }, + }, + }) + @ApiBody({ + type: CreateUserDTO, + examples: { + example: { + summary: "A user example", + value: { username: "admin" }, + }, + }, + }) + async create(@Body() createUserInput: CreateUserDTO): Promise { + return this.userService + .create(createUserInput) + .then((user) => new UserEntity(user)); } @Delete() @Role("ADMIN") - delete(@Query("id") id: string) { - return this.userService.delete(id); + @ApiOkResponse({ + type: UserEntity, + description: "The user has been successfully deleted.", + examples: { + example: { + summary: "A user example", + value: { + id: "1", + role: "ADMIN", + username: "admin", + } as UserEntity, + }, + }, + }) + @ApiQuery({ + name: "id", + type: String, + description: "The user id", + example: "1", + }) + async delete(@Query("id") id: string): Promise { + return this.userService.delete(id).then((user) => new UserEntity(user)); } @Delete("/bulk") @Role("ADMIN") - bulkDelete(@Body() { ids }: BulkDeleteUserDTO) { + @ApiOkResponse({ + description: "The users have been successfully deleted.", + examples: { + example: { + summary: "A count of deleted users", + value: { count: 2 }, + }, + }, + }) + @ApiBody({ + type: BulkDeleteUserDTO, + examples: { + example: { + summary: "A list of user ids", + value: { ids: ["1", "2"] }, + }, + }, + }) + bulkDelete(@Body() { ids }: BulkDeleteUserDTO): Promise<{ + count: number; + }> { return this.userService.bulkDelete(ids); } } diff --git a/src/modules/user/user.entity.ts b/src/modules/user/user.entity.ts index be0cef9..7068146 100644 --- a/src/modules/user/user.entity.ts +++ b/src/modules/user/user.entity.ts @@ -1,14 +1,19 @@ +import { ApiProperty, ApiSchema } from "@nestjs/swagger"; import { $Enums } from "@prisma/client"; import { Expose } from "class-transformer"; +@ApiSchema({ name: "User" }) export class UserEntity { @Expose() + @ApiProperty() id: string; @Expose() + @ApiProperty() username: string; @Expose() + @ApiProperty() role: $Enums.Role constructor(partial: Partial) { diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 9c0f24a..f50cb95 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -3,13 +3,26 @@ import { PrismaService } from "@Modules/prisma/prisma.service"; import { CreateUserDTO } from "./dto/create-user.dto"; import { UpdateUserDTO } from "./dto/update-user.dto"; +import { Prisma } from "@prisma/client"; @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} - async findAll() { - return await this.prisma.user.findMany(); + async findAll({ + include, + cursor, + take, + }: { + include?: Prisma.UserInclude; + cursor?: string; + take?: number; + } = {}) { + return await this.prisma.user.findMany({ + include, + cursor: cursor ? { id: cursor } : undefined, + take: take || 10, + }); } async findById(id: string) { @@ -51,6 +64,6 @@ export class UserService { in: ids, }, }, - }) + }); } } diff --git a/yarn.lock b/yarn.lock index ab71305..234434a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -233,6 +233,11 @@ resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== +"@microsoft/tsdoc@^0.15.0": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz#d4f6937353bc4568292654efb0a0e0532adbcba2" + integrity sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw== + "@nestjs/cli@^10.0.0": version "10.4.8" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.8.tgz#e6dec4eeda8a125918cbf1c0d10773ac6bb6d40e" @@ -296,7 +301,7 @@ "@types/jsonwebtoken" "9.0.5" jsonwebtoken "9.0.2" -"@nestjs/mapped-types@^2.0.6": +"@nestjs/mapped-types@2.0.6", "@nestjs/mapped-types@^2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz#d2d8523709fd5d872a9b9e0c38162746e2a7f44e" integrity sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ== @@ -328,6 +333,18 @@ jsonc-parser "3.3.1" pluralize "8.0.0" +"@nestjs/swagger@^8.0.7": + version "8.0.7" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-8.0.7.tgz#7347fcd919a413a001838732f1b1966ad19df094" + integrity sha512-zaTMCEZ/CxX7QYF110nTqJsn7eCXp4VI9kv7+AdUcIlBmhhgJpggBw2Mx2p6xVjyz1EoWXGfxxWKnxEyaQwFlg== + dependencies: + "@microsoft/tsdoc" "^0.15.0" + "@nestjs/mapped-types" "2.0.6" + js-yaml "4.1.0" + lodash "4.17.21" + path-to-regexp "3.3.0" + swagger-ui-dist "5.18.2" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -409,6 +426,11 @@ dependencies: "@prisma/debug" "5.22.0" +"@scarf/scarf@=1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" + integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" @@ -2125,7 +2147,7 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^4.1.0: +js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -3072,6 +3094,13 @@ supports-color@^8.0.0: dependencies: has-flag "^4.0.0" +swagger-ui-dist@5.18.2: + version "5.18.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz#62013074374d272c04ed3030704b88db5aa8c0b7" + integrity sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw== + dependencies: + "@scarf/scarf" "=1.4.0" + symbol-observable@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"