Backend GraphQL API 가이드

이 문서에는 GitLab GraphQL API의 백엔드를 구현하는 엔지니어를 위한 스타일 및 기술 지침이 포함되어 있습니다.

REST API 와의 관련성

GraphQL과 REST API 섹션을 참조하십시오.

버전 관리

GraphQL API는 버전 없음입니다.

GitLab에서 GraphQL 학습

GitLab에서 GraphQL을 학습하고 싶은 백엔드 엔지니어는 GraphQL Ruby gem 가이드와 함께 이 가이드를 읽어야합니다. 이러한 가이드는 해당 gem의 기능을 가르치며, 여기에 있는 정보는 일반적으로 여기에 다시 표시되지 않습니다.

GraphQL 자체의 설계 및 기능에 대해 자세히 알아보려면 graphql.org의 가이드를 참조하십시오. 이 가이드는 GraphQL 사양의 접근 가능하지만 간략화된 버전입니다.

심층적인 탐구

2019년 3월, Nick Thomas는 GitLab 팀 멤버 전용으로 Deep Dive(깊이 있는 탐구)를 진행했습니다: https://gitlab.com/gitlab-org/create-stage/issues/1에서 GitLab GraphQL API를 소개하여 앞으로 이 코드베이스의 일부에서 작업 할 수있는 사람들과 도메인 특화 지식을 공유했습니다. 관련 내용을 YouTube에서 녹화Google 슬라이드, 그리고 PDF에서 찾을 수 있습니다. 당시와 구체적인 세부 정보는 변경되었지만 여전히 좋은 소개로 제공될 것으로 기대됩니다.

GitLab이 GraphQL을 구현하는 방법

Robert Mosolgo가 작성한 GraphQL Ruby gem을 사용합니다. 또한, GraphQL Pro의 구독을 가지고 있습니다. 자세한 내용은 GraphQL Pro 구독을 참조하십시오.

모든 GraphQL 쿼리는 단일 엔드포인트로 이동합니다 (app/controllers/graphql_controller.rb#execute) 이는 /api/graphql에서 API 엔드포인트로 노출됩니다.

GraphiQL

GraphiQL은 대화 형 GraphQL API 탐색기로 기존 쿼리를 사용하여 놀 수있는 도구입니다. 당신은 GitLab.com을 포함한 모든 GitLab 환경에서 이를 액세스 할 수 있습니다.

GraphQL 변경 사항을 검토하는 병합 요청

GraphQL 프레임워크에는 고려해야 할 몇 가지 특정 사항이 있으며, 이러한 사항을 충족시키기 위해 도메인 전문 지식이 필요합니다.

GraphQL 파일을 수정하거나 엔드포인트를 추가하는 병합 요청을 검토하도록 요청 받으면 GraphQL 리뷰 가이드를 살펴보세요.

GraphQL 로그 읽기

GraphQL 요청의 로그를 검사하고 GraphQL 쿼리의 성능을 모니터링하기 위한 팁은 GraphQL 로그 읽기 가이드를 참조하십시오.

인증

인증은 현재 GraphqlController를 통해 수행되며 지금은 레일즈 응용프로그램과 동일한 인증을 사용합니다. 따라서 세션은 공유 할 수 있습니다.

또한 쿼리 문자열에 private_token을 추가하거나 HTTP_PRIVATE_TOKEN 헤더를 추가할 수도 있습니다.

제한

GraphQL API에는 여러 가지 제한이 적용되며 이 중 일부는 개발자에 의해 재정의 될 수 있습니다.

최대 페이지 크기

기본적으로 연결은 페이지 당 정의된 최대 레코드 수를 반환 할 수 있습니다. app/graphql/gitlab_schema.rb 에서 개발자가 사용자 정의 최대 페이지 크기를 지정할 수 있습니다.

최대 복잡성

복잡성은 클라이언트를 대상으로 하는 API 페이지에서 설명되어 있습니다.

필드는 기본적으로 쿼리의 복잡성 점수에 1을 추가하나, 개발자가 사용자 정의 복잡성을 지정할 수 있습니다.

쿼리의 복잡성 점수는 그 자체로 쿼리 할 수 있습니다.

요청 시간 제한

요청은 30초 후 타임 아웃됩니다.

최대 필드 호출 수 제한

경우에 따라 특정 필드의 여러 부모 노드에서의 평가를 방지하려는 경우 여러 부모 노드에서 특정 필드의 평가를 방지하려는 경우 N+1 쿼리 문제로 인해 최적의 해결책이 없기 때문에이 문제를 방지하는 것이 좋습니다. 이는 lookahead를 사용하여 연관 관계를 미리로드시 (lookahead to preload associations)하거나 배치 사용을 고려한 후에만 사용해야합니다.

예를 들어:

# 이 사용은 예상대로 실행됩니다.
query {
  project {
    environments
  }
}

# 이 사용은 예상대로 실행되지 않습니다.
# N+1 쿼리 문제가 발생합니다. EnvironmentsResolver는 GraphQL 페이징을 선호하지 않으므로 GraphQL 배치 로더 사용할 수 없습니다.
query {
  projects {
    nodes {
      environments
    }
  }
}

이를 방지하려면 해당 필드에 Gitlab::Graphql::Limit::FieldCallCount 확장을 사용할 수 있습니다.

# 이것은 `environments` 필드에 대해 최대 1 회 호출을 허용합니다. 필드가 한 번 이상의 노드에서 평가되면 오류가 발생합니다.
field :environments do
        extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
      end

또는 resolver 클래스에서 확장을 적용할 수 있습니다.

module Resolvers
  class EnvironmentsResolver < BaseResolver
    extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
    # ...
  end
end

이 제한을 추가 할 때 영향받는 필드의 description도 업데이트되었는지 확인하십시오. 예를 들어,

field :environments,
      description: '프로젝트의 환경. 이 필드는 단일 요청에서 한 프로젝트에 대해서만 해결될 수 있습니다.'

팽이 변경 사항

GitLab GraphQL API는 버전 없는 방식을 사용하며, 개발자는 폐기 및 제거 프로세스에 익숙해져야 합니다.

팽이 변경 사항은 다음과 같습니다:

  • 필드, 인자, enum 값 또는 뮤테이션을 제거하거나 이름을 변경합니다.
  • 인자의 유형 또는 유형 이름을 변경합니다. 인자의 유형은 변수를 사용할 때 클라이언트에 의해 선언되며, 변경 시 이전 유형 이름을 사용하는 쿼리는 API에서 거부될 수 있습니다.
  • JSON으로 값이 직렬화되는 방식에 변화를 일으키는 필드 또는 enum 값의 스칼라 유형 변경입니다. 예를 들어, JSON 문자열에서 JSON 숫자로의 변경 또는 문자열 형식의 변경 등이 있습니다. 객체 유형의 모든 스칼라 유형 필드가 여전히 동일한 방식으로 직렬화되는 경우 다른 객체 유형의 변경은 허용될 수 있습니다.
  • 필드의 복잡성 또는 리졸버 내의 복잡성 배수를 높입니다.
  • 필드를 _nullable_에서 (null: false) null 가능으로 변경하거나 (null: true) 반대로 변경합니다. 이에 대해 nullable 필드에서 논의된 바 있습니다.
  • 인자를 선택적(required: false)에서 필수(required: true)로 변경합니다.
  • 연결의 최대 페이지 크기를 변경합니다.
  • 쿼리 복잡성 및 깊이의 전역 제한을 낮춥니다.
  • 이전에 허용되던 제한에 도달하게 하는 기타 사항.

아이템을 폐기하는 방법은 스키마 항목 폐기 섹션을 참조하십시오.

팽이 변경 사항 면제

GraphQL API 팽이 변경 사항 면제 문서를 참조하십시오.

글로벌 ID

GitLab GraphQL API는 글로벌 ID를 사용합니다(예: "gid://gitlab/MyObject/123"), 그리고 절대로 데이터베이스 기본 키 ID를 사용하지 않습니다.

글로벌 ID는 클라이언트 측 라이브러리에서의 캐싱 및 검색을 위해 사용되는 관행입니다.

또한 다음을 참조하십시오:

우리는 Types::GlobalIDType이라는 사용자 정의 스칼라 유형을 사용해야 합니다(입력 및 출력 인자의 유형이 GlobalID인 경우). ID 대신에 이 유형을 사용하는 이점은 다음과 같습니다:

  • 값이 GlobalID인지 확인합니다.
  • 사용자 코드로 전달되기 전에 GlobalID로 파싱합니다.
  • 객체 유형의 유형에 매개변수화할 수 있으며(예: GlobalIDType[Project]), 더 나은 유효성 검사와 보안을 제공합니다.

새로운 인자 및 결과 유형에 대해 이 유형을 사용하는 것을 고려하십시오. 이 유형을 관심사 또는 슈퍼 유형으로 매개변수화하는 것이 가능하며, 더 넓은 범위의 객체(예: GlobalIDType[Issuable]GlobalIDType[Issue])를 허용할 수 있습니다.

최적화

기본적으로 GraphQL은 N+1 문제를 도입할 수 있으며, 이를 최소화하려는 노력이 없는 한 발생할 수 있습니다.

안정성과 확장성을 위해 쿼리가 N+1 성능 문제에 시달리지 않도록 보장해야 합니다.

다음은 GraphQL 코드의 최적화를 돕는 도구 목록입니다:

  • 선행 로드는 쿼리에서 선택된 필드에 기반하여 데이터를 사전로드할 수 있도록 합니다.
  • 일괄 로드는 데이터베이스 쿼리를 일괄로 실행되도록 묶을 수 있게 합니다.
  • BatchModelLoader는 일괄로 레코드를 조회하도록 권장되며, ID를 활용합니다.
  • before_connection_authorization타입 권한 권한 확인과 관련된 N+1 문제를 해결할 수 있도록 합니다.
  • 최대 필드 호출 횟수 제한을 설정하여 최적화할 수 없는 경우 데이터를 반환할 수 있는 필드 호출 횟수를 제한할 수 있습니다.

개발 중 N+1 문제 확인 방법

N+1 문제는 다음과 같이 기능을 개발하는 동안 확인할 수 있습니다:

  • 데이터 컬렉션을 반환하는 GraphQL 쿼리를 실행하는 동안 development.log를 추적합니다. Bullet을 사용할 수 있습니다.
  • GitLab UI에서 쿼리를 실행하는 경우 성능 바를 관찰합니다.
  • 기능을 추가하는 동안 N+1 문제를 확인하는 요청 스펙을 추가합니다.

필드

유형

우리는 코드 우선 스키마를 사용하며, 모든 것의 유형을 루비로 선언합니다.

예를 들어, app/graphql/types/project_type.rb:

graphql_name 'Project'

field :full_path, GraphQL::Types::ID, null: true
field :name, GraphQL::Types::String, null: true

각 유형에 이름(이 경우 Project)을 지정합니다.

full_pathname스칼라 GraphQL 유형입니다. full_pathGraphQL::Types::ID입니다 (GraphQL::Types::ID를 사용해야 하는 경우를 참조하십시오). name은 일반 GraphQL::Types::String 유형입니다. 스칼라 데이터 유형(예: TimeType)에 대한 사용자 정의 GraphQL 데이터 유형을 선언할 수도 있습니다.

GraphQL API를 통해 모델을 노출할 때, app/graphql/types에 새로운 유형을 만들어 수행합니다.

유형에서 속성을 노출할 때, 내부 로직을 최소한으로 유지하고 정의에 로직을 결합하지 말아야 합니다. 대신, 프리젠터 내부로 어떤 로직을 이동하는 것을 고려하십시오:

class Types::MergeRequestType < BaseObject
  present_using MergeRequestPresenter

  name 'MergeRequest'
end

기존 프리젠터를 사용할 수 있지만, GraphQL을 위해 특별히 새로운 프리젠터를 만드는 것도 가능합니다.

프리젠터는 필드에서 해결된 객체와 컨텍스트를 사용하여 초기화됩니다.

Nullable fields

GraphQL은 필드를 “nullable” 또는 “non-nullable”로 허용합니다. “nullable”는 특정 타입의 값 대신 null이 반환될 수 있다는 것을 의미합니다. 일반적으로 다음과 같은 이유로 non-nullable 필드 대신 nullable 필드를 사용하는 것이 좋습니다.

  • 데이터가 필요한 상태에서 필요하지 않은 상태로 전환되는 것이 일반적이기 때문입니다.
  • 필드가 옵셔널 상태가 아닐지라도, 쿼리 시에 해당 필드가 사용 불가능할 수 있습니다.
    • 예를 들어, blob의 content는 Gitaly에서 조회해야 할 수 있습니다.
    • content가 nullable하다면 전체 쿼리를 실패시키지 않고 부분 응답을 반환할 수 있습니다.
  • 버전이 없는 스키마에서 non-nullable 필드를 nullable 필드로 변경하는 것은 어렵습니다.

Non-nullable 필드는 필수 필드이고 미래에 옵셔널 상태가 되기 힘들며, 쉽게 계산될 때에만 사용해야 합니다. 예를 들어, id 필드가 해당됩니다.

non-nullable GraphQL 스키마 필드는 느낌표 !로 끝나는 객체 유형입니다. gitlab_schema.graphql 파일에서 다음과 같은 예시가 있습니다.

  id: ProjectID!

다음은 non-nullable GraphQL 배열의 예시입니다.

  errors: [String!]!

자세한 내용은 아래에서 확인할 수 있습니다.

Exposing Global IDs

GitLab이 Global IDs를 사용하는 방식과 일치하게, 데이터베이스 주 키 ID를 노출할 때 항상 Global IDs로 변환해야 합니다.

id로 지정된 모든 필드는 자동으로 변환됩니다 object의 Global ID로.

id로 명명되지 않은 필드는 수동으로 변환해야 합니다. 우리는 Gitlab::GlobalID.build 를 사용하거나, GlobalID::Identification 모듈이 섞인 객체에서 #to_global_id를 호출하여 이 작업을 수행할 수 있습니다.

예시: Types::Notes::DiscussionType에서 예시를 확인할 수 있습니다.

field :reply_id, Types::GlobalIDType[Discussion]

def reply_id
  Gitlab::GlobalId.build(object, id: object.reply_id)
end

GraphQL::Types::ID 사용 시기

GraphQL::Types::ID를 사용하면 해당 필드는 GraphQL ID 유형이 되며, JSON 문자열로 직렬화됩니다. 그러나 ID는 클라이언트에게 특별한 의미를 가집니다. GraphQL 사양은 다음과 같이 말합니다.

ID 스칼라 유형은 주로 고유 식별자를 나타내며, 객체를 다시 가져오거나 캐시의 키로 사용됩니다.

GraphQL 사양은 ID의 고유성에 대한 범위를 명확하게하지 않습니다. GitLab은 ID가 최소한 유형 이름별로 고유해야 한다고 결정했습니다. 유형 이름은 Types:: 클래스 중 하나의 graphql_name입니다. 예를 들어 ProjectIssue 등이 해당됩니다.

이에 따라 다음이 요약됩니다.

  • Project.fullPathID여야 합니다. 왜냐하면 API 전반에 걸쳐 해당 fullPath를 가진 다른 Project가 없기 때문이며, 해당 필드는 식별자이기도 합니다.
  • Issue.iidID가 아니어야 합니다. 왜냐하면 API 전반에 걸쳐 동일한 iid를 가진 많은 Issue 유형이 있을 수 있기 때문입니다. 다른 프로젝트의 Issue 캐시가 있는 경우 ID로 취급될 시 문제가 발생할 수 있습니다.
  • Project.id는 일반적으로 ID가 되는 자격이 있습니다. 왜냐하면 해당 ID 값으로는 하나의 Project만 있을 수 있기 때문입니다. 다만 우리는 데이터베이스 ID 값에 대해 Global ID types를 사용하기 때문에 이를 Global ID 대신 타입 지정합니다.

다음 표에서 확인할 수 있습니다:

필드 목적 GraphQL::Types::ID 사용 여부
Full path
데이터베이스 ID 아니요
IID 아니요

markdown_field

markdown_fieldfield를 래핑하는 도우미 메서드로, 렌더링된 마크다운을 반환하는 필드에 항상 사용해야 합니다.

이 도우미는 현재 GraphQL 쿼리의 컨텍스트에서 이미 있는 MarkupHelper를 사용하여 모델의 마크다운 필드를 렌더링합니다.

HTML 렌더링 시 쿼리가 발생할 수 있기 때문에, 이러한 필드의 복잡성이 기본값보다 5가 높아집니다.

마크다운 필드 도우미는 다음과 같이 사용할 수 있습니다.

markdown_field :note_html, null: false

이렇게 하면 모델의 마크다운 필드 note를 렌더링하는 필드가 생성됩니다. method: 인수를 추가하여 재정의할 수 있습니다.

markdown_field :body_html, null: false, method: :note

기본적으로 필드에는 다음과 같은 설명이 부여됩니다:

note의 GitLab 특화 마크다운 렌더링

description: 인수를 전달하여 이를 재정의할 수 있습니다.

연결 유형

참고: 구현 세부 정보는 페이지네이션 구현을 참조하십시오.

GraphQL은 커서 기반 페이징을 사용하여 항목 컬렉션을 노출합니다. 이를 통해 클라이언트에게 많은 유연성을 제공하면서 백엔드에서 다양한 페이지네이션 모델을 사용할 수 있습니다.

리소스 컬렉션을 노출하기 위해 연결 유형을 사용할 수 있습니다. 예를 들어 프로젝트-파이프라인에 대한 쿼리는 다음과 같이 보일 수 있습니다:

query($project_path: ID!) {
  project(fullPath: $project_path) {
    pipelines(first: 2) {
      pageInfo {
        hasNextPage
        hasPreviousPage
      }
      edges {
        cursor
        node {
          id
          status
        }
      }
    }
  }
}

이는 프로젝트의 처음 2개 파이프라인과 관련된 페이징 정보를 반환하고, 내림차순으로 정렬됩니다. 반환된 데이터는 다음과 같을 것입니다:

{
  "data": {
    "project": {
      "pipelines": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": false
        },
        "edges": [
          {
            "cursor": "Nzc=",
            "node": {
              "id": "gid://gitlab/Pipeline/77",
              "status": "FAILED"
            }
          },
          {
            "cursor": "Njc=",
            "node": {
              "id": "gid://gitlab/Pipeline/67",
              "status": "FAILED"
            }
          }
        ]
      }
    }
  }
}

