반응형
Waht Subscription?

 

GraphQL 구독은 서버의 실시간 메시지를 듣기로 선택한 클라이언트들에게 서버의 데이터를 푸시하는 방법입니다.

 

WebSocket이라고 이해하면 된다 !

 

SubScription 설정방법

 

노마드 우버이츠 클론코딩을보며 공부하였으나,

버전이 올라감에따라 Subscription설정방법이 바뀌어서

 

기록용으로 서술합니다 !

 

// app.module.ts


imports:[
...

    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      installSubscriptionHandlers: true, //구독 활성화
      subscriptions: { //Nest10버전부터는 이와같이 설정해주어야 구독이 활성화된다 !
        'subscriptions-transport-ws': {
          onConnect: (connectionParams, WebSocket, context) => {
            // header정보 는 ConnectionParams로 들어옴
            // console.log('context', context); //Context정보
            // console.log('WebSocket', WebSocket); //Websockect정보
            // console.log('connectionParams', connectionParams);
            return { message:"Hello Connection!!!!" };
          },
        },
      },
      autoSchemaFile: true, //스키마를 메모리에 저장,
      context: ({ req, connection }) => {
        if (req) {
          return req;
        }
        if (connection) {
         //구버전에서는 구독 활성화 시 Connection으로 Websocket정보가 전달되었으나,
         //Nest10버전 기준으로 undefined된 듯 함.
        }
      },
      formatError: formmatError, //에러 포맷 설정
    }),
    

...
반응형
반응형
export enum OrderStatus {
  Pending = 'Pending',
  Cooking = 'Cooking',
  PickedUp = 'PickedUp',
  Delivered = 'Delivered',
}


registerEnumType(OrderStatus, { name: 'OrderStatus' });

 

위처럼 enum 적용 후 registerEnumType으로 정의하면

 

Schema에 적용된다 !!!

반응형
반응형
Repository 패턴이란,

 

먼저 CustomRepository를 사용하기에앞서,

Repository패턴이 무엇인지 알아보죠.

소프트웨어 디자인 패턴 중 하나로, 데이터베이스와의 상호 작용울 추상화하여
데이터 엑세스 코드를 분리하는데 사용됩니다

 

Repository 패턴의 핵심 아이디어는 아래와 같습니다.

 

1. 데이터 엑세스의 추상화 : Repository 패턴은 데이터 엑세스를 위한 인터페이스를 제공하여, 데이터베이스와의 통신을 추상화 합니다.

이로써 데이터 엑세스 코드가 실제데이터베이스와 직접  결합되자않고 인터페이스를 통해 접근할 수 있습니다.

 

2. 도메인로직과 데이터 엑세스의 분리 : Repository패턴을 사용하면 도메인 로직과 데이터 엑세스 코드를 분리할 수 있습니다.
도메인 로직은 데이터에 대한 비즈니스 규칙과 관련이 있고, Repository는 데이터 엑세스에 대한 구체적인 구현을 담당합니다.

 

3. 유지보수성 향상 : 데이터베이스 구현의 변경이나 데이터 엑세스 로직의 수정이 상대적으로 쉬어집니다.

실제 데이터베이스와의 상호 작용을 추상화하기 때문에 데이터베이스 변경이나 데이터 엑세스 코드 수정에 대한 영향을 최소화 할 수 있습니다.

 

4. 테스트 용이성 : 데이터베이스에 의존하지 않고도 도메인 로직을 테스트하기 쉬워집니다. (모크데이터를 만들어 단위테스트가 용이해짐)

 

 

CustomRepository의 사용이유

 

자주 호출되며 복잡한 요청이 필요할때 CustomRepository를 이용해

TypeORM의 Repository를 확장하여 사용자 정의 메서드를 추가해 사용합니다.

 

한가지 예를 들어 아래 코드는 등록하고자하는 카테고리가 없는 경우 생성해주는 작업입니다.

등록, 수정시에 사용되는 작업이라면 12줄의 코드가 총 2번 사용될텐데,

