이후 댓글과 채팅 등등에서도 페이지 네이션을 사용하기 위해 객체지향 적으로 코드를 고도화 시켜 일반화 해보고자 한다.
basePaginationdto.ts
export class BasePaginationDto {
@IsNumber()
@IsOptional()
page?: number;
@IsNumber()
@IsOptional()
where__id__less_than?: number;
// 이전 마지막 데이터의 ID
// 이 프로퍼티에 입력된 ID 보다 높은 ID 부터 값을 가져오기
@IsNumber()
@IsOptional()
where__id__more_than?: number;
// 정렬
// createdAt -> 생성된 시간의 내림차/오름차 순으로 정렬
// Ascending / Descending
@IsIn(["ASC", "DESC"])
@IsOptional()
order__createdAt: "ASC" | "DESC" = "ASC";
// 몇개의 데이터를 응답으로 받을지
@IsNumber()
@IsOptional()
take: number = 20;
}
페이지 네이션을 사용하는 dto들의 최상위 클래스인 BaseDTO 생성
common.service.ts
paginate<T extends BaseModel>(
dto: BasePaginationDto,
repository: Repository<T>,
overrideFindOptions: FindManyOptions<T> = {},
path: string,
) {
if (dto.page) {
return this.pagePaginate(
dto,
repository,
overrideFindOptions
);
} else {
return this.cursorPaginate(
dto,
repository,
overrideFindOptions,
path,
);
}
}
일반화 시킨 레퍼지토리를 사용하기 위해 제네릭을 사용해서 타입을 받는다.
그럼 post 게시물에만 국한되지 않고 페이지 네이션을 사용할 수 있다.
overrideFindOptions를 사용해서 추가적으로 세부 페이지 네이션 마다 필요한 기능을 더 추가해 줄 수 있다.
const nextUrl = lastItem && new URL(`${protocol}://${host}/posts`);
path는 상당 URL에서 posts에 들어갈 다른 경로를 받는데 사용 된다.
EX) chat, comments
페이지 네이션에서 사용될 typeorm 유틸리티
import {
Any,ArrayContainedBy,ArrayContains,
ArrayOverlap, Between, Equal, ILike,
In, IsNull,LessThan, LessThanOrEqual,
Like, MoreThan, MoreThanOrEqual,Not,
} from "typeorm";
export const FILTER_MAPPER = {
not: Not,
less_than: LessThan,
less_than_or_equal: LessThanOrEqual,
more_than: MoreThan,
more_than_or_equal: MoreThanOrEqual,
equal: Equal,
like: Like,
i_like: ILike,
between: Between,
in: In,
any: Any,
is_null: IsNull,
array_contains: ArrayContains,
array_contained_by: ArrayContainedBy,
array_overlap: ArrayOverlap,
};
페이지 네이션 일반화를 위해 필요한 함수 2가지
1.composeFindOptions
2.parseFilter
composeFindOptions는
where,order,take,skip(page기반일때만)을 반환하는 함수이다.
현재는 where__id__more_than과 같은 where 필터만 사용 중이지만 차후 wehre__likeCount__more_than 혹은 where__title__ilike등 추가 필터를 구현하려 할 때 모든 where필터를 자동으로 파싱할 수 있게 하는 기능을 구현하기 위해 일반화를 한다.
1) where로 시작한다면 필터 로직을 적용한다.
2) order로 시작한다면 정렬 로직을 적용한다.
parseFilter에서
3-1) where경우 __ 를 기준으로 split시 3개로 나뉘는지 2개로 나뉘는지 확인하다.
3-1-1) 3개로 나뉜다면 FILTER_MAPPER에서 해당되는 operator함수를 찾아서 적용
ex) where __ id__more_than
3-1-2) 2개의 값으로 나뉜다면 정확한 값을 필터 하기 떄문에 opertaor 없이 적용한다.
ex) where__id
4-1) ordrer 경우 2개의 값으로만 나뉘기 때문에 operator없이 적용한다.
private composeFindOptions<T extends BaseModel>(
dto: BasePaginationDto,
): FindManyOptions<T> {
let where: FindOptionsWhere<T> = {};
let order: FindOptionsOrder<T> = {};
for (const [key, value] of Object.entries(dto)) {
if (key.startsWith('where__')) {
where = {
...where,
...this.parseFilter(key, value),
}
} else if (key.startsWith('order__')) {
order = {
...order,
...this.parseFilter(key, value),
}
}
}
return {
where,
order,
take: dto.take,
skip: dto.page ? dto.take * (dto.page - 1) : null,
};
}
위와 같이 DTO 값들을 루핑하며 하나의 키값에 따라 parseFilter를 적용하여 where혹은 order객체를 새로 만들면서 옵션들을 추가한다.
private parseFilter<T extends BaseModel>(
key: string,
value: any
): FindOptionsWhere<T> | FindOptionsOrder<T> {
const options: FindOptionsWhere<T> = {};
// key 예시) where__id__more_than
const split = key.split("__");
if (split.length !== 2 && split.length !== 3) {
throw new BadRequestException(
`where 필터는 '__'로 split 했을때 길이가 2 또는 3이어야합니다 - 문제되는 키값 : ${key}`
);
}
if (split.length === 2) {
// ['where', 'id']
const [_, field] = split;
options[field] = value;
} else {
// where__id__more_than의 경우 where는 버려도 되고 두번째 값은 필터할 키값이 되고 세번째 값은 typeorm 유틸리티가 된다.
const [_, field, operator] = split;
if (operator === "i_like") {
options[field] = FILTER_MAPPER[operator](`%${value}%`);
} else {
options[field] = FILTER_MAPPER[operator](value);
}
}
return options;
}
우선 위와 같이 more_than 과 같은 유틸리티와 ILIKE와 같은 유틸리에 대한 분기 처리를 해주었다.
차후 필요한 필터링 로직이 필요하면 그때 바로 유지보수를 편하게 하면 된다 !!
commer.service.ts
private async cursorPaginate<T extends BaseModel>(
dto: BasePaginationDto,
repository: Repository<T>,
overrideFindOptions: FindManyOptions<T> = {},
path: string
) {
const findOptions = this.composeFindOptions<T>(dto);
const results = await repository.find({
...findOptions,
...overrideFindOptions,
});
const lastItem =
results.length > 0 && results.length === dto.take
? results[results.length - 1]
: null;
const protocol = this.configService.get<string>("PROTOCOL");
const host = this.configService.get<string>("HOST");
const nextUrl = lastItem && new URL(`${protocol}://${host}/${path}`);
if (nextUrl) {
for (const key of Object.keys(dto)) {
if (dto[key]) {
if (
key !== "where__id__more_than" &&
key !== "where__id__less_than"
) {
nextUrl.searchParams.append(key, dto[key]);
}
}
}
let key = null;
if (dto.order__createdAt === "ASC") {
key = "where__id__more_than";
} else {
key = "where__id__less_than";
}
nextUrl.searchParams.append(key, lastItem.id.toString());
}
return {
data: results,
cursor: {
after: lastItem?.id ?? null,
},
count: results.length,
next: nextUrl?.toString() ?? null,
};
}
이후 이전 post페이지 네이션에서 사용했던 로직 그대로 작성 하면 끝 !
post페이지네이션에서 추가 필터와 overrideoption사용
post controller.ts와 post service.ts
@Get()
getPosts(@Query() query: PaginatePostDto) {
return this.postsService.paginatePosts(query);
}
//////////////////////////////////////////////////////////
async paginatePosts(dto: PaginatePostDto) {
return this.commonService.paginate(
dto,
this.postsRepository,
{ relations: ["author"] },
"posts"
);
}
이제 post 모듈에서 commonservice를 사용해 일반화된 페이지 네이션을 사용할 수 있다.
override옵션으로는 author 관계를 볼수 있게 하였따.
PaginatePostDto.ts
export class PaginatePostDto extends BasePaginationDto {
@IsNumber()
@IsOptional()
where__postLike__more_than: number;
@IsString()
@IsOptional()
where__postTitle__i_like: string;
}
또한 게시물post dto에 추가적으로 원하는 필터기능을 추가 할수 있다.
그럼 위와 같이 게시물 페이지네이션 DTO에서 추가한 대로 postLIke 10개 이상 과 postTitle에 '2'라는 문자가 들어간 데이터만 필터링하고 override옵션으로 준대로 author 관계가 포함되도록 데이터를 조회 할 수 있다 !!
DTO 프로퍼티 whitelisting하기
위와 같이 게시물 페이지네이션 dto를 주석으로 지웠지만 postman으로 요청을 보내도 그대로 적용이 된것 처럼 응답이 온다.
이와 같은 경우에 해커가 필터로직에 대해 이해를 한다면 쿼리를 자신이 마음대로 URL에 넣어 DB에 있는 데이터를 자유롭게 조회할 수 있게 된다.
그래서 dto에 적힌 값 이외의 값들이 들어오면 막기 위해 whitelisting 옵션을 사용해야 한다.
main.ts
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
whitelist: true,
forbidNonWhitelisted: true,
})
);
whitelist를 true로 하여 벨리데이션 데코레이터가 적용 되지 않은 모든 프로퍼티들을 삭제한다 !
forbidNonWhitelisted를 true로 하여 프론트에서 오타 혹은 실수로 인해 삭제됬을 경우 에러를 확인할 수 있도록 메세지가 담긴 에러를 던져 준다.
그럼이제 dto에서 벨리데이션 데코레이터를 적용하지 않은 프로퍼티가 쿼리로 들어올경우 에러를 잘 던진다 !!
'캡스톤 설계 [건물별 소통 플랫폼 BBC]' 카테고리의 다른 글
채팅 기능 구현하기 (0) | 2024.03.26 |
---|---|
인터셉터과 트랜잭션을 활용한 게시물 작성 [파일 업로드] (0) | 2024.03.25 |
게시물 페이지 네이션 (0) | 2024.03.24 |
유효성 체크 [DTO && Class Validator/Transformer] + 게시물 수정+비밀번호 안보이게하기 (0) | 2024.03.08 |
포스트맨 기능활용[환경변수,토큰 삽입 자동화] (0) | 2024.03.07 |