다음 페이지를 가져오려면 마지막으로 알려진 요소의 커서를 전달할 수 있습니다:

query($project_path: ID!) {
  project(fullPath: $project_path) {
    pipelines(first: 2, after: "Njc=") {
      pageInfo {
        hasNextPage
        hasPreviousPage
      }
      edges {
        cursor
        node {
          id
          status
        }
      }
    }
  }
}

일관된 정렬을 위해 기본 키에 정렬을 추가합니다. 기본 키는 일반적으로 id이므로, 관계 끝에 order(id: :desc)를 추가합니다. 기본 키는 기본 테이블에서 반드시 사용 가능해야 합니다.

#### 바로 가기 필드

어떤 경우에는 "바로 가기 필드"를 구현하는 것이 간단해 보일 수 있습니다. 파라미터가 전달되지 않으면 리졸버가 컬렉션의 첫 번째 값을 반환하도록 하는 것입니다. 이러한 "바로 가기 필드"는 유지 관리 오버헤드를 초래하기 때문에 권장되지 않습니다. 이러한 필드들은 정식 필드와 동기화 유지와 의존성이 있습니다. 정식 필드가 변경되면 이러한 필드들도 폐기되거나 수정해야 합니다. 특별한 이유가 없는 이상, 프레임워크가 제공하는 기능을 사용하세요.

예를 들어, `latest_pipeline` 대신 `pipelines(last: 1)`을 사용하세요.

#### 페이지 크기 제한

기본적으로 API는 연결당 최대 레코드 수를 반환하며, 이 값은 클라이언트가 제한 인자(`first:` 또는 `last:`)를 제공하지 않은 경우에도 기본 페이지당 반환되는 레코드 수입니다. 

`max_page_size` 인자를 사용하여 연결에 대한 다른 페이지 크기 제한을 지정할 수 있습니다.

경고:
`max_page_size`를 높이는 것보다 프론트엔드 클라이언트나 제품 요구 사항을 수정하여 페이지 당 레코드 수가 많이 필요하지 않도록 하는 것이 더 좋습니다. 기본값은 GraphQL API의 성능 유지를 보장하기 위해 설정되었습니다.

예를 들어:

```ruby
field :tags,
  Types::ContainerRepositoryTagType.connection_type,
  null: true,
  description: '컨테이너 저장소의 태그',
  max_page_size: 20

필드 복잡성

GitLab GraphQL API는 쿼리의 과도한 복잡성을 제한하기 위해 복잡성 점수를 사용합니다. 복잡성에 대한 설명은 우리 클라이언트 문서에서 확인할 수 있습니다.

기본적으로 필드는 쿼리의 복잡성 점수에 1을 추가합니다. 필드별로 사용자 정의 complexity 값을 제공하여 재정의할 수 있습니다.

서버에 데이터를 반환하는 데 더 많은 작업_을 유발하는 필드에 대해 더 높은 복잡성을 지정해야 합니다. 대부분의 경우 데이터를 거의 또는 전혀 _작업 없이 반환할 수 있는 필드, 예를 들어 대부분의 경우 id 또는 title,의 복잡성을 0으로 지정할 수 있습니다.

calls_gitaly

Gitaly 호출을 수행할 수 있는 가능성이 있는 필드는 calls_gitaly: truefield를 정의할 때 전달하여 표시해야 합니다.

예를 들어:

field :blob, type: Types::Snippets::BlobType,
      description: '스니펫 blob',
      null: false,
      calls_gitaly: true

이로써 필드의 복잡성 점수가 1만큼 증가합니다.

리졸버가 Gitaly를 호출하는 경우 BaseResolver.calls_gitaly!로 주석 처리할 수 있습니다. 이는 이 리졸버를 사용하는 모든 필드에 대해 calls_gitaly: true를 전달합니다.

예를 들어:

class BranchResolver < BaseResolver
  type ::Types::BranchType, null: true
  calls_gitaly!

  argument name: ::GraphQL::Types::String, required: true

  def resolve(name:)
    object.branch(name)
  end
end

그런 다음 BranchResolver를 사용할 때 calls_gitaly:에 올바른 값을 사용합니다.

유형에 대한 권한 노출

현재 사용자가 리소스에 대해 가지고 있는 권한을 노출하려면, 리소스에 대한 권한을 나타내는 별도의 유형을 전달하여 expose_permissions를 호출할 수 있습니다.

예를 들어:

module Types
  class MergeRequestType < BaseObject
    expose_permissions Types::MergeRequestPermissionsType
  end
end

권한 유형은 BasePermissionType를 상속하며 리소스에 대한 권한을 노출하는 데 도움이 되는 몇 가지 도우미 메서드를 포함합니다.

class MergeRequestPermissionsType < BasePermissionType
  graphql_name 'MergeRequestPermissions'

  present_using MergeRequestPresenter

  abilities :admin_merge_request, :update_merge_request, :create_note

  ability_field :resolve_note,
                description: '사용자가 합병 요청의 토론을 해결할 수 있는지 여부를 나타냅니다.'
  permission_field :push_to_source_branch, method: :can_push_to_source_branch?
end
  • permission_field: graphql-rubyfield 메서드와 동일하게 작동하지만 기본 설명 및 유형을 설정하고 이들을 변경할 수 없는 논-널러블로 만듭니다. 이 옵션은 여전히 인수로 추가하여 재정의할 수 있습니다.
  • ability_field: 정책에서 정의된 능력을 노출합니다. 이것은 permission_field와 동일하게 작동하며 같은 인수를 재정의합니다.
  • abilities: 한 번에 여러 정책에서 정의된 능력을 노출할 수 있습니다. 이들을 위한 필드는 모두 기본 설명이 있는 논-널러블 불리언이어야 합니다.

기능 플래그

GraphQL에서 기능 플래그를 구현하여 다음을 토글할 수 있습니다.

  • 필드의 반환 값.
  • 인수 또는 뮤테이션의 동작.

이는 귀하의 선호 및 상황에 따라 리졸버에서, 유형에서 또는 심지어 모델 메서드에서 수행할 수 있습니다.

참고: 기능 플래그로부터 수정 또는 제거된 경우, Alpha 속성을 제거하여 이를 공개 상태로 만들지 않아도 되기 때문에 항목을 Alpha로 표시하는 것이 권장됩니다. 뿐만 아니라 “제품 변경 면제”를 거치지 않고도 언제든지 Alpha 항목을 변경하거나 삭제할 수도 있습니다. 플래그가 제거된 경우, 스키마 항목을 공개 상태로 만들기 위해 이 Alpha 속성을 제거합니다.

기능 플래그 항목에 대한 설명

스키마 항목의 값을 또는 동작을 토글하는 데 기능 플래그를 사용하는 경우, 항목의 description은 다음을 반드시 포함해야 합니다.

  • 값을 또는 동작을 기능 플래그로 토글할 수 있다고 명시합니다.
  • 기능 플래그의 이름을 명시합니다.
  • 기능 플래그를 해제(또는 활성화)할 때의 필드 반환 값을 또는 동작을 명시합니다.

기능 플래그 사용 예

기능 플래그가 적용된 필드

기능 플래그 상태에 따라 필드 값이 토글됩니다. 기능 플래그가 비활성화된 경우 null을 반환하는 경우가 일반적입니다:

field :foo, GraphQL::Types::String, null: true,
      alpha: { milestone: '10.0' },
      description: '`my_feature_flag` 기능 플래그가 비활성화되어 있는 경우 `null`을 반환하는 일부 테스트 필드입니다.'

def foo
  object.foo if Feature.enabled?(:my_feature_flag, object)
end

#### Feature-flagged argument

기능 플래그 상태에 따라 인수를 무시하거나 값을 변경할 수 있습니다.
일반적으로 기능 플래그가 비활성화된 경우 인수를 무시하는 데 사용됩니다:

```ruby
argument :foo, type: GraphQL::Types::String, required: false,
         alpha: { milestone: '10.0' },
         description: '`my_feature_flag` 기능 플래그가 비활성화된 경우 무시되는 일부 테스트 인수입니다.'