CustomRepository를 사용해 확장 메소드를 등록해놓으면 간소화할 수 있겠죠

    const categoryName = name.trim().toLowerCase();
    const categorySlug = categoryName.replace(/ /g, '-');
    let category = await this.categories.findOne({
      where: { slug: categorySlug },
    });
    if (!category) {
      category = await this.categories.save(
        this.categories.create({ slug: categorySlug, name: categoryName }),
      );
    }
    return category;

 

 

TypeOrm 버전Up

 

TypeOrm 버전이 0.2.x에서 0.3.x로 올라오면서 많은 변화들이 생겼다.

 

그중 하나가 @EntityRepositody를 사용해서 커스텀 레포지토리를 만들어서 사용할 수 있었다면,

0.3.x버전으로 올라오면서 deprecated되어 더이상 사용할 수 없다.

 

 

0.2 버전의 Custom Repository

 

// cateogory.service.ts
@Injectable()
export class CategoryService {
	constructor(
		@InjectRepository(Category)
		private categoryRepository: CategoryRepository) {}

	createCategory(): Promise<CreateCategoryOutput> {
		return this.categoryRepository.getOrCreateCategory();
	}
}

 

// category.repository.ts
@EntityRepository(Category)
export class CategoryRepository extends Repository<Category> {
	async getOrCreateCategory() {
		const categoryName = name.trim().toLowerCase();
    		const categorySlug = categoryName.replace(/ /g, '-');
    		let category = await this.categories.findOne({
    		  where: { slug: categorySlug },
    		});
    		if (!category) {
    		  category = await this.categories.save(
   		     this.categories.create({ slug: categorySlug, name: categoryName }),
   		   );
  		  }
  		  return category;
	}
}

 

0.2버전의 방식은 @EntityRepository 데코레이터를 Repository에 작성해준 뒤 Service 레이어에서 Repository 의존성을 주입 한 후 사용했습니다.

 

하지만 TypeOrm 버전이 0.3으로 올라감에 따라 @EntityRepository 데코레이터를 사용할 수 없게 변경되어 이에따라 Custom Repository를 사용하기 위해선 직접 데코레이터를 사용한 후 Service 레이어로 의존성을 주입해야 하도록 변경되었습니다.

 

0.3버전의 Custom Repository
/* src/typeorm/custom.repository.decorator.ts */

import { SetMetadata } from '@nestjs/common';

export const TPYEORM_CUSTOM_REPOSITORY = 'TPYEORM_CUSTOM_REPOSITORY';

// eslint-disable-next-line
export function CustomRepository(entity: Function): ClassDecorator {
  return SetMetadata(TPYEORM_CUSTOM_REPOSITORY, entity);
}

 

우선 CustomRepository의 데코레이터를 정의해줍니다. SetMetaData는 메타데이터를 설정해주는 역할을 합니다.
(TYPEORM_CUSTOM_REPOSITORY라는 키의 메타데이터를 설정함)

 

 

/* src/typeorm/typeorm.module.ts */

