From 8467faa0107084d3c7fc9d02869b5896a3f18049 Mon Sep 17 00:00:00 2001 From: scarf Date: Sun, 13 Aug 2023 22:11:03 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20`v2/reviews`=EC=97=90=20kysely=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/kysely/shared.ts | 22 +++++ backend/src/v2/reviews/repository.ts | 140 +++++++++++++++++++++------ backend/src/v2/reviews/service.ts | 57 ++++++----- 3 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 backend/src/kysely/shared.ts diff --git a/backend/src/kysely/shared.ts b/backend/src/kysely/shared.ts new file mode 100644 index 00000000..0ff8ad05 --- /dev/null +++ b/backend/src/kysely/shared.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +/** 반환값이 조건을 만족하지 않으면 오류 throw */ +const throwIf = (value: T, ok: (v: T) => boolean) => { + if (ok(value)) { + return value; + } + throw new Error(`값이 예상과 달리 ${value}입니다`); +}; + +export type Visibility = 'public' | 'private' | 'all' +const roles = ['user', 'cadet', 'librarian', 'staff'] as const; +export type Role = typeof roles[number] + +const fromEnum = (role: number): Role => + throwIf(roles[role], (v) => v === undefined); + +export const toRole = (role: Role): number => + throwIf(roles.indexOf(role), (v) => v === -1); + +export const roleSchema = z.number().int().min(0).max(3) + .transform(fromEnum); diff --git a/backend/src/v2/reviews/repository.ts b/backend/src/v2/reviews/repository.ts index 0c828323..40f9fa13 100644 --- a/backend/src/v2/reviews/repository.ts +++ b/backend/src/v2/reviews/repository.ts @@ -1,36 +1,122 @@ -import jipDataSource from '~/app-data-source'; -import { BookInfo, Reviews } from '~/entity/entities'; +import { match } from 'ts-pattern'; +import { db } from '~/kysely/mod.ts'; +import { executeWithOffsetPagination } from 'kysely-paginate'; +import { Visibility } from '~/kysely/shared.js'; +import { SqlBool } from 'kysely'; -export const reviewsRepo = jipDataSource.getRepository(Reviews); -export const bookInfoRepo = jipDataSource.getRepository(BookInfo); +export const bookInfoExistsById = (id: number) => + db.selectFrom('book_info').where('id', '=', id).executeTakeFirst(); -type ToggleReviewArgs = { +export const getReviewById = (id: number) => + db + .selectFrom('reviews') + .where('id', '=', id) + .select(['userId', 'isDeleted', 'disabled', 'disabledUserId']) + .executeTakeFirst(); + +type SearchOption = { + query: string; + page: number; + perPage: number; + visibility: Visibility; + sort: 'asc' | 'desc'; +}; + +const queryReviews = () => + db + .selectFrom('reviews') + .leftJoin('user', 'user.id', 'reviews.userId') + .leftJoin('book_info', 'book_info.id', 'reviews.bookInfoId') + .select([ + 'id', + 'userId', + 'bookInfoId', + 'content', + 'createdAt', + 'book_info.title', + 'user.nickname', + 'user.intraId', + ]); + +export const searchReviews = ({ + query, + sort, + visibility, + page, + perPage, +}: SearchOption) => { + const searchQuery = queryReviews() + .where('content', 'like', `%${query}%`) + .orderBy('updatedAt', sort); + + const withVisibility = match(visibility) + .with('public', () => searchQuery.where('disabled', '=', false)) + .with('private', () => searchQuery.where('disabled', '=', true)) + .with('all', () => searchQuery) + .exhaustive(); + + return executeWithOffsetPagination(withVisibility, { page, perPage }); +}; + +type InsertOption = { + userId: number; + bookInfoId: number; + content: string; +}; +export const insertReview = ({ userId, bookInfoId, content }: InsertOption) => + db + .insertInto('reviews') + .values({ + userId, + updateUserId: userId, + bookInfoId, + content, + disabled: false, + isDeleted: false, + createdAt: new Date(), + }) + .executeTakeFirst(); + +type DeleteOption = { + reviewsId: number; + deleteUserId: number; +}; +export const deleteReviewById = ({ reviewsId, deleteUserId }: DeleteOption) => + db + .updateTable('reviews') + .where('id', '=', reviewsId) + .set({ deleteUserId, isDeleted: true }) + .executeTakeFirst(); + +type ToggleVisibilityOption = { reviewsId: number; userId: number; - disabled: boolean; + disabled: SqlBool; }; -export const toggleReviewVisibilityById = async ({ +export const toggleVisibilityById = ({ reviewsId, userId, disabled, -}: ToggleReviewArgs) => - reviewsRepo.update(reviewsId, { - disabled: !disabled, - disabledUserId: disabled ? null : userId, - }); - -export const findDisabledReviewById = ({ - id, -}: { - id: number; -}): Promise | null> => - reviewsRepo.findOne({ - select: { disabled: true, disabledUserId: true }, - where: { id }, - }); -type RemoveReviewById = { reviewsId: number; deleteUserId: number }; -export const removeReviewById = async ({ +}: ToggleVisibilityOption) => + db + .updateTable('reviews') + .where('id', '=', reviewsId) + .set({ disabled: !disabled, disabledUserId: userId }) + .executeTakeFirst(); + +type UpdateOption = { + reviewsId: number; + userId: number; + content: string; +}; + +export const updateReviewById = ({ reviewsId, - deleteUserId, -}: RemoveReviewById) => - reviewsRepo.update(reviewsId, { deleteUserId, isDeleted: true }); + userId, + content, +}: UpdateOption) => + db + .updateTable('reviews') + .where('id', '=', reviewsId) + .set({ content, updateUserId: userId }) + .executeTakeFirst(); diff --git a/backend/src/v2/reviews/service.ts b/backend/src/v2/reviews/service.ts index af58e006..34cbb514 100644 --- a/backend/src/v2/reviews/service.ts +++ b/backend/src/v2/reviews/service.ts @@ -8,37 +8,36 @@ import { } from './errors'; import { ParsedUser } from '~/v2/shared'; import { - bookInfoRepo, - findDisabledReviewById, - reviewsRepo, - toggleReviewVisibilityById, + bookInfoExistsById, + deleteReviewById, + getReviewById, + insertReview, + toggleVisibilityById, + updateReviewById, } from './repository'; -import { removeReviewById } from './repository'; type CreateArgs = { bookInfoId: number; userId: number; content: string }; -export const createReview = async ({ - bookInfoId, - userId, - content, -}: CreateArgs) => { - const bookInfo = await bookInfoRepo.findOneBy({ id: bookInfoId }); +export const createReview = async (args: CreateArgs) => { + const bookInfo = await bookInfoExistsById(args.bookInfoId); - return match(bookInfo) - .with(null, () => new BookInfoNotFoundError(bookInfoId)) - .otherwise(({ id: bookInfoId }) => - reviewsRepo.insert({ userId, updateUserId: userId, bookInfoId, content }), - ); + return await match(bookInfo) + .with(false, () => new BookInfoNotFoundError(args.bookInfoId)) + .otherwise(() => insertReview(args)); }; type RemoveArgs = { reviewsId: number; deleter: ParsedUser }; export const removeReview = async ({ reviewsId, deleter }: RemoveArgs) => { const isAdmin = () => deleter.role === 'librarian'; const doRemoveReview = () => - removeReviewById({ reviewsId, deleteUserId: deleter.id }); + deleteReviewById({ reviewsId, deleteUserId: deleter.id }); - const review = await reviewsRepo.findOneBy({ id: reviewsId }); + const review = await getReviewById(reviewsId); return match(review) - .with(null, { isDeleted: true }, () => new ReviewNotFoundError(reviewsId)) + .with( + undefined, + { isDeleted: true }, + () => new ReviewNotFoundError(reviewsId), + ) .when(isAdmin, doRemoveReview) .with({ userId: deleter.id }, doRemoveReview) .otherwise( @@ -52,14 +51,12 @@ export const updateReview = async ({ userId, content, }: UpdateArgs) => { - const review = await reviewsRepo.findOneBy({ id: reviewsId }); + const review = await getReviewById(reviewsId); - return match(review) - .with(null, () => new ReviewNotFoundError(reviewsId)) + return await match(review) + .with(undefined, () => new ReviewNotFoundError(reviewsId)) .with({ disabled: true }, () => new ReviewDisabledError(reviewsId)) - .with({ userId }, () => - reviewsRepo.update(reviewsId, { content, updateUserId: userId }), - ) + .with({ userId }, () => updateReviewById({ reviewsId, userId, content })) .otherwise(() => new ReviewForbiddenAccessError({ userId, reviewsId })); }; @@ -68,11 +65,11 @@ export const toggleReviewVisibility = async ({ reviewsId, userId, }: ToggleReviewArgs) => { - const review = await findDisabledReviewById({ id: reviewsId }); + const review = await getReviewById(reviewsId); - return match(review) - .with(null, () => new ReviewNotFoundError(reviewsId)) - .otherwise(async ({ disabled }) => - toggleReviewVisibilityById({ reviewsId, userId, disabled }), + return await match(review) + .with(undefined, () => new ReviewNotFoundError(reviewsId)) + .otherwise(({ disabled }) => + toggleVisibilityById({ reviewsId, userId, disabled }), ); };