def resolve(args)
  args.delete(:foo) unless Feature.enabled?(:my_feature_flag, object)
  # ...
end

Feature-flagged mutation

기능 플래그 상태로 인해 수행할 수 없는 변이는 복구할 수 없는 변이 오류로 처리됩니다. 오류는 최상위 수준에서 반환됩니다:

description '`my_feature_flag` 기능 플래그가 비활성화된 경우 개체를 변이시키지 않습니다.'

def resolve(id: )
  object = authorized_find!(id: id)

  raise_resource_not_available_error! '`my_feature_flag` 기능 플래그가 비활성화되었습니다.' \
    if Feature.disabled?(:my_feature_flag, object)
  # ...
end

스키마 항목의 유지보수 중단

GitLab GraphQL API는 버전이 없으며, 모든 변경 사항에서 이전 버전의 API와의 하위 호환성을 유지합니다.

필드, 인수, 열거형 값 또는 변이을 제거하는 대신, 대신 _유지보수 중단_해야 합니다.

GraphQL에서 스키마 항목의 유지보수를 위해:

  1. 유지보수를 위한 이슈 생성하기
  2. 스키마에서 항목을 유지보수 중단으로 표시하기

관련 항목:

유지보수 이슈 생성

모든 GraphQL 유지보수에는 해당 유지보수 및 제거를 추적하는 Deprecations` 이슈 템플릿을 사용하여 생성해야 합니다.

이 유지보수 이슈에 다음 두 가지 라벨을 적용하십시오:

  • ~GraphQL
  • ~deprecation

항목을 유지보수 중단으로 표시하기

필드, 인수, 열거 값 및 변이는 deprecated 속성을 사용하여 중지되었습니다. 속성 값은 다음의 해시입니다:

  • reason - 유지보수 중단의 이유
  • milestone - 필드가 중지되었던 마일스톤

예시:

field :token, GraphQL::Types::String, null: true,
      deprecated: { reason: '토큰을 통한 로그인이 제거되었습니다.', milestone: '10.0' },
      description: '로그인용 토큰.'

유지된 스키마 항목의 원래 description은 유지되어야 하며, 유지보수는 _유지보수라는 내용을 명시적으로 언급하도록 업데이트해서는 안됩니다. 대신, reasondescription에 추가합니다.

유지보수 이유 스타일 가이드

유지보수의 이유가 해당 필드, 인수 또는 열거 값이 대체되기 때문인 경우 reason으로 대체 사항을 나타내어야 합니다. 예를 들어, 대체된 필드를 위한 다음은 reason입니다:

`otherFieldName`을 사용하세요.

예시:

field :designs, ::Types::DesignManagement::DesignCollectionType, null: true,
      deprecated: { reason: '`designCollection`을 사용하세요.', milestone: '10.0' },
      description: '이 이슈와 관련된 디자인입니다.',
module Types
  class TodoStateEnum < BaseEnum
    value 'pending', deprecated: { reason: 'PENDING을 사용하세요.', milestone: '10.0' }
    value 'done', deprecated: { reason: 'DONE을 사용하세요.', milestone: '10.0' }
    value 'PENDING', value: 'pending'
    value 'DONE', value: 'done'
  end
end

만약 유지보수되는 필드, 인수 또는 열거 값이 대체되지 않는 경우에는 설명적인 유지보수 reason을 제공해야 합니다.

전역 ID 유지보수하기

우리는 rails/globalid 젬을 사용하여 전역 ID를 생성하고 구문 분석하기 때문에 이들은 모델 이름에 결합됩니다. 모델 이름을 바꾸면 해당 전역 ID도 변경됩니다.

만일 전역 ID가 스키마 어디에선가 인수 유형으로 사용된다면, 그리고 전역 ID 변경은 일반적으로 파괴적인 변경으로 간주됩니다.

이전의 전역 ID 인수를 계속 지원하려면 Gitlab::GlobalId::Deprecations에 유지보수를 추가하십시오.

참고: 전역 ID가 필드로만 노출되는 경우에는 유지보수할 필요가 없습니다. 전역 ID가 필드에서 표현되는 방식을 역 호환성으로 간주합니다. 우리는 클라이언트가 이러한 값을 구문 분석하지 않을 것으로 기대합니다: 그들은 불투명한 토큰으로 취급되어야 하며 그 중의 구조는 우연한 것으로 간주되어서는 안됩니다.

예시 시나리오:

이 예시 시나리오는 이 MR을 기반으로 합니다.

PrometheusService라는 모델은 Integrations::Prometheus로 이름이 변경됩니다. 이전 모델 이름은 전역 ID 유형을 만들기 위해 사용되는데:

# Mutations::UpdatePrometheus:

argument :id, Types::GlobalIDType[::PrometheusService],
              required: true,
              description: "변형시킬 통합의 ID."

클라이언트는 PrometheusServiceID로 명명된, input.id 인수로 "gid://gitlab/PrometheusService/1"와 같이 보이는 전역 ID 문자열을 전달하여 변이를 호출합니다:

mutation updatePrometheus($id: PrometheusServiceID!, $active: Boolean!) {
  prometheusIntegrationUpdate(input: { id: $id, active: $active }) {
    errors
    integration {
      active
    }
  }
}

우리는 모델을 Integrations::Prometheus로 바꾸고, 새로운 이름으로 코드베이스를 업데이트합니다. 이후에 변이를 업데이트할 때, 변경된 모델을 Types::GlobalIDType[]로 전달합니다:

# Mutations::UpdatePrometheus:

argument :id, Types::GlobalIDType[::Integrations::Prometheus],
              required: true,
              description: "변형시킬 통합의 ID."

이것은 변이에 파괴적인 변경을 초래하며, API가 "gid://gitlab/PrometheusService/1"로 인수를 전달하는 클라이언트나 쿼리 서명에서 PrometheusServiceID 유형을 지정하는 클라이언트를 거부합니다.

클라이언트가 변이와 상호작용할 수 있도록 허용하기 위해서는 DEPRECATIONS 상수에서 Gitlab::GlobalId::Deprecations에 새로운 Deprecation을 추가하십시오.

그런 다음 우리의 정규 유지보수 프로세스를 따르십시오. 나중에 이전 인수 스타일의 지원을 제거하려면 Deprecation을 제거하십시오.

유지보수 기간 동안 API는 인수 값에 다음 형식 중 하나를 허용합니다:

  • "gid://gitlab/PrometheusService/1"
  • "gid://gitlab/Integrations::Prometheus/1"

API는 또한 인수의 쿼리 서명에 다음 유형을 허용합니다:

  • PrometheusServiceID
  • IntegrationsPrometheusID

참고: 이 예에선 이전 유형(PrometheusServiceID)을 사용하는 쿼리는 API에게 유효하며 실행 가능하지만, 유효성 검사 도구에서는 유효하지 않다는 것을 명심해주세요. 이유는 우리가@deprecated 지침 외부적인 방법을 사용하는 것을 유지보수하고 있는데, 따라서 유효성 검사 도구에서는 이를 인식하지 못합니다.

문서는 현재 이전의 전역 ID 스타일이 유지보수된 사실을 언급합니다.

알파로 스키마 항목 표시

GraphQL 스키마 항목(필드, 매개변수, enum 값 및 뮤테이션)을 알파로 표시할 수 있습니다.

알파로 표시된 항목은 퇴폐화(deprecation) 프로세스에서 제외되며 언제든 알림 없이 제거할 수 있습니다. 항목을 알파로 표시하여 변경 사항에 노출되고 공개 사용 준비가 되지 않은 경우에 사용하세요.

참고: 새로운 항목만 알파로 표시하세요. 이미 공개된 항목을 알파로 표시하지 마세요.

스키마 항목을 알파로 표시하려면 alpha: 키워드를 사용해야 합니다. 알파 항목이 도입된 milestone:을 제공해야합니다.

예를 들면:

field :token, GraphQL::Types::String, null: true,
      alpha: { milestone: '10.0' },
      description: '로그인용 토큰.'

유사하게, app/graphql/types/mutation_type.rb에서 뮤테이션이 마운트된 위치를 업데이트하여 전체 뮤테이션을 알파로도 표시할 수 있습니다:

mount_mutation Mutations::Ci::JobArtifact::BulkDestroy, alpha: { milestone: '15.10' }

알파 GraphQL 항목은 GraphQL 퇴폐화를 활용한 사용자 지정 GitLab 기능입니다. 알파 항목은 GraphQL 스키마에서 퇴폐된 것처럼 나타납니다. 모든 퇴폐된 스키마 항목과 마찬가지로 알파 필드를 대화형 GraphQL 엑스플로러(GraphiQL)에서 테스트할 수 있습니다. 그러나 GraphiQL 자동 완성 편집기는 퇴폐된 필드를 제안하지 않는 점을 유의하세요.

항목은 생성된 GraphQL 설명서 및 그 GraphQL 스키마 설명에 알파로 표시됩니다.

Enum

GitLab GraphQL enum은 app/graphql/types에 정의됩니다. 새로운 enum을 정의할 때 다음 규칙이 적용됩니다:

  • 값은 대문자여야 합니다.
  • 클래스 이름은 Enum 문자열로 끝나야 합니다.
  • graphql_name에는 Enum 문자열을 포함해서는 안 됩니다.

예를 들면:

module Types
  class TrafficLightStateEnum < BaseEnum
    graphql_name 'TrafficLightState'
    description '신호등의 상태'

    value 'RED', description: '운전자는 정지해야 합니다.'
    value 'YELLOW', description: '안전할 때만 운전자가 정지해야 합니다.'
    value 'GREEN', description: '운전자는 출발하거나 계속 운전할 수 있습니다.'
  end
end

Ruby의 대문자 문자열이 아닌 경우 클래스 속성용 enum은 value: 옵션을 제공할 수 있습니다.

다음 예제에서:

  • OPENED의 GraphQL 입력은 'opened'로 변환됩니다.
  • 'opened'의 Ruby 값은 GraphQL 응답에서 "OPENED"로 변환됩니다.
module Types
  class EpicStateEnum < BaseEnum
    graphql_name 'EpicState'
    description 'GitLab epic의 상태'

    value 'OPENED', value: 'opened', description: '열린 Epic.'
    value 'CLOSED', value: 'closed', description: '닫힌 Epic.'
  end
end

Enum 값은 스키마 항목 퇴치를 사용하여 퇴폐될 수 있습니다.

레일즈 enum으로부터 동적으로 GraphQL enum 정의하기

만약 GraphQL enum이 레일즈 enum에 의해 지원된다면, 레일즈 enum을 사용하여 동적으로 GraphQL enum 값을 정의하는 것을 고려하세요. 값이 레일즈 enum에 추가되면 GraphQL enum이 자동으로 변경 사항을 반영하므로 GraphQL enum 값을 동적으로 정의합니다.

예제:

module Types
  class IssuableSeverityEnum < BaseEnum
    graphql_name 'IssuableSeverity'
    description '사건 심각도'

    ::IssuableSeverity.severities.each_key do |severity|
      value severity.upcase, value: severity, description: "#{severity.titleize} 심각도."
    end
  end
end

JSON

GraphQL이 반환하는 데이터가 JSON으로 저장되는 경우 가능한 경우에는 계속해서 GraphQL 타입을 사용해야 합니다. JSON 데이터의 구조가 변동적이지만 알려진 가능한 구조 집합 중 하나인 경우 union를 사용하세요. 이용 사례로는 !30129가 있습니다.

필요에 따라 필드 이름을 hash_key: 키워드를 사용하여 해시 데이터 키에 매핑할 수 있습니다.

예를 들면, 다음 JSON 데이터가 주어진 경우:

{
  "title": "내 차트",
  "data": [
    { "x": 0, "y": 1 },
    { "x": 1, "y": 1 },
    { "x": 2, "y": 2 }
  ]
}

다음과 같이 GraphQL 타입을 사용할 수 있습니다:

module Types
  class ChartType < BaseObject
    field :title, GraphQL::Types::String, null: true, description: '차트 제목.'
    field :data, [Types::ChartDatumType], null: true, description: '차트 데이터.'
  end
end

module Types
  class ChartDatumType < BaseObject
    field :x, GraphQL::Types::Int, null: true, description: '차트 데이터 x축 값.'
    field :y, GraphQL::Types::Int, null: true, description: '차트 데이터 y축 값.'
  end
end

설명

모든 필드 및 매개변수는 설명이 있어야 합니다.

필드 또는 매개변수의 설명은 description: 키워드를 사용하여 제공됩니다. 예를 들면:

field :id, GraphQL::Types::ID, description: '이슈의 ID.'
field :confidential, GraphQL::Types::Boolean, description: '이슈가 비밀 여부를 나타냅니다.'
field :closed_at, Types::TimeType, description: '이슈가 닫힌 타임스탬프.'

필드 및 매개변수의 설명은 다음에서 확인할 수 있습니다:

설명 스타일 가이드

언어 및 구두점

필드 및 매개변수를 설명할 때 가능한 경우{x}의 {y}를 사용하여 {x}를 설명하는 항목이 적용되는 리소스{y}를 사용하세요.

이슈의 ID.
이픽의 작성자.

정렬이나 검색을 수행하는 매개변수의 경우 적절한 동사로 시작하세요. 지정된 값이나 간단함을 나타내기 위해 the giventhe specified 대신 this를 사용할 수 있습니다.

예를 들면:

이 기준으로 이슈를 정렬합니다.

일관성과 간결함을 위해 설명을 TheA로 시작하지 마세요.

모든 설명은 마침표(.)로 끝나야 합니다.

부울

부울 필드(GraphQL::Types::Boolean)의 경우, 해당 기능을 설명하는 동사로 시작합니다. 예를 들면:

이슈가 비밀이라는 것을 나타냅니다.

필요한 경우 기본값을 제공합니다. 예를 들면:

이슈를 비밀로 설정합니다. 기본값은 false입니다.

정렬 enum

정렬을 위한 enum'값은 {x}에 대한 정렬입니다.' 라는 설명을 가져야 합니다. 예를 들면:

컨테이너 저장소를 정렬하기 위한 값입니다.

Types::TimeType 필드 설명

Types::TimeType GraphQL 필드의 경우, 속성의 형식이 Date가 아닌 시간임을 알려주는 timestamp라는 단어를 포함해야 합니다.

예시: ruby field :closed_at, Types::TimeType, description: '이슈가 닫힌 시간입니다.'

copy_field_description 도우미

가끔 두 가지 설명이 항상 동일하도록 보장하려고 합니다. 예를 들어, 타입 필드 설명을 동일한 속성을 나타내는 변이 인자와 같게 유지하려고 합니다.

설명을 제공하는 대신, copy_field_description 도우미를 사용할 수 있습니다. 이 도우미에 타입과 설명을 복사하려는 필드 이름을 전달합니다.

예시: ruby argument :title, GraphQL::Types::String, required: false, description: copy_field_description(Types::MergeRequestType, :title)

문서 참조

가끔 우리는 설명에서 외부 URL을 참조하려 합니다. 이를 더 쉽게 만들고 생성된 참조 문서에 적절한 마크업을 제공하기 위해, 필드에 see 속성을 제공합니다.

예를 들면:

field :genus,
      type: GraphQL::Types::String,
      null: true,
      description: '분류 속의 종입니다.'
      see: { '계통 품종에 대한 위키백과 문서' => 'https://wikipedia.org/wiki/Genus' }

이것은 문서에서 다음과 같이 렌더링됩니다:

분류 속의 종입니다. 참조: [계통 품종에 대한 위키백과 문서](https://wikipedia.org/wiki/Genus)

여러 문서 참조를 제공할 수 있습니다. 이 속성의 구문은 키가 텍스트 설명이고 값이 URL인 HashMap입니다.

구독 티어 뱃지

만약 다른 필드보다 높은 구독 티어에서 이용할 수 있는 필드나 인자가 있다면, 내장 구독 가능성 설명을 추가합니다.

예를 들면: ruby description: '사용자 정의 템플릿의 전체 경로. 프리미엄 및 얼티밋 전용입니다.'

권한

참조: GraphQL 권한

리졸버

app/graphql/resolvers 디렉터리에 저장된 _리졸버_를 사용하여 응용 프로그램이 응답을 제공하는 방법을 정의합니다. 리졸버는 응답에 표시할 객체를 찾는 데 사용할 수 있으며 app/graphql/resolvers에 리졸버를 추가할 수 있습니다.

인자는 변이와 동일한 방식으로 리졸버에서 정의할 수 있습니다. 인자 섹션을 참조하세요.

수행하는 쿼리를 제한하기 위해 BatchLoader를 사용할 수 있습니다.

리졸버 작성

우리의 코드는 보통 조회기 및 서비스를 둘러싼 얇은 선언적 래퍼이어야 합니다. 목록을 반복하거나 관심사로 추출할 수 있습니다. 대부분의 경우 상속보다는 구성이 선호됩니다. 리졸버를 컨트롤러처럼 취급하세요: 리졸버는 다른 응용 프로그램 추상화를 구성하는 DSL이어야 합니다.

예시: ```ruby class PostResolver < BaseResolver type Post.connection_type, null: true authorize :read_blog description ‘블로그 게시물, 선택적으로 이름으로 필터링됨’

