Skip to content

Commit

Permalink
[GWL-257] common service 에러 처리, 테스트 코드 작성, 리팩토링 (#272)
Browse files Browse the repository at this point in the history
* chore: queryBuilder 에러 처리

* add: base-paginate-res.dto.ts 파일 추가

* test: test 환경 구축

* test: common.service.spec.ts 작성

* chore: format 적용

---------

Co-authored-by: jeong-yong-shin <[email protected]>
  • Loading branch information
sjy982 and jeong-yong-shin authored Dec 7, 2023
1 parent 1528c40 commit c87920e
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 25 deletions.
64 changes: 64 additions & 0 deletions BackEnd/src/common/common.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommonService } from './common.service';
import { QueryFailedError, Repository } from 'typeorm';
import { posts } from '../posts/mocks/mocks';
import { Post } from '../posts/entities/posts.entity';
import {
mockGetCreateUpdateQueryOptions,
mockItems,
mockPaginationDto,
} from './mocks/mocks';
import { InternalServerErrorException } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';

describe('commonService', () => {
let service: CommonService;
let postsRepository: Repository<Post>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CommonService,
{
provide: getRepositoryToken(Post),
useValue: {
createQueryBuilder: jest.fn(),
},
},
],
}).compile();
service = module.get<CommonService>(CommonService);
postsRepository = module.get<Repository<Post>>(getRepositoryToken(Post));
});

