Skip to content

Commit

Permalink
feat: implement get reviews service method
Browse files Browse the repository at this point in the history
  • Loading branch information
skgndi12 committed Mar 12, 2024
1 parent dce8ecf commit 37262e0
Show file tree
Hide file tree
Showing 3 changed files with 300 additions and 3 deletions.
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);
});
});
});

0 comments on commit 37262e0

Please sign in to comment.