argument :name, [::GraphQL::Types::String], required: false, as: :slug

alias_method :blog, :object

def resolve(**args) PostFinder.new(blog, current_user, args).execute end end ```

이 같은 리졸버 클래스를 두 개의 다른 위치, 예를 들어, 동일한 객체를 노출하는 두 개의 다른 필드에서 사용할 수 있지만, 리졸버 개체를 직접 재사용해서는 안 됩니다. 리졸버에는 복잡한 수명 주기가 있으며 인가, 준비 및 결의가 프레임워크에 의해 조정되며 각 단계에서 게으른 값을 사용하여 일괄 처리 기회를 활용할 수 있습니다. 응용 프로그램 코드에서 리졸버나 변이를 생성하지 말아야 합니다.

리졸버를 이용하는 코드의 재사용 단위는 응용 프로그램의 다른 부분과 매우 유사합니다:

  • 데이터 조회를 위한 쿼리 내 검색기.
  • 작업 적용을 위한 변이 내 서비스.
  • 쿼리에 특화된 로더(일괄 처리 가능한 조회기).

변이에 일괄 처리를 사용할 이유가 전혀 없습니다. 변이는 연속으로 실행되므로 일괄 처리 기회가 없습니다. 권한이 있는 객체를 직접 조회해도 무방합니다. 리졸버 또는 BaseObject의 메서드 등에서만 일괄 처리 기회를 허용하고자 하는 경우 사용할 수 있습니다.

오류 처리

리졸버는 적절한 최상위 오류로 변환되는 오류를 발생시킬 수 있습니다. 모든 예견된 오류는 적절한 GraphQL 오류로 잡아서 변환해야 합니다(참조: Gitlab::Graphql::Errors). 잡히지 않은 오류는 억제되고 클라이언트는 내부 서비스 오류 메시지를 받습니다.

이 하나의 특별한 사례는 권한 오류입니다. REST API에서는 사용자가 액세스 권한이 없는 리소스에 대해 404 Not Found를 반환합니다. 이러한 동작은 GraphQL에서는 존재하지 않거나 사용 권한이 없는 리소스에 대해 null을 반환해야 합니다. 쿼리 리졸버는 권한이없는 리소스에 대해 오류를 발생해서는 안 됩니다.

이것에 대해 걱정할 필요가 거의 없습니다 - 이는 우리가 authorize DSL 호출을 사용하여 선언된 리졸버 필드 권한이 올바르게 처리된다는 것입니다. 그러나 더 사용자 정의할 필요가 있으면 기억하세요. 필드를 해결할 때 current_user가 접근 권한이 없는 개체를 만나면 전체 필드가 null로 해결되어야 합니다.

파생 리졸버

(BaseResolver.singleBaseResolver.last를 포함하여)

일부 사용 사례에는 다른 리졸버에서 리졸버를 도출할 수 있습니다. 주된 사용 사례는 모든 항목을 찾아내는 한 리졸버와 특정 항목을 찾아내는 데 다른 리졸버를 사용하는 것입니다. 이를 위해 편리한 메서드를 제공합니다.

  • 첫 번째 항목을 선택하는 새 리졸버를 구성하는 BaseResolver.single
  • 마지막 항목을 선택하는 리졸버로 구성하는 BaseResolver.last

정확한 단수형 타입은 컬렉션 타입에서 추론되므로 여기에서 타입을 정의할 필요가 없습니다.

이러한 메서드를 사용하기 전에 다른 리졸버를 작성하거나 쿼리를 추상화하는 관심사를 작성하는 것이 더 간단한지 생각해보십시오.

BaseResolver.single를 너무 자유롭게 사용하면 안티 패턴이 될 수 있습니다. 예를 들어, 인수가 없으면 처음 MR을 리턴하는 Project.mergeRequest 필드와 같은 비의미 있는 필드로 이어질 수 있습니다. 단일 리졸버를 유래의 컬렉션 리졸버에서 생성하는 모든 경우에는 더 제한적인 인수가 있어야합니다.

이것을 가능하게 하려면 단일 리졸버를 사용자 정의하려면 when_single 블록을 사용합니다. 모든 when_single 블록은 다음을 수행해야 합니다: - 적어도 하나의 인수를 정의(또는 재정의)합니다. - 선택적 필터를 필수 필터로 만듭니다.

예를 들면, 기존의 선택적 인수를 재정의하고 유형을 변경하고 필수로 만들 수 있습니다:

class JobsResolver < BaseResolver
  type JobType.connection_type, null: true
  authorize :read_pipeline

  argument :name, [::GraphQL::Types::String], required: false

  when_single do
    argument :name, ::GraphQL::Types::String, required: true
  end

  def resolve(**args)
    JobsFinder.new(pipeline, current_user, args.compact).execute
  end

여기서는 파이프라인 작업을 얻기위한 리졸버가 있습니다. name 인수는 목록을 얻을 때는 선택 사항이지만, 단일 작업을 얻을 때는 필수입니다.

여러 개의 인수가 있는 경우와 어느 것이 필수로 만들 수 없다면 레디 조건을 추가할 수 있습니다.

class JobsResolver < BaseResolver
  alias_method :pipeline, :object

  type JobType.connection_type, null: true
  authorize :read_pipeline

  argument :name, [::GraphQL::Types::String], required: false
  argument :id, [::Types::GlobalIDType[::Job]],
           required: false,
           prepare: ->(ids, ctx) { ids.map(&:model_id) }

  when_single do
    argument :name, ::GraphQL::Types::String, required: false
    argument :id, ::Types::GlobalIDType[::Job],
             required: false
             prepare: ->(id, ctx) { id.model_id }

    def ready?(**args)
      raise ::Gitlab::Graphql::Errors::ArgumentError, 'Only one argument may be provided' unless args.size == 1
    end
  end

  def resolve(**args)
    JobsFinder.new(pipeline, current_user, args.compact).execute
  end

그런 다음 이러한 리졸버를 필드에서 사용할 수 있습니다.

# PipelineType에서

field :jobs, resolver: JobsResolver, description: '모든 작업입니다.'
field :job, resolver: JobsResolver.single, description: '단일 작업입니다.'

리졸버 최적화

미리보기

실행 중에 전체 쿼리가 미리 알려져 있기 때문에 미리보기를 활용하여 쿼리를 최적화하고 필요한 연관관계를 일괄로 불러올 수 있습니다. N+1 성능 문제를 피하기 위해 리졸버에 미리보기 지원을 추가하는 것을 고려해보세요.

일반적인 미리보기 사용 사례를 지원하기 위해 (자식 필드를 요청할 때 연관관계를 미리로드하는) LooksAhead을 포함할 수 있습니다. 예를 들어:

# 속성이 `[child_attribute, other_attribute, nested]`인 모델 `MyThing`을 가정해봅니다.
# 여기서 중첩된 것은 `included_attribute`라는 속성을 가집니다.
class MyThingResolver < BaseResolver
  include LooksAhead

  # `resolve(**args)`를 정의하는 대신에 `resolve_with_lookahead(**args)`를 구현합니다.
  def resolve_with_lookahead(**args)
    apply_lookahead(MyThingFinder.new(current_user).execute)
  end

  # 항상 미리로드해야 하는 것들을 나열합니다.
  # 예를 들어, child_attribute가 항상 필요한 경우(인가 중에) 여기에 포함할 수 있습니다.
  def unconditional_includes
    [:child_attribute]
  end

  # 해당 필드를 선택한 경우에만 포함해야 하는 것을 나열합니다:
  def preloads
    {
        field_one: [:other_attribute],
        field_two: [{ nested: [:included_attribute] }]
    }
  end
end

기본적으로 #preloads에 정의된 필드는 쿼리에서 선택된 경우 미리로드됩니다. 때로는 너무 많이 미리로드되거나 잘못된 콘텐츠를 피하기 위해 더 세밀한 제어가 필요할 수 있습니다.

위의 예제를 확장하여, 특정 필드가 함께 요청되는 경우 다른 연관관계를 미리로드할 수 있습니다. 이를 #filtered_preloads를 오버라이드하여 수행할 수 있습니다:

class MyThingResolver < BaseResolver
  # ...

  def filtered_preloads
    return [:alternate_attribute] if lookahead.selects?(:field_one) && lookahead.selects?(:field_two)

    super
  end
end

LooksAhead 관련은 또한 중첩된 GraphQL 필드 정의를 기반으로 연관관계를 미리로드하는 기본 지원도 제공합니다. WorkItemsResolver가 이를 잘 보여주는 좋은 예입니다. nested_preloads은 다른 연관관계를 미리로드하기 위해 해시를 반환하는 메서드이지만, preloads 메서드와 달리 각 해시 키에 대한 값이 다른 연관관계 목록이 아닌 다른 해시입니다. 따라서, 이전 예제에서 nested_preloads를 오버라이드할 수 있습니다:

class MyThingResolver < BaseResolver
  # ...

  def nested_preloads
    {
      root_field: {
        nested_field1: :association_to_preload,
        nested_field2: [:association1, :association2]
      }
    }
  end
end

실제 사용 사례 예는 ResolvesMergeRequests에서 확인할 수 있습니다.

before_connection_authorization

before_connection_authorization 훅은 타입 인가 권한 확인에서 기인한 N+1 문제를 해결하는 데 도움이 되는데, before_connection_authorization 메서드는 해결된 노드와 현재 사용자를 받습니다. 블록 내에서 ActiveRecord::Associations::Preloader 또는 Preloaders:: 클래스를 사용하여 타입 인가 확인을 위해 데이터를 미리로드할 수 있습니다.

예제:

class LabelsResolver < BaseResolver
  before_connection_authorization do |labels, current_user|
    Preloaders::LabelsPreloader.new(labels, current_user).preload_all
  end
end

BatchLoading

GraphQL BatchLoader를 참조하십시오.

Resolver#ready?의 올바른 사용

리졸버에는 프레임워크의 일부로서 두 가지 공개 API 메서드가 있습니다: #ready?(**args)#resolve(**args). #ready?를 사용하여 #resolve를 호출하지 않고 설정 또는 일찍 반환할 수 있습니다.

#ready?를 사용하는 좋은 이유에는 다음이 포함됩니다:

  • 결과가 불가능한 것을 사전에 알고 있다면 Relation.none을 반환합니다.
  • 인스턴스 변수를 초기화하는 것과 같은 세팅을 수행합니다 (이 경우에는 지연 초기화된 메서드를 고려해보세요).

Resolver#ready?(**args)의 구현은 다음과 같이 (Boolean, early_return_data)을 반환해야 합니다:

def ready?(**args)
  [false, '이를 대신 사용하세요']
end

따라서 리졸버를 호출할 때마다 (주로 프레임워크 추상화인 리졸버는 재사용 가능하다고 보기 어려우므로, 대신 찾는 것이 선호됩니다) #ready? 메서드를 호출하고 부울 플래그를 호출하기 전에 확인하는 것을 기억해야 합니다. 예제는 GraphqlHelpers에서 확인할 수 있습니다.

인수를 유효성 검사하는 경우 validators를 사용하는 것이 #ready?를 사용하는 것보다 선호됩니다.

부울 인수

부울 인수를 사용하여 일부 리소스를 필터링할 수 있습니다 (예: ‘bug’ 레이블이 있는 모든 이슈를 찾지만 ‘bug2’ 레이블이 할당되지 않은 모든 이슈를 찾습니다). not 인수는 부정된 인수를 전달하기 위한 우선 구문입니다:

issues(labelName: "bug", not: {labelName: "bug2"}) {
  nodes {
    id
    title
  }
}

타입이나 리졸버에서 Gitlab::Graphql::NegatableArgumentsnegated 헬퍼를 사용할 수 있습니다. 예를 들어:

extend ::Gitlab::Graphql::NegatableArguments

negated do
  argument :labels, [GraphQL::STRING_TYPE],
            required: false,
            as: :label_name,
            description: 'Array of label names. All resolved merge requests will not have these labels.'
end

메타데이터

리졸버를 사용할 때, 필드 메타데이터의 SSoT로 작동할 수 있으며 그것이 바람직합니다. 필드 옵션들(필드 이름을 제외한 모든 옵션)은 리졸버에 선언될 수 있습니다. 이러한 내용은 다음과 같습니다:

  • type (필수 - 모든 리졸버는 타입 어노테이션을 포함해야 합니다)
  • extras
  • description
  • Gitaly 어노테이션 (calls_gitaly! 포함)

예시:

module Resolvers
  MyResolver < BaseResolver
    type Types::MyType, null: true
    extras [:lookahead]
    description '단일 MyType을 검색합니다'
    calls_gitaly!
  end
end

부모 객체를 자식 프레젠터로 전달

가끔은 필드를 계산하기 위해 자식 컨텍스트에서 해결된 쿼리 부모에 접근해야 할 수 있습니다. 보통 부모는 Resolver 클래스에서만 parent로 사용할 수 있습니다.

프레젠터 클래스에서 부모 객체를 찾으려면:

  1. 리졸버의 resolve 메서드에서 GraphQL context에 부모 객체를 추가합니다:

      def resolve(**args)
        context[:parent_object] = parent
      end
    
  2. 리졸버나 필드가 parent 필드 컨텍스트를 요구하도록 선언합니다. 예:

      # ChildType에서
      field :computed_field, SomeType, null: true,
            method: :my_computing_method,
            extras: [:parent], # 필수
            description: '내 필드 설명.'
    
      field :resolver_field, resolver: SomeTypeResolver
    
      # SomeTypeResolver에서
    
      extras [:parent]
      type SomeType, null: true
      description '내 필드 설명.'
    
  3. 프레젠터 클래스에 필드 메서드를 선언하고 parent 키워드 인자를 받도록 합니다. 이 인자에는 부모 GraphQL 콘텍스트가 포함되어 있으므로, 부모 객체에는 parent[:parent_object]나 사용한 키로 접근해야 합니다.

      # ChildPresenter에서
      def my_computing_method(parent:)
        # 여기서 `parent[:parent_object]`와 어떤 작업을 수행합니다
      end
    
      # SomeTypeResolver에서
    
      def resolve(parent:)
        # ...
      end
    

실제 사용 사례를 보려면 이 MR을 확인하십시오. IterationPresenterscopedPathscopedUrl을 추가했습니다

뮤테이션

뮤테이션은 저장된 값을 변경하거나 작업을 트리거하기 위해 사용됩니다. GET 요청이 데이터를 수정해서는 안 되는 것처럼, 일반 GraphQL 쿼리에서는 데이터를 수정할 수 없습니다. 그러나 뮤테이션에서는 가능합니다.

뮤테이션 작성

뮤테이션들은 app/graphql/mutations에 저장되며, 이상적으로는 서비스처럼 수정하는 리소스에 따라 그룹화되어야 합니다. Mutations::BaseMutation을 상속해야 합니다. 뮤테이션에서 정의된 필드들은 뮤테이션의 결과로 반환됩니다.

뮤테이션 갱신 정도

GitLab의 서비스 지향 아키텍처는 대부분의 뮤테이션이 Create, Delete 또는 Update 서비스를 호출한다는 것을 의미합니다. 예를 들어 UpdateMergeRequestService. 갱신 뮤테이션의 경우, 객체의 한 측면만 업데이트할 수 있고 따라서 세분화된 뮤테이션이 필요할 수 있습니다. 예를 들어 MergeRequest::SetDraft와 같이 구체적인 뮤테이션이 필요할 수 있습니다.

세분화된 뮤테이션이 여러 개여도 괜찮지만, 너무 많은 세분화된 뮤테이션은 유지 관리 가능성, 코드 이해도 및 테스트에서 조직적인 어려움을 야기할 수 있음을 인지해야 합니다. 각 뮤테이션에는 기술적 부채가 발생할 수 있는 새로운 클래스가 필요합니다. 또한, 스키마가 매우 커지면 사용자가 스키마를 탐색하기 어려워질 수 있습니다. 또한, 각 뮤테이션에는 테스트가 필요하며(느린 요청 통합 테스트 포함), 뮤테이션을 추가하면 테스트 스위트가 느려집니다.

변경을 최소화하기 위해:

  • 가능한 경우 기존 뮤테이션을 사용합니다(MergeRequest::Update 등).
  • 기존 서비스를 세분화된 뮤테이션으로 노출시킵니다.

세분화된 뮤테이션이 더 적합한 경우:

  • 특정 권한이나 다른 특수 로직을 필요로 하는 속성 수정.
  • 상태 기계와 유사한 전이 노출(이슈 잠금, MR 병합, 에픽 종료 등).
  • 중첩된 속성(하위 객체의 속성을 받는 경우)
  • 명료하고 간결하게 뮤테이션의 의미가 표현될 수 있는 경우.

자세한 내용은 이슈 #233063을 참조하세요.

네이밍 규칙

각 뮤테이션은 GraphQL 스키마에서 뮤테이션의 이름인 graphql_name을 정의해야 합니다.

예시:

class UserUpdateMutation < BaseMutation
  graphql_name 'UserUpdate`
