From 5c6aa6e04523365a83e55b57b368567451a2fdb6 Mon Sep 17 00:00:00 2001 From: Seungil Kim Date: Tue, 5 Sep 2023 21:29:51 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Feat:=20collection=20crud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # --- src/app.module.ts | 2 + src/collection/collection.controller.ts | 66 ++++++++++ src/collection/collection.module.ts | 11 ++ src/collection/collection.service.ts | 122 ++++++++++++++++++ .../dto/request/create-collection.dto.ts | 19 +++ src/collection/dto/response/collection.dto.ts | 21 +++ 6 files changed, 241 insertions(+) create mode 100644 src/collection/collection.controller.ts create mode 100644 src/collection/collection.module.ts create mode 100644 src/collection/collection.service.ts create mode 100644 src/collection/dto/request/create-collection.dto.ts create mode 100644 src/collection/dto/response/collection.dto.ts diff --git a/src/app.module.ts b/src/app.module.ts index e112a67..bcd6150 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { PrismaModule } from './prisma/prisma.module'; import { PrismaService } from './prisma/prisma.service'; import { UserModule } from './user/user.module'; import { TagModule } from './tag/tag.module'; +import { CollectionModule } from './collection/collection.module'; @Module({ imports: [ @@ -17,6 +18,7 @@ import { TagModule } from './tag/tag.module'; DocumentModule, UserModule, TagModule, + CollectionModule, ], controllers: [AppController], providers: [AppService, PrismaService], diff --git a/src/collection/collection.controller.ts b/src/collection/collection.controller.ts new file mode 100644 index 0000000..324500d --- /dev/null +++ b/src/collection/collection.controller.ts @@ -0,0 +1,66 @@ +import { + Body, + Controller, + DefaultValuePipe, + Get, + Param, + ParseIntPipe, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { CollectionService } from './collection.service'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; +import { GetUid } from '../decorators/get-uid.decorator'; +import { CollectionDto } from './dto/response/collection.dto'; +import { CreateCollectionDto } from './dto/request/create-collection.dto'; + +@Controller('collection') +@UseGuards(AuthGuard('jwt')) +@ApiBearerAuth() +export class CollectionController { + constructor(private readonly collectionService: CollectionService) {} + + @Get() + @ApiQuery({ + name: 'limit', + required: false, + type: Number, + description: '한 번에 받을 태그의 개수. 최대 20개까지 가능. 기본값은 20.', + }) + @ApiQuery({ + name: 'offset', + required: false, + type: Number, + description: '몇 번째 태그부터 검색 결과에 포함할지. 기본값은 0.', + }) + getCollections( + @GetUid() uid: string, + @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number, + @Query('offset', new DefaultValuePipe(0), ParseIntPipe) offset: number, + ): Promise { + return this.collectionService.findAll(uid, limit, offset); + } + + @Post() + createCollection( + @GetUid() uid: string, + @Body() createCollectionDto: CreateCollectionDto, + ): Promise { + return this.collectionService.create(uid, createCollectionDto); + } + + @Get('find/:name') + findByName( + @GetUid() uid: string, + @Param('name') name: string, + ): Promise { + return this.collectionService.findOne(uid, name); + } + + @Post('delete/:name') + delete(@GetUid() uid: string, @Param('name') name: string): Promise { + return this.collectionService.deleteOne(uid, name); + } +} diff --git a/src/collection/collection.module.ts b/src/collection/collection.module.ts new file mode 100644 index 0000000..2dbc1f1 --- /dev/null +++ b/src/collection/collection.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { CollectionService } from './collection.service'; +import { CollectionController } from './collection.controller'; +import { UserModule } from '../user/user.module'; + +@Module({ + imports: [UserModule], + controllers: [CollectionController], + providers: [CollectionService], +}) +export class CollectionModule {} diff --git a/src/collection/collection.service.ts b/src/collection/collection.service.ts new file mode 100644 index 0000000..2aa30f5 --- /dev/null +++ b/src/collection/collection.service.ts @@ -0,0 +1,122 @@ +import { + BadRequestException, + Injectable, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { CollectionDto } from './dto/response/collection.dto'; +import { UserService } from '../user/user.service'; +import { CreateCollectionDto } from './dto/request/create-collection.dto'; + +@Injectable() +export class CollectionService { + private readonly logger: Logger = new Logger(CollectionService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly userService: UserService, + ) {} + + async findAll( + uid: string, + limit: number, + offset: number, + ): Promise { + limit = limit > 20 ? 20 : limit; + const collections = await this.prisma.collection.findMany({ + include: { + _count: { select: { documents: true } }, + }, + where: { user: { uid } }, + orderBy: [{ documents: { _count: 'desc' } }, { name: 'asc' }], + take: limit, + skip: offset, + }); + + this.logger.log(`getTags: ${JSON.stringify(collections)}`); + + return collections.map((collection) => ({ + name: collection.name, + description: collection.description, + count: collection._count.documents, + })); + } + + async create( + uid: string, + createCollectionDto: CreateCollectionDto, + ): Promise { + const user = await this.userService.findByUid(uid); + + const documents = await this.prisma.document.findMany({ + select: { id: true }, + where: { + userId: user.id, + docId: { in: createCollectionDto.docIds ?? [] }, + }, + }); + + // TODO: CreateCollectionDto에 docIds가 너무 많을 경우 처리 + + try { + const collection = await this.prisma.collection.create({ + data: { + name: createCollectionDto.name, + description: createCollectionDto.description, + user: { connect: { id: user.id } }, + documents: { + connect: documents.map((doc) => ({ id: doc.id })), + }, + }, + }); + + return { + name: collection.name, + description: collection.description, + count: documents.length, + }; + } catch (error) { + if (error.code === 'P2002') { + // "Unique constraint failed on the {constraint}" 에러 + throw new BadRequestException('Collection already exists'); + } + } + } + + async deleteOne(uid: string, name: string): Promise { + const user = await this.userService.findByUid(uid); + const collection = await this.prisma.collection.findUnique({ + where: { userId_name: { userId: user.id, name } }, + include: { user: true }, + }); + + if (!collection) { + throw new BadRequestException('Collection does not exist'); + } + + if (collection.user.uid !== uid) { + throw new UnauthorizedException('Unauthorized'); + } + + await this.prisma.collection.delete({ where: { id: collection.id } }); + } + + async findOne(uid: string, name: string) { + const user = await this.userService.findByUid(uid); + const collection = await this.prisma.collection.findUnique({ + where: { userId_name: { userId: user.id, name } }, + include: { _count: { select: { documents: true } } }, + }); + + if (!collection) { + throw new BadRequestException('Collection does not exist'); + } + + return { + name: collection.name, + description: collection.description, + count: collection._count.documents, + }; + } +} diff --git a/src/collection/dto/request/create-collection.dto.ts b/src/collection/dto/request/create-collection.dto.ts new file mode 100644 index 0000000..11a68ef --- /dev/null +++ b/src/collection/dto/request/create-collection.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateCollectionDto { + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + description: string; + + @IsOptional() + @IsString({ each: true }) + @ApiProperty({ + description: '컬렉션에 속한 문서의 uuid', + required: false, + }) + docIds?: string[]; +} diff --git a/src/collection/dto/response/collection.dto.ts b/src/collection/dto/response/collection.dto.ts new file mode 100644 index 0000000..c9c542c --- /dev/null +++ b/src/collection/dto/response/collection.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CollectionDto { + @ApiProperty({ + description: '컬렉션 이름', + example: '백엔드', + }) + name: string; + + @ApiProperty({ + description: '컬렉션 설명', + example: '백엔드 개발과 관련된 블로그 글 모음', + }) + description: string; + + @ApiProperty({ + description: '컬렉션에 속한 문서의 개수', + example: 1, + }) + count: number; +}