인프런 커뮤니티 질문&답변

둘리콩콩님의 프로필 이미지
둘리콩콩

작성한 질문수

[코드팩토리] [초급] NestJS REST API 백엔드 완전 정복 마스터 클래스 - NestJS Core

composeFindOptions 함수 완성하기

where과 order가 적용안되는

해결된 질문

작성

·

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가 빈 객체로 반환되는 것을 확인했는데 어떻게 고쳐야 할까요

답변 1

0

둘리콩콩님의 프로필 이미지
둘리콩콩
질문자

return options의 위치가 이상했네요 스스로 해결했습니다

둘리콩콩님의 프로필 이미지
둘리콩콩

작성한 질문수

질문하기