describe('paginate', () => {
const mockQueryBuilder = {
getMany: jest.fn(),
} as any;
it('QueryBuilder가 잘못 설정되어 있다면 InternalServerErrorException 에러', async () => {
jest.spyOn(service, 'makeQueryBuilder').mockReturnValue(mockQueryBuilder);
jest
.spyOn(mockQueryBuilder, 'getMany')
.mockRejectedValue(
new QueryFailedError('SELECT *', [], 'Error message'),
);
await expect(
service.paginate(
mockPaginationDto,
postsRepository,
mockGetCreateUpdateQueryOptions,
),
).rejects.toThrow(InternalServerErrorException);
});

it('게시글 요청하면 paginate 함수를 실행해서 요청한 게시글들, metaData 반환', async () => {
jest.spyOn(service, 'makeQueryBuilder').mockReturnValue(mockQueryBuilder);
jest.spyOn(mockQueryBuilder, 'getMany').mockResolvedValue(mockItems);
const result = await service.paginate(
mockPaginationDto,
postsRepository,
mockGetCreateUpdateQueryOptions,
);
expect(result).toEqual(posts);
});
});
});
58 changes: 37 additions & 21 deletions BackEnd/src/common/common.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { Injectable } from '@nestjs/common';
import { FindManyOptions, QueryBuilder, Repository } from 'typeorm';
import {
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import {
BaseEntity,
FindManyOptions,
QueryBuilder,
Repository,
SelectQueryBuilder,
} from 'typeorm';
import { BasePaginationDto } from './dto/base-pagination.dto';
import { ORM_OPERATION } from './const/orm-operation.const';
import { BaseModel } from './type/base-model.type';
import { JoinType, QueryOptions } from './type/query-options.type';
import { PaginateResponseDto } from './dto/base-paginate-res.dto';

@Injectable()
export class CommonService {
Expand All @@ -12,7 +23,7 @@ export class CommonService {
repository: Repository<T>,
queryOptions: QueryOptions,
overrideFindOptions: FindManyOptions<T> = {},
) {
): Promise<PaginateResponseDto> {
const findManyOptions: FindManyOptions<T> = {
...overrideFindOptions,
};
Expand All @@ -22,19 +33,24 @@ export class CommonService {
queryOptions,
findManyOptions,
);
const results: Array<T> = await queryBuilder.getMany();
const lastItemId: number =
results.length > 0 ? results[results.length - 1].id : null;
const isLastCursor: boolean =
results.length === paginationDto.take ? false : true;
return {
items: results,
metaData: {
lastItemId,
isLastCursor,
count: results.length,
},
};
try {
const results: Array<T> = await queryBuilder.getMany();
const lastItemId: number =
results.length > 0 ? results[results.length - 1].id : null;
const isLastCursor: boolean =
results.length === paginationDto.take ? false : true;
return {
items: results,
metaData: {
lastItemId,
isLastCursor,
count: results.length,
},
};
} catch (error) {
Logger.error(`쿼리 실행 중 오류: ${error.message}`, { error });
throw new InternalServerErrorException();
}
}

composeFindManyOptions<T>(
Expand Down Expand Up @@ -65,16 +81,16 @@ export class CommonService {
repository: Repository<T>,
queryOptions: QueryOptions,
findManyOptions: FindManyOptions<T> = {},
) {
): SelectQueryBuilder<T> {
let queryBuilder = repository.createQueryBuilder(queryOptions.mainAlias);
queryBuilder = queryBuilder.setFindOptions(findManyOptions);
if (queryOptions.join) {
queryOptions.join.forEach((value: JoinType) => {
if (queryOptions.joins) {
queryOptions.joins.forEach((value: JoinType) => {
queryBuilder = queryBuilder.leftJoin(value.joinColumn, value.joinAlias);
});
}
if (queryOptions.select) {
queryBuilder = queryBuilder.select(queryOptions.select);
if (queryOptions.selects) {
queryBuilder = queryBuilder.select(queryOptions.selects);
}
return queryBuilder;
}
Expand Down
8 changes: 8 additions & 0 deletions BackEnd/src/common/dto/base-paginate-res.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class PaginateResponseDto {
items: any[];
metaData: {
lastItemId: number | null;
isLastCursor: boolean;
count: number;
};
}
85 changes: 85 additions & 0 deletions BackEnd/src/common/mocks/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { BasePaginationDto } from '../dto/base-pagination.dto';
import { QueryOptions } from '../../common/type/query-options.type';
import { Profile } from '../../profiles/entities/profiles.entity';

export const mockPaginationDto: BasePaginationDto = {
order__createdAt: 'DESC',
take: 5,
where__id__less_then: 7,
};
export const profile = {
publicId: 'XVZXC-ASFSA123-ASFSF',
nickname: 'testNickname',
} as Profile;

export const mockItems = [
{
id: 6,
publicId: profile.publicId,
content: '안녕하세요 누구 누구 입니다.',
like: null,
createdAt: new Date('2023-12-04T05:14:15.879Z'),
updatedAt: new Date('2023-12-04T05:14:15.879Z'),
deletedAt: null,
postUrl: 'https://www.naver.com',
record: {
id: 2,
workoutTime: 6000000,
distance: 100000,
calorie: 360,
avgHeartRate: 60,
minHeartRate: 120,
maxHeartRate: 180,
},
profile: {
nickname: profile.nickname,
},
},
{
id: 5,
publicId: profile.publicId,
content: '수정한 내용입니다.',
like: null,
createdAt: new Date('2023-12-03T13:47:08.677Z'),
updatedAt: new Date('2023-12-04T12:44:44.000Z'),
deletedAt: null,
postUrl: 'google.com',
record: {
id: 1,
workoutTime: 100,
distance: 100,
calorie: 100,
avgHeartRate: 100,
minHeartRate: 100,
maxHeartRate: 100,
},
profile: {
nickname: profile.nickname,
},
},
];

export const mockGetCreateUpdateQueryOptions: QueryOptions = {
mainAlias: 'post',
joins: [
{
joinColumn: 'post.record',
joinAlias: 'record',
},
{
joinColumn: 'post.profile',
joinAlias: 'profile',
},
],
selects: [
'post',
'record.id',
'record.workoutTime',
'record.distance',
'record.calorie',
'record.avgHeartRate',
'record.minHeartRate',
'record.maxHeartRate',
'profile.nickname',
],
};
4 changes: 2 additions & 2 deletions BackEnd/src/common/type/query-options.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export interface JoinType {

export interface QueryOptions {
mainAlias: string;
join?: JoinType[];
select?: string[];
joins?: JoinType[];
selects?: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { QueryOptions } from '../../common/type/query-options.type';

export const getCreateUpdateQueryOptions: QueryOptions = {
mainAlias: 'post',
join: [
joins: [
{
joinColumn: 'post.record',
joinAlias: 'record',
Expand All @@ -12,7 +12,7 @@ export const getCreateUpdateQueryOptions: QueryOptions = {
joinAlias: 'profile',
},
],
select: [
selects: [
'post',
'record.id',
'record.workoutTime',
Expand Down

0 comments on commit c87920e

Please sign in to comment.