[NestJS] GraphQL 적용하기

hahaha·2022년 1월 16일
0

NestJS

목록 보기
11/11

접근 방식

  1. code first
    • decotrator, TS classes 사용 → generate GraphQL schema
    • autoSchmaFile 지정: path or memory
  2. schema first
    • Graphql SDL files
    • typePaths 지정: SDL files path
      • ts-node generate-typings : TS 클래스 생성 가능

@ObjectType

@ObjectType({description: 'Author Model'})
export class Author {
	@Field(type => Int)
	id: number;

	@Field({nullable: true})
	firstName?: string;

	@Field({nullable: true)}
	lastName?: string;

	@Field(tpye => [Post])
	posts: Post[];
}
  • @Field
    • GraphQL Type 정의
      • TS ↔ GraphQL 타입 사이의 모호성이 있는 경우 필수
        • numberInt or Float
    • option
      • nullable
        • 배열 내의 항목에 대해 선언: nullable: ‘items’
        • 배열 및 배열 내의 항목에 대해 선언: nullable: ‘itemsAndList’
      • description
      • deprecationReason
  • 자동 생성되는 SDL
    • SDL: Schema Definition Language

      type Author {
        id: Int!
        firstName: String
        lastName: String
        posts: [Post]
      }

@Resolver

  • resolver function 정의 및 Query type 생성
    • REST의 Controller 역할
  • 상위 유형과 연관됨을 명시
    • @ResolveField 가 포함될 때마다 필요
    • 클래스 상단에 설정하는 방법 권장
@Resolver(of => Author)
export class AuthorsResolver {
  constructor(
    private authorsService: AuthorsService,
    private postsService: PostsService,
  ) {}

  @Query(returns => Author)
  async author(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField()
  async posts(@Parent() author: Author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }

	@Mutation(returns => Post)
	async upvotePost(@Args({ name: 'postId', type: () => Int }) postId: number) {
	  return this.postsService.upvoteById({ id: postId });
	}

	@Subscription(returns => Comment)
  commentAdded() {
    return pubSub.asyncIterator('commentAdded');
  }
}

// SDL schema
type Query {
  author(id: Int!): Author
}

type Mutation {
  upvotePost(postId: Int!): Post
}

type Subscription {
  commentAdded(): Comment!
}

Operation Type

  • @Query

    • 쿼리 핸들러 메소드
    • 쿼리 유형이름 = 쿼리 메소드 이름
      • get, find 와 같은 네이밍 지양
    • options
      • name: 쿼리 유형이름 별도 설정 가능
        • ex. @Query({ name: ‘author’ })
      • description
      • deprecationReason
      • nullable
  • @Mutation

    • 데이터를 조작할 때 사용
      • REST 의 POST 방식 역할
    • @InputType 입력 유형 선언
      @InputType()
      export class UpvotePostInput {
        @Field()
        postId: number;
      }
      
      @Mutation(returns => Post)
      async upvotePost(
        @Args('upvotePostData') upvotePostData: UpvotePostInput,
      ) {}
  • @Subscription

    • Web Socket 사용
      • 실시간 통신에서 서버에서 특정 이벤트가 발생할 때마다 클라이언트에 결과 전송
    • 활성화 설정: installSubscriptionHandlers: true
    • Pubsub
      • 구독 이벤트 발생 및 감지에 사용
        • Apollo Server 내에 자체 Pubsub 엔진 내장
      • 접근 방식
        1. 로컬 Pubsub 인스턴스화
        2. Pubsub provider 정의 및 생성자를 통한 삽입
    • Subscription
      • Pubsub#asyncIterator : 이벤트에 대한 구독 처리
        • 매개변수: triggerName
      • filter: 특정 이벤트 필터링
        @Subscription(returns => Comment, {
          filter: (payload, variables) =>
            payload.commentAdded.title === variables.title,
        })
        commentAdded(@Args('title') title: string) {
          return pubSub.asyncIterator('commentAdded');
        }
    • Publishing
      • Pubsub#publish: 이벤트 게시
        - 매개변수: triggerName, eventPayload
        - 이벤트 페이로드 모양이 구독에서 반환되는 값의 모양과 일치해야 함
        - 클라이언트 측 업데이트를 트리거하기 위해 뮤테이션내에서 자주 사용