import { DynamicModule, Provider } from '@nestjs/common';
import { getDataSourceToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { TPYEORM_CUSTOM_REPOSITORY } from './custom.repository.decorator';
/**
 *  @CustomRepository 데코레이터를 사용할경우
 *  CustomRepository에 담겨있는 메타데이터들을 가져온다. 그 후 DataSource 정보를 주입받고
 *  providers 에 추가한다.
 *  해당 모듈을 통해 우리가 만든 데코레이터로 CustomRepository 기능을 사용할 수 있게 된다.
 */
export class TypeOrmCustomModule {
  public static forCustomRepository<T extends new (...args: any[]) => any>(
    repositories: T[],
  ): DynamicModule {
    const providers: Provider[] = [];

    console.log('repositories', repositories);
    for (const repository of repositories) {
      const entity = Reflect.getMetadata(TPYEORM_CUSTOM_REPOSITORY, repository);

      if (!entity) {
        continue;
      }

      providers.push({
        inject: [getDataSourceToken()],
        provide: repository,
        useFactory: (dataSource: DataSource): typeof repository => {
          const baseRepository = dataSource.getRepository<any>(entity);
          return new repository(
            baseRepository.target,
            baseRepository.manager,
            baseRepository.queryRunner,
          );
        },
      });
    }

    return {
      exports: providers,
      module: TypeOrmCustomModule,
      providers,
    };
  }
}

 

그 후 CustomRepository를 사용하기 위해 모듈을 생성줍니다

@CustomRepository 데코레이터를 사용할 경우 CustomRepository에 담겨있는 메타데이터들을 가져옵니다.

그 후 DataSource 정보를 주입받고 providers에 추가합니다.

해당 모듈을 통해 우리가 만든 데코레이터로 CustomRepository기능을 사용할 수 있게 됩니다.

 

/* /src/categories/category.module.ts */

@Module({
  imports: [
    TypeOrmModule.forFeature([Restaurant, Category]),
    TypeOrmCustomModule.forCustomRepository([CategoryRepository]),
  ],
  providers: [CategoryResolver, CategoryService],
})
export class CategoryModule {}

 

CustomRepository를 사용하려면 기존 TypeORM모듈에서 생성한 CustomModule로 바꿔주어야 합니다.

 

이후 Serivide에 의존성 주입후 사용하면 됩니다 !

 

EntityRepository 데코레이터 대신, CustomRepository를 사용하는 부분을 유념하세요 !

// category.repository.ts
@CustomRepository(Category)
export class CategoryRepository extends Repository<Category> {
	async getOrCreateCategory() {
		const categoryName = name.trim().toLowerCase();
    		const categorySlug = categoryName.replace(/ /g, '-');
    		let category = await this.categories.findOne({
    		  where: { slug: categorySlug },
    		});
    		if (!category) {
    		  category = await this.categories.save(
   		     this.categories.create({ slug: categorySlug, name: categoryName }),
   		   );
  		  }
  		  return category;
	}
}

 

 

 

반응형
반응형
가드란

 

가드는 라우팅된 핸들러(컨트롤러의 메서드)에 도달하기 전에 실행되는 중간에 위치하는 기능입니다.

이중 CanActivate라는 가드에 대해 알아볼 것 입니다.

CanActivate가드는 NestJS에서 사용되는 가드 중 하나이며, 주로 라우트로의 접근을 허용 또는 거부하는데 사용합니다.

 

사용방법

 

CanActivate를 구현한 클래스는 반드시 canActivate 메서드를 모함해야합니다.

 

이 메서드는 true를 요청하면 요청을 계속 진행하고, false를 반환하면 요청이 중단되어 forbidden오류가 발생합니다.

 

사용예시

 

아래는 제가 사용한 예시입니다.

/* /src/auth/auth.guard.ts */

//request를 진행할지 말지 결정

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Observable } from 'rxjs';
import { ErrorCode } from 'src/common/types/exception.types';
import { CustomError } from 'src/HttpExceptionFilter';
import { User } from 'src/users/entities/user.entity';
import { AllowedRoles } from './role.decorator';

//미들웨어 > 가드 > route
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const roles = this.reflector.get<AllowedRoles>(
      'roles',
      context.getHandler(),
    );
    if (!roles) {
      //모두 접근이 가능한 페이지
      return true;
    }
    const gqlContext = GqlExecutionContext.create(context).getContext();
    const user: User = gqlContext.req.user;
    if (!user) {
      throw new CustomError(ErrorCode.UNAUTHORIZED);
    }
    if (roles.includes('Any')) {
      //모두 접근이 가능한페이지
      return true;
    }

    //접근이 가능한경우 true 아닌경우 false로 인해 forbidden에러 발생
    return roles.includes(user.role);
  }
}

 

 

/* /src/auth.module.ts */

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth.guard';

//모든 요청에서 Guard를 확인함
@Module({ providers: [{ provide: APP_GUARD, useClass: AuthGuard }] })
export class AuthModule {}

 

 

/* /src/auth/role.decorator.ts */
/* 유저 롤을 구분하기 위해 사용하며 롤은 Client, Owner, Delivery 밑에 추가되는 Any 총 4개이다 */

import { SetMetadata } from '@nestjs/common';
import { UserRole } from 'src/users/entities/user.entity';

