Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#164] Implement get reviews service method #270

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions api/src/core/services/review/review.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AccessLevel } from '@prisma/client';

import { Review } from '@src/core/entities/review.entity';
import { Reply, Review } from '@src/core/entities/review.entity';
import { User } from '@src/core/entities/user.entity';
import { ReplyRepository } from '@src/core/ports/reply.repository';
import {
CreateReviewParams,
FindReviewsParams,
ReviewRepository
} from '@src/core/ports/review.repository';
import {
Expand All @@ -15,7 +16,9 @@ import { UserRepository } from '@src/core/ports/user.repository';
import {
CreateReviewDto,
CreateReviewResponse,
GetReviewResponse
GetReviewResponse,
GetReviewsDto,
GetReviewsResponse
} from '@src/core/services/review/types';
import { AccessLevelEnum } from '@src/core/types';
import { AppErrorCode, CustomError } from '@src/error/errors';
Expand Down Expand Up @@ -92,4 +95,73 @@ export class ReviewService {
review
};
};

public getReviews = async (
dto: GetReviewsDto
): Promise<GetReviewsResponse> => {
const sortBy = dto.sortBy ?? 'createdAt';
const direction = dto.direction ?? 'desc';
const pageOffset = dto.pageOffset ?? 1;
const pageSize = dto.pageSize ?? 10;

if (!(pageSize >= 1 && pageSize <= 100)) {
throw new CustomError({
code: AppErrorCode.BAD_REQUEST,
message: 'page size should be between 1 and 100',
context: { pageSize }
});
}

const [users, reviews, reviewCount] = await this.txManager.runInTransaction(
async (
txClient: TransactionClient
): Promise<[User[], Review[], number]> => {
const params: FindReviewsParams = {
nickname: dto.nickname,
title: dto.title,
movieName: dto.movieName,
sortBy,
direction,
pageOffset,
pageSize
};
const { reviews, reviewCount } =
await this.reviewRepository.findManyAndCount(params, txClient);

const userIds = this.extractUserIds(reviews);
const users = await this.userRepository.findByIds(userIds, txClient);

return [users, reviews, reviewCount];
},
IsolationLevel.READ_COMMITTED
);

const additionalPageCount = reviewCount % pageSize !== 0 ? 1 : 0;
const totalPageCount =
Math.floor(reviewCount / pageSize) + additionalPageCount;
const pagination = {
sortBy,
direction,
pageOffset,
pageSize,
totalEntryCount: reviewCount,
totalPageCount
};

return {
users,
reviews,
pagination
};
};

private extractUserIds = (entries: Review[] | Reply[]): string[] => {
const userIds: string[] = [];
entries.forEach((entry) => {
userIds.push(entry.userId);
});
const uniqueUserIds = [...new Set(userIds)];

return uniqueUserIds;
};
}
30 changes: 30 additions & 0 deletions api/src/core/services/review/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ export type SortBy = 'createdAt' | 'movieName';

export type Direction = 'asc' | 'desc';

export interface ReviewsPaginationResponse {
sortBy: SortBy;
direction: Direction;
pageOffset: number;
pageSize: number;
totalEntryCount: number;
totalPageCount: number;
}