        @Mutation(returns => Post)
        async addComment(
          @Args('postId', { type: () => Int }) postId: number,
          @Args('comment', { type: () => Comment }) comment: CommentInput,
        ) {
          const newComment = this.commentsService.addComment({ id: postId, comment });
          pubSub.publish('commentAdded', { commentAdded: newComment });
          return newComment;
        }
      • resolve: 게시된 이벤트 페이로드 변경

  • @Args

    • 인라인 방식 외에도 DTO 클래스 사용 가능
      @Args() args: GetAuthorArgs
      
      // get-author.args.ts
      @ArgsType()
      class GetAuthorArgs {
        @Field({ nullable: true })
        firstName?: string;
      
        @Field({ defaultValue: '' })
        @MinLength(3)
        lastName: string;
      }
    • options
      • type: TS 유형에서 예상되는 GraphQL 유형이 없는 경우 명시적 전달
        • @Args('id', { type: () => Int }) id: number)
      • defaultValue
      • description
      • deprecationReason
      • nullable
  • @ResolveField

    • @Parent 부모 객체 정의 필요

Scalars

  • ID

  • Int

  • Float

  • GraphQLISODateTime

  • GraphQLTimestamp

  • @Scalar

    • 사용자 지정 스칼라

    • provider로 등록 필요

      @Scalar('Date', (type) => Date)
      export class DateScalar implements CustomScalar<number, Date> {
        description = 'Date custom scalar type';
      
        parseValue(value: number): Date {
          return new Date(value); // value from the client
        }
      
        serialize(value: Date): number {
          return value.getTime(); // value sent to the client
        }
      
        parseLiteral(ast: ValueNode): Date {
          if (ast.kind === Kind.INT) {
            return new Date(ast.value);
          }
          return null;
        }
      }

Directives

  • 기본 지시문
    • @include(if: Boolean)
    • @skip(if: Boolean)
    • @deprecated(reason: String)
  • 커스텀 지시문
    • SchemaDirectiveVisitor 클래스를 확장하는 클래스 선언
      export class UpperCaseDirective extends SchemaDirectiveVisitor {
        visitFieldDefinition(field: GraphQLField<any, any>) {
          const { resolve = defaultFieldResolver } = field;
          field.resolve = async function(...args) {
            const result = await resolve.apply(this, args);
            if (typeof result === 'string') {
              return result.toUpperCase();
            }
            return result;
          };
        }
      }
    • 커스텀 지시문 클래스 등록
      GraphQLModule.forRoot({
        // ...
        schemaDirectives: {
          upper: UpperCaseDirective,
        },
      });
      • 사용방법
        • code first
          @Directive('@deprecated(reason: "This query will be removed in the next version")')
          @Query(returns => Author, { name: 'author' })
          async getAuthor(@Args({ name: 'id', type: () => Int }) id: number) {
            return this.authorsService.findOneById(id);
          }
        • schema first
          directive @upper on FIELD_DEFINITION
          
          type Post {
            id: Int!
            title: String! @upper
            votes: Int
          }

Interface

  • 특정 필드 집합을 포함하는 추상 타입
  • @InterfaceType() 으로 정의
    - TS 인터페이스는 GraphQL 인터페이스 정의에 사용 불가
@InterfaceType()
	export abstract class Character {
		@Field(type => ID)
		id: string;
        
		@Field()
        name: string;
	}
        
	// 인터페이스 구현
	@ObjectType({
		implements: () => [Character],
	})
	export class Human implements Character {
		id: string;
		name: string;
	}
profile
junior backend-developer 👶💻

0개의 댓글