export type AllowedRoles = keyof typeof UserRole | 'Any';
export const Role = (roles: AllowedRoles[]) => SetMetadata('roles', roles);

 

 

/* /src/app.bodule.ts */
/* AuthModule을 Import해준다 */

생략
///
	import:[AuthModule,
    생략///]

///
생략

 

/* /src/users/users.resolver.ts */
/* Role Decorator가 없으면 모든 사용자 접근 가능 */
/* Role Decorator가 있는 경우, auth.guard.ts 소스 로직에 따라 동작함. */


import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { AuthUser } from 'src/auth/auth-user.decorator';
import { Role } from 'src/auth/role.decorator';
import {
  CreateAccountInput,
  CreateAccountOutput,
} from './dtos/create-account..dto';
import { EditProfileInput, EditProfileOutput } from './dtos/edit-profile.dto';
import { LoginInput, LoginIOutput } from './dtos/login.dto';
import { UserProfileInput, UserProfileOutput } from './dtos/user-profile.dto';
import { VerifyEmailInput, VerifyEmailOutput } from './dtos/verify-email.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
/* eslint-disable */

@Resolver(of => User)
export class UsersResolver {
  constructor(private readonly userService: UsersService) {}

  @Mutation(returns => CreateAccountOutput)
  createAccount(
    @Args('input') createAccountInput: CreateAccountInput,
  ): Promise<CreateAccountOutput> {
    return this.userService.createAccount(createAccountInput);
  }

  @Role(['Any'])
  @Query(returns => UserProfileOutput)
  userProfile(
    @Args() userProfileInput: UserProfileInput,
  ): Promise<UserProfileOutput> {
    return this.userService.findById(userProfileInput.userId);
  }

}

 

반응형
반응형
에러 핸들링(Formmat)을 하게된 배경.

 

스터디에서 NestJS를 공부하고 있다.

 

에러 포맷팅을 한 케이스

{
  "errors": [
    {
      "message": "[10000] - 로그인정보가 잘못되었습니다."
    }
  ],
  "data": null
}

 

 

에러 포맷팅을 하지 않은 케이스

{
  "errors": [
    {
      "message": "Custom Error",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "me"
      ],
      "extensions": {
        "code": "BAD_REQUEST",
        "stacktrace": [
          "CustomError: Custom Error",
          "    at AuthGuard.canActivate (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/src/auth/auth.guard.ts:20:15)",
          "    at GuardsConsumer.tryActivate (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@nestjs/core/guards/guards-consumer.js:15:34)",
          "    at canActivateFn (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@nestjs/core/helpers/external-context-creator.js:155:59)",
          "    at target (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@nestjs/core/helpers/external-context-creator.js:73:37)",
          "    at Object.me (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@nestjs/core/helpers/external-proxy.js:9:30)",
          "    at field.resolve (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@apollo/server/src/utils/schemaInstrumentation.ts:82:22)",
          "    at executeField (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/graphql/execution/execute.js:492:20)",
          "    at executeFields (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/graphql/execution/execute.js:414:22)",
          "    at executeOperation (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/graphql/execution/execute.js:344:14)",
          "    at execute (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/graphql/execution/execute.js:136:20)"
        ],
        "originalError": {
          "ok": false,
          "statusCode": 10000
        }
      }
    }
  ],
  "data": null
}

 

 

에러포맷팅을 하지 않는 경우, GraphQL에서 Exception이 발생한 경우, 클라이언트는 확인할 필요없는 StackTrace같은 정보도 나와서,

보기도 어렵고 클라이언트는 매번 에러작업을 해야하는데 불편함을 겪을 수 있어,

클라이언트도 작업을 편하게 할 수 있도록 포맷팅을 하였다

 

소스작업



/* src/ExceptiomnFormat.ts */

import { GraphQLError } from 'graphql';
import { ErrorDto } from './common/dtos/exception,dto';
import { ErrorCode, ErrorMessageV1 } from './common/types/exception.types';

export const formmatError = (error: GraphQLError): ErrorDto => {
  const originalError: any = error.extensions.originalError;
  const newError = new ErrorDto(originalError.statusCode);

  return newError;
};