end

graphql-ruby 젬의 1.13 버전에서의 변경으로 인해, 타입 이름이 올바르게 생성되도록하기 위해 graphql_name은 클래스의 첫 번째 줄이어야 합니다. Graphql::GraphqlNamePosition 검사기가 이를 집행합니다. 자세한 내용은 이슈 #27536을 참조하세요.

우리의 GraphQL 뮤테이션 이름들은 역사적으로 일관성이 없었지만, 새로운 뮤테이션 이름은 '{리소스}{액션}' 또는 '{리소스}{액션}{속성}'의 규약을 따라야 합니다.

새로운 리소스를 생성하는 뮤테이션은 Create 동사를 사용해야 합니다.

예시:

  • CommitCreate

데이터를 업데이트하는 뮤테이션은 다음과 같이 지정해야 합니다:

  • Update 동사를 사용합니다.
  • 더 적합한 경우 Set, Add, 또는 Toggle과 같이 도메인 특화 동사로 구성합니다.

예시:

  • EpicTreeReorder
  • IssueSetWeight
  • IssueUpdate
  • TodoMarkDone

데이터를 삭제하는 뮤테이션은 Destroy 대신에 Delete 동사를 사용해야 합니다. 더 적합한 경우 특화된 동사 Remove 등을 사용해야 합니다.

