feat: add swagger

This commit is contained in:
M1000fr 2024-12-02 15:25:36 +01:00
parent b07b6082e4
commit f3c5673c75
7 changed files with 178 additions and 20 deletions

View File

@ -21,6 +21,7 @@
"@nestjs/mapped-types": "^2.0.6", "@nestjs/mapped-types": "^2.0.6",
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.11", "@nestjs/platform-express": "^10.4.11",
"@nestjs/swagger": "^8.0.7",
"@prisma/client": "5.22.0", "@prisma/client": "5.22.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",

View File

@ -1,6 +1,7 @@
import { NestFactory, Reflector } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ClassSerializerInterceptor, ValidationPipe } from "@nestjs/common"; 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() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
@ -11,9 +12,23 @@ async function bootstrap() {
app.useGlobalPipes(new ValidationPipe()); app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector), { app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector), {
excludeExtraneousValues: true, 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"); await app.listen(process.env.PORT ?? 3000, "0.0.0.0");
} }

View File

@ -1,6 +1,8 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString } from "class-validator"; import { IsString } from "class-validator";
export class CreateUserDTO { export class CreateUserDTO {
@IsString() @IsString()
@ApiProperty()
username: string; username: string;
} }

View File

@ -7,6 +7,13 @@ import {
Query, Query,
UseGuards, UseGuards,
} from "@nestjs/common"; } from "@nestjs/common";
import {
ApiBearerAuth,
ApiBody,
ApiOkResponse,
ApiQuery,
ApiUnauthorizedResponse,
} from "@nestjs/swagger";
import { Role } from "@Modules/auth/Decorators/roles.decorator"; import { Role } from "@Modules/auth/Decorators/roles.decorator";
import { JwtAuthGuard } from "@Modules/auth/Guards/jwt.guard"; import { JwtAuthGuard } from "@Modules/auth/Guards/jwt.guard";
@ -21,32 +28,118 @@ import { UserService } from "./user.service";
@Controller("user") @Controller("user")
@UseGuards(RolesGuard) @UseGuards(RolesGuard)
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiUnauthorizedResponse({
description: "Unauthorized",
example: {
message: "Unauthorized",
statusCode: 401,
},
})
export class UserController { export class UserController {
constructor(private readonly userService: UserService) {} constructor(private readonly userService: UserService) {}
@Get() @Get()
@Role("ADMIN") @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<UserEntity[]> { async findAll(): Promise<UserEntity[]> {
const users = await this.userService.findAll(); return await this.userService
.findAll()
return users.map((user) => new UserEntity(user)); .then((users) => users.map((user) => new UserEntity(user)));
} }
@Post() @Post()
@Role("ADMIN") @Role("ADMIN")
create(@Body() createUserInput: CreateUserDTO) { @ApiOkResponse({
return this.userService.create(createUserInput); 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<UserEntity> {
return this.userService
.create(createUserInput)
.then((user) => new UserEntity(user));
} }
@Delete() @Delete()
@Role("ADMIN") @Role("ADMIN")
delete(@Query("id") id: string) { @ApiOkResponse({
return this.userService.delete(id); 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<UserEntity> {
return this.userService.delete(id).then((user) => new UserEntity(user));
} }
@Delete("/bulk") @Delete("/bulk")
@Role("ADMIN") @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); return this.userService.bulkDelete(ids);
} }
} }

View File

@ -1,14 +1,19 @@
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
import { $Enums } from "@prisma/client"; import { $Enums } from "@prisma/client";
import { Expose } from "class-transformer"; import { Expose } from "class-transformer";
@ApiSchema({ name: "User" })
export class UserEntity { export class UserEntity {
@Expose() @Expose()
@ApiProperty()
id: string; id: string;
@Expose() @Expose()
@ApiProperty()
username: string; username: string;
@Expose() @Expose()
@ApiProperty()
role: $Enums.Role role: $Enums.Role
constructor(partial: Partial<UserEntity>) { constructor(partial: Partial<UserEntity>) {

View File

@ -3,13 +3,26 @@ import { PrismaService } from "@Modules/prisma/prisma.service";
import { CreateUserDTO } from "./dto/create-user.dto"; import { CreateUserDTO } from "./dto/create-user.dto";
import { UpdateUserDTO } from "./dto/update-user.dto"; import { UpdateUserDTO } from "./dto/update-user.dto";
import { Prisma } from "@prisma/client";
@Injectable() @Injectable()
export class UserService { export class UserService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
async findAll() { async findAll({
return await this.prisma.user.findMany(); 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) { async findById(id: string) {
@ -51,6 +64,6 @@ export class UserService {
in: ids, in: ids,
}, },
}, },
}) });
} }
} }

View File

@ -233,6 +233,11 @@
resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe"
integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== 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": "@nestjs/cli@^10.0.0":
version "10.4.8" version "10.4.8"
resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.8.tgz#e6dec4eeda8a125918cbf1c0d10773ac6bb6d40e" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.8.tgz#e6dec4eeda8a125918cbf1c0d10773ac6bb6d40e"
@ -296,7 +301,7 @@
"@types/jsonwebtoken" "9.0.5" "@types/jsonwebtoken" "9.0.5"
jsonwebtoken "9.0.2" 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" version "2.0.6"
resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz#d2d8523709fd5d872a9b9e0c38162746e2a7f44e" resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz#d2d8523709fd5d872a9b9e0c38162746e2a7f44e"
integrity sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ== integrity sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==
@ -328,6 +333,18 @@
jsonc-parser "3.3.1" jsonc-parser "3.3.1"
pluralize "8.0.0" 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": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -409,6 +426,11 @@
dependencies: dependencies:
"@prisma/debug" "5.22.0" "@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": "@sideway/address@^4.1.5":
version "4.1.5" version "4.1.5"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" 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" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^4.1.0: js-yaml@4.1.0, js-yaml@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
@ -3072,6 +3094,13 @@ supports-color@^8.0.0:
dependencies: dependencies:
has-flag "^4.0.0" 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: symbol-observable@4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"