Skip to content

Commit

Permalink
refactor: v2/reviews에 kysely 적용
Browse files Browse the repository at this point in the history
  • Loading branch information
scarf005 committed Aug 24, 2023
1 parent 38b682e commit 8467faa
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 57 deletions.
22 changes: 22 additions & 0 deletions backend/src/kysely/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod';

/** 반환값이 조건을 만족하지 않으면 오류 throw */
const throwIf = <T>(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);
140 changes: 113 additions & 27 deletions backend/src/v2/reviews/repository.ts
Original file line number Diff line number Diff line change
@@ -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<Pick<Reviews, 'disabled' | 'disabledUserId'> | 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();
57 changes: 27 additions & 30 deletions backend/src/v2/reviews/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 }));
};

Expand All @@ -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 }),
);
};

0 comments on commit 8467faa

Please sign in to comment.