예시:

  • AwardEmojiRemove
  • NoteDelete

뮤테이션 이름에 대한 조언이 필요하다면, Slack의 #graphql 채널에서 피드백을 요청하세요.

필드

가장 일반적인 상황에서 뮤테이션은 2개의 필드를 반환해야 합니다:

  • 수정되는 리소스
  • 작업을 수행할 수 없는 이유를 설명하는 오류 목록. 뮤테이션이 성공하면, 이 목록은 비어 있을 것입니다.

어떤 새로운 뮤테이션이든지 Mutations::BaseMutation을 상속함으로써 errors 필드가 자동으로 추가됩니다. clientMutationId 필드도 추가되며, 이는 클라이언트가 단일 요청에서 여러 뮤테이션을 수행할 때 결과를 식별하는 데 사용될 수 있습니다.

resolve 메소드

리졸버 작성과 유사하게, 뮤테이션의 resolve 메소드는 서비스를 감싼 얇은 선언적 래퍼를 목표로 해야 합니다.

resolve 메소드는 뮤테이션의 인자를 키워드 인자로 받습니다. 여기서 리소스를 수정하는 서비스를 호출할 수 있습니다.

resolve 메소드는 뮤테이션에서 정의된 필드와 동일한 필드 이름을 가진 해시를 반환해야 합니다. 예를 들어, Mutations::MergeRequests::SetDraftmerge_request 필드를 정의합니다:

field :merge_request,
      Types::MergeRequestType,
      null: true,
      description: "The merge request after mutation."

즉, 이 뮤테이션의 resolve에서 반환된 해시는 다음과 같아야 합니다:

{
  # 수정된 머지 요청, 이는 필드에서 정의된 타입으로 래핑됩니다
  merge_request: merge_request,
  # 권한 부여 후 변경 사항
  errors: errors_on_object(merge_request)
}

뮤테이션 마운트

뮤테이션을 사용할 수 있게 만들려면 graphql/types/mutation_type에 저장된 뮤테이션 유형에 정의해야 합니다. mount_mutation 도우미 메소드는 뮤테이션의 GraphQL 이름을 기반으로 필드를 정의합니다:

module Types
  class MutationType < BaseObject
    graphql_name 'Mutation'

    include Gitlab::Graphql::MountMutation

    mount_mutation Mutations::MergeRequests::SetDraft
  end
end

이렇게 하면 Mutations::MergeRequests::SetDraft에 기반한 mergeRequestSetDraft 필드가 생성됩니다.

리소스 권한 부여

뮤테이션 내에서 리소스를 권한 부여하려면, 먼저 다음과 같이 뮤테이션에 필요한 권한을 제공합니다:

module Mutations
  module MergeRequests
    class SetDraft < Base
      graphql_name 'MergeRequestSetDraft'

      authorize :update_merge_request
    end
  end
end

그런 다음 resolve 메소드에서 authorize!를 호출하여 확인할 리소스를 전달합니다.

또는 뮤테이션에서 객체를로드하는 find_object 메소드를 추가할 수 있습니다. 이렇게 하면 authorized_find! 도우미 메소드를 사용할 수 있습니다.

사용자가 작업을 수행할 수 없거나 리소스를 찾을 수 없는 경우, resolve 메소드 내에서 raise_resource_not_available_error!를 호출하여 Gitlab::Graphql::Errors::ResourceNotAvailable를 발생시켜야 합니다.

뮤테이션의 오류

뮤테이션에서 데이터로서의 오류를 따르는 것을 권장하며, 이는 오류를 처리할 수 있는 사람에 따라 오류를 구별합니다.

주요 포인트:

  • 모든 뮤테이션 응답에는 errors 필드가 있어야 합니다. 이는 실패 시 채워져야 하며, 성공 시 채워질 수 있습니다.
  • 오류를 볼 필요가 있는 사람을 고려하십시오: 사용자 또는 개발자.
  • 클라이언트는 항상 뮤테이션 수행 시 errors 필드를 요청해야 합니다.
  • 클라이언트는 오류를 항상 $root.errors (최상위 오류) 또는 $root.data.mutationName.errors (뮤테이션 오류)로 요청해야 합니다. 위치는 오류의 종류 및 보유 정보에 따라 달라집니다.
  • 뮤테이션 필드에는 null: true가 있어야 합니다.

다음은 doTheThing 뮤테이션 예제로, 두 가지 필드를 반환하는 응답이 있습니다: errors: [String]thing: ThingType. thing의 구체적인 성질은 예제와 관련이 없으며, 오류만 고려합니다.

뮤테이션 응답의 세 가지 상태는 다음과 같습니다:

성공

성공 시, 예상된 페이로드 외에도 오류가 반환될 수 있지만, 모든 것이 성공했다면 errors는 보통 비어 있어야 합니다. 왜냐하면 사용자에게 알려줄 문제가 없기 때문입니다.

{
  data: {
    doTheThing: {
      errors: [] // 성공 시 이 배열은 일반적으로 비어 있을 것입니다.
      thing: { .. }
    }
  }
}

실패 (사용자와 관련 있음)

사용자에게 영향을 미치는 오류가 발생했습니다. 이를 _뮤테이션 오류_라고 합니다.

생성 뮤테이션에서 일반적으로 반환할 thing이 없습니다.

업데이트 뮤테이션에서는 thing의 현재 실제 상태를 반환합니다. 개발자는 thing 인스턴스에서 #reset을 호출해야 할 수 있습니다.

{
  data: {
    doTheThing: {
      errors: ["the thing를 손대면 안 됩니다"],
      thing: { .. }
    }
  }
}

이에는 다음과 같은 예시가 포함됩니다:

  • 모델 유효성 오류: 사용자는 입력을 변경해야 할 수 있습니다.
  • 권한 오류: 사용자는 이 작업을 수행할 수 없다는 것을 알아야 하며, 권한을 요청하거나 로그인해야 할 수 있습니다.
  • 사용자의 작업을 방해하는 응용 프로그램 상태의 문제(예: 머지 충돌 또는 잠긴 리소스).

이상적으로는 사용자가 여기까지 도달하지 않아야 합니다. 그러나 도달한다면 문제점을 이해하고 의도를 이루기 위해 어떤 것을 할 수 있는 이유를 이해해야 하므로 문제를 알려주어야 합니다. 예를 들어, 요청을 다시 시도할 수도 있습니다.

복구 가능한 오류는 뮤테이션 데이터와 함께 반환될 수 있습니다. 예를 들어, 사용자가 10개의 파일을 업로드하고 3개가 실패하고 나머지가 성공한 경우, 실패한 파일에 대한 오류를 성공 정보와 함께 사용자에게 제공할 수 있습니다.

실패 (사용자와 관련 없음)

하나 이상의 복구할 수 없는 오류가 _최상위 수준_에서 반환될 수 있습니다. 이는 사용자가 거의 또는 전혀 제어하지 못하는 시스템 또는 프로그래밍 문제여야 하며, 주로 시스템 또는 프로그래밍 문제일 것입니다. 이 경우에는 data가 없습니다:

{
  errors: [
    {"message": "argument error: expected an integer, got null"},
  ]
}

이것은 뮤테이션 중에 오류를 발생시킨 결과입니다. 우리의 구현에서는 인수 오류 및 유효성 오류 메시지가 클라이언트에 반환되며, 다른 StandardError 인스턴스는 catch되어 메시지가 "내부 서버 오류"로 설정된 채 클라이언트에 제공됩니다. 세부 정보는 GraphqlController를 참조하십시오.

이러한 것들은 다음과 같은 프로그래밍 오류를 나타냅니다:

  • Int 대신 String으로 전달된 GraphQL 구문 오류 또는 필수 인자가 없는 경우.
  • 스키마 오류: 예를 들어 무언가의 값을 제공할 수 없는 필수 필드.
  • 시스템 오류: 예를 들어, Git 저장소 예외 또는 데이터베이스 부재 등.

일반적인 사용에서 사용자가 이러한 오류를 유발할 수 없어야 합니다. 이러한 오류는 내부적으로 취급해야 하며, 특정 세부 정보를 사용자에게 표시할 필요가 없습니다.

뮤테이션이 실패할 경우 사용자에게 이를 알려야 하지만 원인을 알려줄 필요는 없으며, 그들이 원인을 만들 수 없기 때문에 이를 해결할 수 있는 것도 없습니다. 그러나 뮤테이션을 다시 시도할 수 있도록 제안할 수는 있습니다.

오류 카테고리화

변이를 작성할 때, 오류 상태가 두 가지 카테고리 중 어디에 속하는지에 대해 주의해야 합니다(그리고 이를 프론트엔드 개발자와 의사소통하여 우리의 가정을 확인해야 합니다). 이는 _사용자_의 필요와 _클라이언트_의 필요를 구별하는 것을 의미합니다.

사용자가 알아야 하는 경우에만 오류를 catch 하지 마세요.

사용자가 알아야 하는 경우, 프론트엔드 개발자와 의사소통하여 우리가 전달하고 있는 오류 정보가 관련이 있고 목적을 제공하는지 확인하세요.

