해결된 질문
작성
·
225
·
수정됨
0
내림차순을 확인하려고 param에 DESC를 넣었는데 내림차순 적용 되지 않길래 디버깅하다 문제에 직면했습니다.
* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { BadRequestException, Injectable } from '@nestjs/common';
import { BasePaginationDto } from './dto/base-pagination.dto';
import {
FindManyOptions,
FindOptionsOrder,
FindOptionsWhere,
Repository,
} from 'typeorm';
import { BaseModel } from './entity/base.entity';
import { FILTER_MAPPER } from './const/filter-mapper.const';
import { HOST, PROTOCOL } from './const/env.const';
@Injectable()
export class CommonService {
paginate<T extends BaseModel>(
dto: BasePaginationDto,
repository: Repository<T>,
overrideFindOptions: FindManyOptions<T> = {},
path: string, // api path의 일반화를 위해
) {
if (dto.page) {
return this.pagePaginate(dto, repository, overrideFindOptions);
} else {
return this.cursorPaginate(dto, repository, overrideFindOptions, path);
}
}
// page기반과 cursor기반 페이지네이션 2가지 만들기
/**page기반 CommonService paginate*/
private async pagePaginate<T extends BaseModel>(
dto: BasePaginationDto,
repository: Repository<T>,
overrideFindOptions: FindManyOptions<T> = {},
) {}
/**cursor기반 CommonService paginate*/
private async cursorPaginate<T extends BaseModel>(
dto: BasePaginationDto,
repository: Repository<T>,
overrideFindOptions: FindManyOptions<T> = {},
path: string,
) {
/**
* where__likeCount__more_than
*
* where__title__ilike
*/
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;
// 새로운 URL을 생성할 때 새로운 쿼리값을 추가하는 방법
const nextUrl = lastItem && new URL(`${PROTOCOL}://${HOST}/posts`);
if (nextUrl) {
// dto의 키값들을 loop
/**
* dto의 키값들을 루핑하면서
* 키값에 해당되는 밸류가 존재하면
* param에 그대로 붙여넣는다.
*
* 단, where__id_more_than 값만 lastItem의 마지막 값으로 넣어준다.
*/
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';
}
// URL에 추가하니까 string으로
nextUrl.searchParams.append(key, lastItem.id.toString());
}
return {
data: results,
cursor: {
after: lastItem?.id ?? null,
},
count: results.length,
next: nextUrl?.toString() ?? null,
};
}
private composeFindOptions<T extends BaseModel>(
dto: BasePaginationDto,
): FindManyOptions<T> {
/**
* where,
* order,
* take,
* skip -> page 일때만
*/
/**
* DTO의 현재 생긴 구조는 아래와 같다
* {
* where__id__more_than: 1,
* order__createdAt: 'ASC'
* }
*
* 현재는 where__id에 해당되는 where 필터만 사용중이지만
* 나중에 likeCount에 해당되는 추가 필터를 넣고싶으면
* 모든 where 필터들을 자동으로 파싱할 수 있을만한 기능을 제작
*
* 1) where로 시작한다면 필터 로직을 적용한다.
* 2) order로 시작한다면 정렬 로직을 적용한다.
* 3) 필터 로직을 적용한다면 '__' 기준으로 split 했을 때
* 3개의 값으로 나뉘는지 2개의 값으로 나뉘는지 확인한다.
* 3-1) 3개의 값으로 나뉜다면 FILTER_MAPPER에서 해당되는 operator 함수를 찾아서 적용한다.
* 3-2) 2개의 값으로 나뉜다면 정확한 값을 필터하는 것이기 때문에 operator 없이 적용한다.
* 4) order의 경우 3-2와 같이 적용한다.
*/
let where: FindOptionsWhere<T> = {};
let order: FindOptionsOrder<T> = {};
for (const [key, value] of Object.entries(dto)) {
if (key.startsWith('where__')) {
where = {
...where,
...this.parseWhereFilter(key, value),
};
} else if (key.startsWith('order__')) {
order = {
...order,
...this.parseWhereFilter(key, value),
};
}
}
return {
where,
order,
take: dto.take,
skip: dto.page ? dto.take * (dto.page - 1) : null,
};
}
private parseWhereFilter<T extends BaseModel>(
key: string,
value: any,
): FindOptionsWhere<T> | FindOptionsOrder<T> {
const options: FindOptionsWhere<T> | FindOptionsOrder<T> = {};
/**
* 예를들어 where__id__more_than
* __를 기준으로 나눴을때
*
* ['where', 'id', 'more_than']으로 나눌 수 있다.
*/
const split = key.split('__');
if (split.length !== 2 && split.length !== 3) {
throw new BadRequestException(
`where 필터는 '__'로 split 했을때 길이가 2 또는 3이어야합니다 - 문제되는 키값 ${key}`,
);
}
/**
* 길이가 2일 경우는
* where__id = 3
*
* FindOptionsWhere로 풀어보면
* 아래와 같다
*
* {
* where: {
* id: 3,
* }
* }
*/
if (split.length === 2) {
// [where, id]
const [_, field] = split;
// field => id
// value => 3
/**
* {
* id: 3
* }
*/
options[field] = value;
} else {
/**
* 길이가 3일 경우에는 Typeorm 유틸리티 적용이 필요한 경우
*
* where__id_more_than의 경우
* where는 버려도 되고 두번째 값은 필터할 키값이 되고
* 세번째 값은 typeorm 유틸리티가 된다.
*
* FILTER_MAPPER에 미리 정의해둔 값들로
* field 값에 FILTER_MAPPER에서 해당되는 utility를 가져온 후
* 값에 적용 해준다.
*
*/
// ['where', 'id', 'more_than']
const [_, field, operator] = split;
// where__id__between = 3, 4
// 만약에 split대상 문자가 존재하지 않으면 길이가 무조건 1이다.
// const values = value.toString().split(',');
// field -> id
// operator -> more_than
// FILTER_MAPPER -> MoreThan 함수
// if (operator === 'between') {
// options[field] = FILTER_MAPPER[operator](values[0], values[1]);
// } else {
// options[field] = FILTER_MAPPER[operator](value);
// }
options[field] = FILTER_MAPPER[operator](value);
return options;
}
}
}
디버깅으로 중단점 눌러가며 확인해봤는데 parseWhereFilter 메서드에서 options은 올바르게 key, value를 생성하고 반환하는데
composeFindOptions 메서드에서 밑의 코드를 중단점 확인했을 때
let where: FindOptionsWhere<T> = {};
let order: FindOptionsOrder<T> = {};
for (const [key, value] of Object.entries(dto)) {
if (key.startsWith('where__')) {
where = {
...where,
...this.parseWhereFilter(key, value),
};
} else if (key.startsWith('order__')) {
order = {
...order,
...this.parseWhereFilter(key, value),
};
}
}
return {
where,
order,
take: dto.take,
skip: dto.page ? dto.take * (dto.page - 1) : null,
};
order와 where가 빈 객체로 반환되는 것을 확인했는데 어떻게 고쳐야 할까요