/* src/HttpExceptionFilter.ts */
/* 아래 클래스 추가 */

export class CustomError extends BadRequestException {
  errorCode: number;
  
  constructor(errorCode: number) {
    super({ ok: false, statusCode: errorCode });
    this.errorCode = errorCode;
  }
}

 

/* src/common/types/exception.types.ts */

export enum ErrorCode {
  UNAUTHORIZED = 10000,
  FORBIDDEN = 10001,
  EXPIRED_TOKEN = 10002,
  QUEUE_EMPTY = 30000,
  QUEUE_ALREADY_ON_RUNNING = 30001,
  INTERNAL_SERVER_ERROR = 50000,
  AUTH_PASSWORD_SHORT = 20000,
  AUTH_INVALID_PASSWORD = 20001,
  AUTHORIZATION_NOT_EXIST = 20002,
  GPM_CATALOG_REJECT_LIST_EMPTY = 21000,
  GPM_CATALOG_REJECT_LIST_BE_ONE_OR_MORE = 21001,
}
export const ErrorMessageV1 = {
  [ErrorCode.UNAUTHORIZED]: {
    code: 10000,
    message: '로그인정보가 잘못되었습니다.',
  },
  [ErrorCode.FORBIDDEN]: { code: 10001, message: 'Access forbidden.' },
  [ErrorCode.EXPIRED_TOKEN]: { code: 10002, message: 'Token has expired.' },
  [ErrorCode.QUEUE_EMPTY]: { code: 30000, message: 'Queue is empty.' },
  [ErrorCode.QUEUE_ALREADY_ON_RUNNING]: {
    code: 30001,
    message: 'Queue is already on running.',
  },
  [ErrorCode.INTERNAL_SERVER_ERROR]: {
    code: 50000,
    message: 'Internal server error.',
  },
  [ErrorCode.AUTH_PASSWORD_SHORT]: {
    code: 20000,
    message: 'Password is too short.',
  },
  [ErrorCode.AUTH_INVALID_PASSWORD]: {
    code: 20001,
    message: 'Invalid password.',
  },
  [ErrorCode.AUTHORIZATION_NOT_EXIST]: {
    code: 20002,
    message: 'Authorization does not exist.',
  },
  [ErrorCode.GPM_CATALOG_REJECT_LIST_EMPTY]: {
    code: 21000,
    message: 'Catalog reject list is empty.',
  },
  [ErrorCode.GPM_CATALOG_REJECT_LIST_BE_ONE_OR_MORE]: {
    code: 21001,
    message: 'Catalog reject list should be one or more.',
  },
};

 

 

/* src/common/dtos/exception.dto.ts */

import { ErrorCode, ErrorMessageV1 } from '../types/exception.types';

export class ErrorDto {
  message: string;

  constructor(errorCode: ErrorCode) {
    this.message = `[${errorCode}] - ${
      ErrorMessageV1[ErrorCode[ErrorCode[errorCode]]].message
    }`;
  }
}

 

 

위와같이 코드 작업 후 실제 Exception을 발생시킬때,

throw new CustomError(ErrorCode.UNAUTHORIZED);

 

위와 같이 발생시키면 되며,

클라이언트에서는 Response 된 에러에서 Message에서 코드와 메시지를 사용하여 에러메시지를 공통적으로 처리할 수 있다.


반응형
반응형

detectOpenHandles는 Jest에서 제공하는 옵션 중 하나로,

비동기 작업이 완료되지 않은 핸들(예: 타이머, Promise, 소켓 등)을 테스트가 종료된 후에도 여전히 열려있는지 감지하는 데 사용됩니다. 이 옵션을 사용하면 비동기 작업이 제대로 완료되지 않아서 발생할 수 있는 메모리 누수를 식별할 수 있습니다.

 

Jest의 detectOpenHandles 옵션 사용 예시

 

    "test:e2e": "jest --config ./test/jest-e2e.json --detectOpenHandles"

 

package.json에서 위와같이 설정합니다.

반응형

+ Recent posts