프론트엔드 GraphQL 가이드 또한 참조하세요.

별칭 지정 및 변경된 변이

#mount_aliased_mutation 도우미는 MutationType에서 변이를 다른 이름으로 별칭을 지정하는 데 사용합니다.

예를 들어, FooMutation이라는 변이를 BarMutation으로 별칭 지정하는 경우:

mount_aliased_mutation 'BarMutation', Mutations::FooMutation

이를 통해 우리는 변이 이름을 변경하고 이전 이름을 계속 지원할 수 있습니다. 이는 deprecated 인수와 함께 사용할 때 유효합니다.

예시:

mount_aliased_mutation 'UpdateFoo',
                        Mutations::Foo::Update,
                        deprecated: { reason: 'Use fooUpdate', milestone: '13.2' }

폐기된 변이는 Types::DeprecatedMutations에 추가되어 Types::MutationType의 단위 테스트에서 테스트되어야 합니다. !34798에 대한 병합 요청은 이러한 예시를 참조할 때 포함하여 폐기된 별칭 지정된 변이의 테스트 방법을 보여줍니다.

EE(Enterprise Edition) 변이 폐기

EE(Enterprise Edition) 변이는 동일한 프로세스를 따라야 합니다. 병합 요청 프로세스의 예제는 merge request !42588를 참조하세요.

구독

우리는 클라이언트에 업데이트를 푸시하기 위해 구독을 사용합니다. 웹소켓을 통해 메시지를 전달하기 위해 Action Cable implementation를 사용합니다.

클라이언트가 구독하면 Puma 워커의 메모리에 그들의 쿼리를 저장합니다. 그런 다음 구독이 트리거될 때, Puma 워커가 저장된 GraphQL 쿼리를 실행하고 결과를 클라이언트에게 푸시합니다.

참고: 우리는 GraphiQL을 사용하여 구독을 테스트할 수 없습니다. 왜냐하면 GraphiQL이 현재 Action Cable 클라이언트를 지원하지 않기 때문입니다.

구독 빌드

Types::SubscriptionType 아래의 모든 필드는 클라이언트가 구독할 수 있는 구독입니다. 이러한 필드는 Subscriptions::BaseSubscription의 하위 클래스인 구독 클래스로 정의되어 있으며, app/graphql/subscriptions에 저장됩니다.

구독하려면 필요한 인수 및 반환되는 필드는 구독 클래스에서 정의됩니다. 동일한 인수를 가지고 동일한 필드를 반환하는 여러 필드는 동일한 구독 클래스를 공유할 수 있습니다.

이 클래스는 초기 구독 요청 및 후속 업데이트 중에 실행됩니다. 자세한 내용은 GraphQL Ruby 가이드를 참조하세요.

권한 부여

구독 클래스의 #authorized? 메서드를 구현하여 초기 구독 및 후속 업데이트가 인가되도록 해야 합니다.

사용자가 인가되지 않은 경우, unauthorized! 도우미를 호출하여 실행이 중지되고 사용자의 구독이 취소되도록 해야 합니다. false를 반환하면 응답이 감추어지지만 일부 업데이트가 이루어진 것임을 누설합니다. 이러한 누설은 GraphQL gem의 버그 때문입니다.

구독 트리거

GraphqlTriggers 모듈 아래에 메서드를 정의하여 구독을 트리거하세요. 애플리케이션 코드에서 GitlabSchema.subscriptions.trigger를 직접 호출하지 마세요. 이렇게 함으로써 우리는 단일한 진실의 소스를 가지며 서로 다른 인수 및 객체로 구독을 트리거하지 않도록 해야 합니다.

페이지네이션 구현

자세한 내용은 GraphQL 페이지네이션을 참조하세요.

인수

리졸버 또는 변이의 인수argument를 사용하여 정의됩니다.

예시:

argument :my_arg, GraphQL::Types::String,
         required: true,
         description: "인수에 대한 설명."

널 가능성

인수는 required: true로 표시될 수 있으며, 이것은 값이 존재하고 null이 아니어야 함을 의미합니다. required: :nullable 선언을 사용하여 필수 인수의 값이 null일 수 있다면 이를 사용하세요.

예시:

argument :due_date,
         Types::TimeType,
         required: :nullable,
         description: '이슈의 원하는 만료 날짜. null일 경우, 만료 날짜가 제거됩니다.'

위의 예에서 due_date 인수는 주어져야 하지만 GraphQL 사양과는 달리 값이 null일 수 있습니다. 이로써 만료 날짜를 새로 생성하는 대신 하나의 변이에서 만료 날짜를 ‘해제’할 수 있습니다.

{ due_date: null } # => 허용
{ due_date: "2025-01-10" } # => 허용
{  } # => 유효하지 않음 (주어지지 않음)

널 가능성 및 required: false

인수가 required: false로 표시되면 클라이언트가 값으로 null을 보낼 수 있습니다. 이는 종종 바람직하지 않습니다.

인수가 선택적이지만 null이 허용되지 않을 때, 검증을 사용하여 null을 전달하면 오류가 발생하도록 보장하세요:

argument :name, GraphQL::Types::String,
         required: false,
         validates: { allow_null: false }

또는 null이 허용되지 않는 경우 null을 기본 값으로 대체할 수 있습니다:

argument :name, GraphQL::Types::String,
         required: false,
         default_value: "제공된 이름 없음",
         replace_null_with_default: true

자세한 내용은 검증, 널 가능성기본 값을 참조하세요.

상호 배타적 인수

인수는 상호 배타적으로 표시될 수 있으며, 동시에 제공되지 않도록 보장됩니다. 주어진 인수의 개수가 여러 개인 경우, 최상위 수준의 오류가 추가됩니다.

예시:

argument :user_id, GraphQL::Types::String, required: false
argument :username, GraphQL::Types::String, required: false

validates mutually_exclusive: [:user_id, :username]

하나의 인수가 필요한 경우, exactly_one_of 유효성 검사자를 사용할 수 있습니다.

예시:

argument :group_path, GraphQL::Types::String, required: false
argument :project_path, GraphQL::Types::String, required: false

validates exactly_one_of: [:group_path, :project_path]

키워드

각 GraphQL argument는 변이의 #resolve 메서드로 키워드 인수로 전달됩니다.

예시:

def resolve(my_arg:)
  # 변이 수행 ...
end

입력 유형

graphql-ruby는 인수를 입력 유형으로 래핑합니다.

예를 들어, mergeRequestSetDraft 변이 은 이러한 인수를 정의합니다 (어떤 것은 상속을 통해):

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: "병합 요청이 속한 프로젝트."

argument :iid, GraphQL::Types::String,
         required: true,
         description: "병합 요청의 IID."

argument :draft,
         GraphQL::Types::Boolean,
         required: false,
         description: <<~DESC
           병합 요청을 드래프트로 설정할지 여부.
         DESC

이러한 인수들은 자동으로 우리가 지정한 3개의 인수와 clientMutationId를 가진 MergeRequestSetDraftInput라는 입력 유형을 생성합니다.

객체 식별자 인수

객체를 식별하는 인수는 다음과 같아야 합니다:

  • 해당 객체의 전체 경로 또는 IID가 있는 경우.
  • 기타 모든 객체에 대해 객체의 글로벌 ID를 사용합니다. 일반적인 데이터베이스 기본 키 ID를 사용해서는 안 됩니다.

전체 경로 객체 식별자 인수

과거에 우리는 전체 경로 인수의 명명에 중복이 있었지만, 해당 인수를 다음과 같이 명명하는 것을 선호합니다:

  • 프로젝트의 전체 경로일 경우 project_path
  • 그룹의 전체 경로일 경우 group_path
  • 네임스페이스의 전체 경로일 경우 namespace_path

다음은 ciJobTokenScopeRemoveProject 변이에서의 예시입니다:

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: 'CI 작업 토큰 범위가 속한 프로젝트.'

IID 객체 식별자 인수

해당 객체의 iid를 해당 객체의 상위 project_path 또는 group_path와 결합하여 사용합니다. 예를 들어:

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: '이슈가 속한 프로젝트.'

argument :iid, GraphQL::Types::String,
         required: true,
         description: '이슈의 IID.'

글로벌 ID 객체 식별자 인수

다음은 discussionToggleResolve 변이의 예시입니다:

argument :id, Types::GlobalIDType[Discussion],
         required: true,
         description: '토론의 글로벌 ID.'

또한 글로벌 ID의 사용 페기를 참조하세요.

정렬 인수

정렬 인수는 가능한 경우 열거 유형을 사용하여 사용 가능한 정렬 값의 세트를 설명해야 합니다.

열거 유형은 몇 가지 일반적인 값을 상속하기 위해 Types::SortEnum을 상속할 수 있습니다.

열거 값은 {PROPERTY}_{DIRECTION} 형식을 따라야 합니다. 예를 들어:

TITLE_ASC

또한 정렬 열거유형에 대한 설명 스타일 가이드를 참조하세요.

다음은 ContainerRepositoriesResolver의 예시입니다:

# Types::ContainerRepositorySortEnum:
module Types
  class ContainerRepositorySortEnum < SortEnum
    graphql_name 'ContainerRepositorySort'
    description '컨테이너 저장소를 정렬하기 위한 값들'

    value 'NAME_ASC', '오름차순으로 이름.', value: :name_asc
    value 'NAME_DESC', '내림차순으로 이름.', value: :name_desc
  end
end

# Resolvers::ContainerRepositoriesResolver:
argument :sort, Types::ContainerRepositorySortEnum,
          description: '이 기준으로 컨테이너 저장소를 정렬합니다.',
          required: false,
          default_value: :created_desc

GitLab 사용자 정의 스칼라

Types::TimeType

Types::TimeType 은 Ruby TimeDateTime 객체를 다루는 모든 필드 및 인수의 유형으로 사용되어야 합니다.

이 유형은 사용자 정의 스칼라입니다:

  • 우리의 GraphQL 필드 유형으로 사용될 때 Ruby의 TimeDateTime 객체를 표준화된 ISO-8601 형식의 문자열로 변환합니다.
  • 우리의 GraphQL 인수 유형으로 사용될 때 ISO-8601 형식의 시간 문자열을 Ruby Time 객체로 변환합니다.

이를 통해 우리의 GraphQL API는 시간을 표현하고 시간 입력을 처리하는 표준화된 방법을 갖게 됩니다.

예시:

field :created_at, Types::TimeType, null: true, description: '이슈가 만들어진 타임스탬프.'

글로벌 ID 스칼라

우리의 모든 글로벌 ID는 사용자 정의 스칼라입니다. 이들은 추상 스칼라 클래스 로부터 동적으로 생성 됩니다.

테스팅

변이 및 리졸버를 테스트하려면 전체 GraphQL 요청 단위로 테스트하는 것을 고려해보세요. 리졸버에 대한 호출이 아닌 GraphQL 요청을 테스트함으로써 의존성 업그레이드가 훨씬 더 어려워지는 커플링을 피할 수 있습니다.

다음을 해야 합니다.

  • 리졸버 및 변이의 단위 테스트보다는 요청 스펙(prefer request specs)을 선호합니다 (전체 API 엔드포인트 또는 GitlabSchema.execute를 사용).
  • GraphqlHelpers#execute_queryGraphqlHelpers#run_with_clean_stateGraphqlHelpers#resolveGraphqlHelpers#resolve_field를 선호합니다.

예시:

# 좋은 방법:
gql_query = %q(some query text...)
post_graphql(gql_query, current_user: current_user)
# 또는:
GitlabSchema.execute(gql_query, context: { current_user: current_user })

# Deprecated: 피하세요
resolve(described_class, obj: project, ctx: { current_user: current_user })

단위 테스트 작성 (폐기됨)

경고: 동일한 것을 테스트하는 경우, 전체 GraphQL 요청으로도 테스트할 수 있다면 단위 테스트를 피하세요.