export interface CreateReviewResponse {
user: User;
review: Review;
Expand All @@ -16,9 +25,30 @@ export interface GetReviewResponse {
review: Review;
}

export interface GetReviewsResponse {
users: User[];
reviews: Review[];
pagination: ReviewsPaginationResponse;
}

export interface CreateReviewDto {
requesterIdToken: AppIdToken;
title: string;
movieName: string;
content: string;
}

export interface GetReviewsDto {
// filter
nickname?: string;
title?: string;
movieName?: string;

// sort
sortBy?: SortBy;
direction?: Direction;

// pagination
pageOffset?: number;
pageSize?: number;
}
197 changes: 196 additions & 1 deletion api/test/core/services/review/review.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import { ReviewRepository } from '@src/core/ports/review.repository';
import { TransactionManager } from '@src/core/ports/transaction.manager';
import { UserRepository } from '@src/core/ports/user.repository';
import { ReviewService } from '@src/core/services/review/review.service';
import { CreateReviewDto } from '@src/core/services/review/types';
import {
CreateReviewDto,
GetReviewsDto
} from '@src/core/services/review/types';
import { AccessLevelEnum, IdpEnum } from '@src/core/types';
import { AppErrorCode, CustomError } from '@src/error/errors';
import { PrismaTransactionManager } from '@src/infrastructure/prisma/prisma.transaction.manager';
import { ExtendedPrismaClient } from '@src/infrastructure/prisma/types';
import { PostgresqlReviewRepository } from '@src/infrastructure/repositories/postgresql/review.repository';
Expand All @@ -25,6 +29,12 @@ jest.mock('@root/test/infrastructure/prisma/test.prisma.client', () => ({

const prismaMock = extendedPrisma as DeepMockProxy<ExtendedPrismaClient>;

function calculateTotalPageCount(count: number, pageSize?: number) {
const givenPageSize = pageSize ?? 10;
const additionalPageCount = count % givenPageSize !== 0 ? 1 : 0;
return Math.floor(count / givenPageSize) + additionalPageCount;
}

describe('Test review service', () => {
let userRepository: UserRepository;
let reviewRepository: ReviewRepository;
Expand Down Expand Up @@ -240,4 +250,189 @@ describe('Test review service', () => {
expect(reviewFindByIdArgs).toEqual(reviewId);
});
});

describe('Test get reviews', () => {
const userId = 'randomId';
const nickname = 'randomNickname';
const tag = '#TAGG';
const idp = new IdpEnum(Idp.GOOGLE);
const email = '[email protected]';
const accessLevel = new AccessLevelEnum(AccessLevel.USER);
const title = 'randomTitle';
const movieName = 'randomMovie';
const content = 'randomContent';
const currentDate = new Date();
const users: User[] = [];
const reviews: Review[] = [];
const reviewCount = 10;

const userFindByIds = jest.fn(() => Promise.resolve(users)) as jest.Mock;
const reviewFindManyCount = jest.fn(() =>
Promise.resolve({ reviews, reviewCount })
) as jest.Mock;

beforeAll(() => {
users.push(
new User(
userId,
nickname,
tag,
idp,
email,
accessLevel,
currentDate,
currentDate
)
);

for (let i = 1; i <= reviewCount; i++) {
reviews.push(
new Review(
i,
userId,
title,
movieName,
content,
0,
currentDate,
currentDate
)
);
}

prismaMock.$transaction.mockImplementation((callback) =>
callback(prismaMock)
);
userRepository = new PostgresqlUserRepository(prismaMock);
reviewRepository = new PostgresqlReviewRepository(prismaMock);
txManager = new PrismaTransactionManager(prismaMock);
userRepository.findByIds = userFindByIds;
reviewRepository.findManyAndCount = reviewFindManyCount;
});

it('should success when page size is not provided', async () => {
const givenDto: GetReviewsDto = {
nickname,
title,
movieName
};
const actualResult = await new ReviewService(
userRepository,
reviewRepository,
replyRepository,
txManager
).getReviews(givenDto);
const totalPageCount = calculateTotalPageCount(reviewCount);

expect(actualResult.pagination).toEqual({
sortBy: 'createdAt',
direction: 'desc',
pageOffset: 1,
pageSize: 10,
totalEntryCount: reviewCount,
totalPageCount
});
expect(JSON.stringify(actualResult.users)).toEqual(JSON.stringify(users));
for (let i = 0; i <= 10; i++) {
expect(JSON.stringify(actualResult.reviews[i])).toEqual(
JSON.stringify(reviews[i])
);
}

expect(userRepository.findByIds).toBeCalledTimes(1);
const userFindByIdArgs = userFindByIds.mock.calls[0][0];
expect(userFindByIdArgs).toEqual([userId]);

expect(reviewRepository.findManyAndCount).toBeCalledTimes(1);
const reviewFindManyCountArgs = reviewFindManyCount.mock.calls[0][0];
expect(reviewFindManyCountArgs).toEqual(
expect.objectContaining({
nickname: givenDto.nickname,
title: givenDto.title,
movieName: givenDto.movieName,
sortBy: 'createdAt',
direction: 'desc',
pageOffset: 1,
pageSize: 10
})
);
});

it('should success when a page size is in the range of 1 to 100', async () => {
const givenPageSize = 5;
const givenDto: GetReviewsDto = {
nickname,
title,
movieName,
pageSize: givenPageSize
};
const actualResult = await new ReviewService(
userRepository,
reviewRepository,
replyRepository,
txManager
).getReviews(givenDto);
const totalPageCount = calculateTotalPageCount(
reviewCount,
givenPageSize
);

expect(actualResult.pagination).toEqual({
sortBy: 'createdAt',
direction: 'desc',
pageOffset: 1,
pageSize: givenPageSize,
totalEntryCount: reviewCount,
totalPageCount
});
expect(JSON.stringify(actualResult.users)).toEqual(JSON.stringify(users));
for (let i = 0; i <= givenPageSize; i++) {
expect(JSON.stringify(actualResult.reviews[i])).toEqual(
JSON.stringify(reviews[i])
);
}

expect(userRepository.findByIds).toBeCalledTimes(1);
const userFindByIdArgs = userFindByIds.mock.calls[0][0];
expect(userFindByIdArgs).toEqual([userId]);

expect(reviewRepository.findManyAndCount).toBeCalledTimes(1);
const reviewFindManyCountArgs = reviewFindManyCount.mock.calls[0][0];
expect(reviewFindManyCountArgs).toEqual(
expect.objectContaining({
nickname: givenDto.nickname,
title: givenDto.title,
movieName: givenDto.movieName,
sortBy: 'createdAt',
direction: 'desc',
pageOffset: 1,
pageSize: givenPageSize
})
);
});

it('should fail when page size exceeds 100', async () => {
const givenPageSize = 101;
const givenDto: GetReviewsDto = {
nickname,
title,
movieName,
pageSize: givenPageSize
};

try {
await new ReviewService(
userRepository,
reviewRepository,
replyRepository,
txManager
).getReviews(givenDto);
} catch (error: unknown) {
expect(error).toBeInstanceOf(CustomError);
expect(error).toHaveProperty('code', AppErrorCode.BAD_REQUEST);
}

expect(reviewRepository.findManyAndCount).toBeCalledTimes(0);
});
});
});