단위 테스트를 작성하기 전에 다음 예시를 검토하세요:

통합 테스트 작성

통합 테스트는 GraphQL 쿼리 또는 변이의 전체 스택을 확인하며 spec/requests/api/graphql에 저장됩니다.

속도를 위해 GitlabSchema.execute를 직접 호출하거나 테스트하는 유형만 포함하는 작은 테스트 스키마를 활용하세요.

그러나 데이터가 반환되는 풀 요청 통합 테스트는 다음을 확인합니다:

  • 변이가 스키마에서 실제로 쿼리 가능한지 (MutationType에 마운트되었는지).
  • 리졸버 또는 변이가 필드의 반환 유형과 오류 없이 올바르게 일치하는 데이터를 반환하는지.
  • 입력시 인수가 올바르게 변환되고 출력시 필드가 올바르게 직렬화되는지.

통합 테스트에서는 다음의 항목도 검증할 수 있습니다. 왜냐하면 전체 스택을 활용하기 때문입니다.

  • 인수 또는 스칼라의 유효성이 올바르게 적용되는지.
  • 리졸버 또는 변이의 로직의 #ready? 메서드가 올바르게 적용되는지.
  • 인수의 default_value가 올바르게 적용되는지.
  • 객체가 성공적으로 해석되고 N+1 이슈가 없는지.

쿼리를 추가할 때 데이터를 반환하는 작동하는 graphql 쿼리데이터를 반환하지 않는 작동하는 graphql 쿼리 공유 예시를 사용하여 쿼리가 올바른 결과를 렌더링하는지 테스트할 수 있습니다.

GraphqlHelpers#all_graphql_fields_for를 사용하여 사용 가능한 모든 필드를 포함하는 쿼리를 생성할 수 있습니다. 이를 통해 쿼리의 모든 가능한 필드를 렌더링하는 테스트를 보다 간단하게 추가할 수 있습니다.

페이지네이션 및 정렬을 지원하는 쿼리에 필드를 추가하는 경우, 테스트 세부 정보를 확인하세요.

GraphQL 변이 요청을 테스트하려면 GraphqlHelpers에서 두 가지 도우미를 제공합니다: graphql_mutation은 변이의 이름을 취하고 변이의 입력과 함께 해시를 반환합니다. 이는 변이 쿼리와 준비된 변수를 포함하는 구조체를 반환합니다.

그런 다음 이 구조체를 post_graphql_mutation 도우미에 전달하여 GraphQL 클라이언트가 수행하는 것처럼 요청을 게시할 수 있습니다.

변이의 응답을 액세스하려면 graphql_mutation_response 도우미를 사용할 수 있습니다.

이러한 도우미를 사용하여 다음과 같은 스펙을 작성할 수 있습니다:

let(:mutation) do
  graphql_mutation(
    :merge_request_set_wip,
    project_path: 'gitlab-org/gitlab-foss',
    iid: '1',
    wip: true
  )
end

it 'returns a successful response' do
   post_graphql_mutation(mutation, current_user: user)

   expect(response).to have_gitlab_http_status(:success)
   expect(graphql_mutation_response(:merge_request_set_wip)['errors']).to be_empty
end

테스트 팁과 트릭

  • GraphqlHelpers 지원 모듈의 메서드에 익숙해지세요. 이러한 메서드 중 많은 것들이 GraphQL 테스트 작성을 보다 쉽게 만듭니다.

  • GraphqlHelpers#graphql_data_atGraphqlHelpers#graphql_dig_at와 같은 탐색 도우미를 사용하여 결과 필드에 액세스하세요. 예를 들어:

    result = GitlabSchema.execute(query)
    
    mr_iid = graphql_dig_at(result.to_h, :data, :project, :merge_request, :iid)
    
  • 결과와 일치시키기 위해 GraphqlHelpers#a_graphql_entity_for 사용하세요. 예를 들어:

    post_graphql(some_query)
    
    # { id => global_id_of(issue) }를 포함하는 해시인지 확인합니다
    expect(graphql_data_at(:project, :issues, :nodes))
      .to contain_exactly(a_graphql_entity_for(issue))
    
    # 추가 필드를 메소드 이름 또는 값으로 전달할 수 있습니다
    expect(graphql_data_at(:project, :issues, :nodes))
      .to contain_exactly(a_graphql_entity_for(issue, :iid, :title, created_at: some_time))
    
  • GraphqlHelpers#empty_schema를 사용하여 빈 스키마를 생성하세요. 직접 만드는 대신에 이 메서드를 사용하는 것이 좋습니다. 예를 들어:

    # 좋은 방법
    let(:schema) { empty_schema }
    
    # 나쁜 방법
    let(:query_type) { GraphQL::ObjectType.new }
    let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
    
  • GraphqlHelpers#query_double(schema: nil) 또는 double('query', schema: nil)을 사용하세요. 예를 들어:

    # 좋은 방법
    let(:query) { query_double(schema: GitlabSchema) }
    
    # 나쁜 방법
    let(:query) { double('Query', schema: GitlabSchema) }
    
  • 잘못된 긍정적 결과를 피하세요:

    post_graphql에 대한 current_user: 인수로 사용자를 인증하는 경우 해당 동일한 사용자에 대한 첫 번째 요청에는 이후 요청보다 더 많은 쿼리가 생성됩니다. N+1 쿼리를 피하기 위해 QueryRecorder를 사용하는 경우, 각 요청에 대해 다른 사용자를 사용하세요.

    아래 예시는 N+1 쿼리를 피하기 위한 테스트를 보여줍니다:

    RSpec.describe 'Query.project(fullPath).pipelines' do
      include GraphqlHelpers
    
      let(:project) { create(:project) }
    
      let(:query) do
        %(
          {
            project(fullPath: "#{project.full_path}") {
              pipelines {
                nodes {
                  id
                }
              }
            }
          }
        )
      end
    
      it 'avoids N+1 queries' do
        first_user = create(:user)
        second_user = create(:user)
        create(:ci_pipeline, project: project)
    
        control_count = ActiveRecord::QueryRecorder.new do
          post_graphql(query, current_user: first_user)
        end
    
        create(:ci_pipeline, project: project)
    
        expect do
          post_graphql(query, current_user: second_user)  # false positive을 피하기 위해 다른 사용자를 사용하세요
        end.not_to exceed_query_limit(control_count)
      end
    end
    
  • app/graphql/types의 폴더 구조를 모방하세요:

    예를 들어 app/graphql/types/ci/pipeline_type.rbTypes::Ci::PipelineType에 대한 필드의 테스트는 spec/requests/api/graphql/ci/pipeline_spec.rb에 저장되어야 합니다. 가져오는 쿼리에 관계없이

  • GraphqlHelpers#resolve를 사용하여 리졸버를 테스트할 때 리졸버의 인수는 두 가지 방법으로 처리할 수 있습니다.

    1. 리졸버 스펙의 95%에서 루비 객체를 사용하는 경우, GraphQL API를 사용할 때는 문자열과 정수만 사용됩니다. 대부분의 경우에는 이 방법이 잘 작동합니다.
    2. 리졸버에 prepare 프로세스를 사용하는 인수를 받아들이는 경우, 예를 들어 시간 프레임 인수 (TimeFrameArguments)를 받아들이는 리졸버의 경우, resolve 메서드에 arg_style: :internal_prepared 매개변수를 전달해야 합니다. 이를 통해 인수가 문자열 및 정수로 변환되고 일반적인 인수 처리를 통과하여 prepare 프로세스가 올바르게 호출되도록 하는 역할을 합니다. 예를 들어 iterations_resolver_spec.rb에서:

      def resolve_group_iterations(args = {}, obj = group, context = { current_user: current_user })
        resolve(described_class, obj: obj, args: args, ctx: context, arg_style: :internal_prepared)
      end
      

      추가적으로, 리졸버 인수로 열거형을 전달하는 경우 내부 표현 대신 외부 표현을 사용해야 합니다. 예를 들어:

      # 좋은 방법
      resolve_group_iterations({ search: search, in: ['CADENCE_TITLE'] })
      
      # 나쁜 방법
      resolve_group_iterations({ search: search, in: [:cadence_title] })
      

    :internal_prepared의 사용은 GraphQL gem 업그레이드를 위한 전환으로 추가되었습니다. 리졸버를 직접 테스트하는 것은 추후에 제거될 것이며, 리졸버/변이에 대한 단위 테스트 작성은 이미 폐기됨입니다.

Query flow 및 GraphQL 인프라에 관한 참고 사항

GitLab의 GraphQL 인프라는 lib/gitlab/graphql에서 찾을 수 있습니다.

Instrumentation은 쿼리가 실행될 때 둘러싸는 기능입니다. Instrumentation 클래스를 사용하는 모듈로 구현됩니다.

예시: Present

module Gitlab
  module Graphql
    module Present
      #... 일부 코드 ...

      def self.use(schema_definition)
        schema_definition.instrument(:field, ::Gitlab::Graphql::Present::Instrumentation.new)
      end
    end
  end
end

Query Analyzer에는 쿼리를 실행하기 전에 확인하는 일련의 콜백이 포함되어 있습니다. 각 필드는 분석기를 거치고 최종 값도 사용할 수 있습니다.

Multiplex queries를 사용하면 단일 요청에 여러 쿼리를 보낼 수 있습니다. 이를 통해 서버로 보내는 요청 수가 줄어듭니다. (GraphQL Ruby에서 제공하는 사용자 지정 Multiplex Query Analyzers 및 Multiplex Instrumentation도 있습니다).

쿼리 제한

쿼리 및 뮤테이션은 깊이, 복잡성 및 재귀에 의해 제한됩니다. 너무 빈틈없거나 악의적인 쿼리로부터 서버 자원을 보호합니다. 이러한 값을 기본값으로 설정하고 필요에 따라 특정 쿼리에서 재정의할 수 있습니다. 복잡성 값은 개별 객체당 설정할 수 있으며 최종 쿼리 복잡성은 반환되는 객체의 수에 따라 평가됩니다. 이는 비용이 많이 드는 객체에 사용할 수 있습니다 (예: Gitaly 호출이 필요한 경우).

예를 들어 리졸버에 조건부 복잡성 메서드가 있는 경우:

def self.resolver_complexity(args, child_complexity:)
  complexity = super
  complexity += 2 if args[:labelName]

  complexity
end

추가적인 복잡성 정보: GraphQL Ruby 문서.

문서 및 스키마

우리의 스키마는 app/graphql/gitlab_schema.rb에 위치해 있습니다. 자세한 내용은 스키마 참조를 참조하세요.

이 생성된 GraphQL 문서는 스키마 변경 시 업데이트되어야 합니다. GraphQL 문서 및 스키마 파일을 생성하는 방법에 대한 정보는 스키마 문서 업데이트하기를 참조하세요.

독자들을 돕기 위해 GraphQL API 문서에 새 페이지를 추가해야 합니다. 지침은 GraphQL API 페이지를 참조하세요.

변경 사항 엔트리 포함

모든 클라이언트를 대상으로 한 변경 사항에는 반드시 변경 사항 엔트리가 포함되어야 합니다.

Laziness

성능을 관리하기 위한 GraphQL에게 중요한 기술 중 하나는 lazy 값을 사용하는 것입니다. Lazy 값은 결과의 약속으로, 나중에 실행될 수 있도록 허용하여 쿼리 트리의 다른 부분에 대한 일괄 처리를 가능하게 합니다. 코드에서 lazy 값의 주요 예제는 GraphQL BatchLoader입니다.

직접 lazy 값 관리를 위해서는 Gitlab::Graphql::Lazy 및 특히 Gitlab::Graphql::Laziness를 참조하십시오. 이는 약속의 생성 및 제거의 기본 작업을 돕는 #force#delay를 포함하고 있습니다.

약속을 강제로 처리하지 않고 lazy 값을 다루려면 Gitlab::Graphql::Lazy.with_value를 사용하십시오.