백엔드 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의 GraphQL API에 대한 Deep Dive를 개최하였습니다(오직 GitLab 팀원만: https://gitlab.com/gitlab-org/create-stage/issues/1).

이 자리는 향후 이 코드베이스의 일부분에서 작업할 수 있는 누구와 도메인 특정 지식을 공유하기 위해 마련되었습니다.

YouTube에서 녹화본을 찾아보세요, 그리고 슬라이드는 Google SlidesPDF에서 확인할 수 있습니다.

그 이후로 특정 세부사항은 변경되었지만, 여전히 좋은 소개 자료로 활용될 수 있습니다.

GitLab에서 GraphQL 구현하기

우리는 Robert Mosolgo가 작성한 GraphQL Ruby gem을 사용합니다.

또한 우리는 GraphQL Pro에 구독하고 있습니다.

자세한 내용은 GraphQL Pro 구독을 참조하세요.

모든 GraphQL 쿼리는 단일 엔드포인트로 전달됩니다

(app/controllers/graphql_controller.rb#execute),

API 엔드포인트로는 /api/graphql이 노출됩니다.

GraphiQL

GraphiQL은 기존 쿼리로 실험할 수 있는 인터랙티브 GraphQL API 탐색기입니다.

https://<your-gitlab-site.com>/-/graphql-explorer에서 모든 GitLab 환경에서 접근할 수 있습니다.

예를 들어, GitLab.com의 경우입니다.

GraphQL 변경 사항이 있는 병합 요청 검토하기

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

GraphQL 파일을 수정하거나 엔드포인트를 추가하는 병합 요청을 검토하라는 요청을 받으면, 우리의 GraphQL 검토 가이드를 참조하세요.

GraphQL 로그 읽기

GraphQL 로그 읽기 가이드를 참조하여 GraphQL 요청의 로그를 검사하고 GraphQL 쿼리의 성능을 모니터링하는 방법에 대한 팁을 확인하세요.

인증

인증은 GraphqlController를 통해 이루어지며, 현재 이는 Rails 애플리케이션과 동일한 인증을 사용합니다. 따라서 세션을 공유할 수 있습니다.

쿼리 문자열에 private_token을 추가하거나 HTTP_PRIVATE_TOKEN 헤더를 추가하는 것도 가능합니다.

제한 사항

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

최대 페이지 크기

기본적으로는 연결이 페이지당 정의된 최대 개수의 레코드만 반환할 수 있습니다.

개발자는 연결을 정의할 때 사용자 지정 최대 페이지 크기를 지정할 수 있습니다.

최대 복잡도

복잡도에 대한 설명은 클라이언트 대면 API 페이지에 있습니다.

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

쿼리의 복잡도 점수는 쿼리할 수 있습니다.

요청 시간 초과

요청은 30초 후에 시간 초과됩니다.

최대 필드 호출 수 제한

특정 필드를 여러 부모 노드에서 평가하는 것을 방지하려는 경우가 있습니다. 이는 N+1 쿼리 문제를 초래하며 최적의 솔루션이 없습니다. 이것은 최후의 수단으로 고려되어야 하며, lookahead를 통한 사전 로드 연관 또는 배치 사용과 같은 방법이 고려된 경우에만 사용해야 합니다.

예를 들면:

# 이 사용법은 예상한 것입니다.
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

또는 다음과 같이 리졸버 클래스에 확장을 적용할 수 있습니다:

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

이 제한을 추가할 때는 영향받는 필드의 description도 적절히 업데이트해야 합니다. 예를 들면,

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

파괴적인 변경 사항

GitLab GraphQL API는 버전 없는 것으로, 개발자는 우리의 사용 중단 및 제거 프로세스에 익숙해져야 합니다.

파괴적인 변경 사항은 다음과 같습니다:

  • 필드, 인수, 열거형 값 또는 변이를 제거하거나 이름을 변경하는 것.
  • 인수의 유형 또는 유형 이름을 변경하는 것. 인수의 유형은 변수 사용 시 클라이언트에 의해 선언되며, 이는 이전 유형 이름을 사용하는 쿼리가 API에 의해 거부되도록 합니다.
  • 필드 또는 열거형 값의 스칼라 유형을 변경하여 값이 JSON으로 직렬화되는 방식에 변화가 생기는 경우. 예를 들어, JSON 문자열에서 JSON 숫자로의 변경 혹은 문자열 형식의 변경입니다. 다른 객체 유형으로의 변경은 허용될 수 있으며, 객체의 모든 스칼라 유형 필드가 동일하게 직렬화되는 한 가능합니다.
  • 필드의 복잡도 또는 리졸버의 복잡도 배수를 높이는 것.
  • 필드를 널이 아님 (null: false)에서 (null: true)로 변경하는 것, 널 가능 필드에서 논의된 바와 같이.
  • 인수를 선택적(required: false)에서 필수(required: true)로 변경하는 것.
  • 연결의 최대 페이지 크기를 변경하는 것.
  • 쿼리 복잡도 및 깊이에 대한 전역 제한을 낮추는 것.
  • 이전에 허용되었던 쿼리가 한계를 초과할 수 있는 기타 모든 것.

항목을 사용 중단하기 위한 사용 중단 스키마 항목 섹션을 참조하세요.

변경 사항 면제

GraphQL API 변경 사항 면제 문서를 참조하세요.

글로벌 ID

GitLab GraphQL API는 글로벌 ID를 사용합니다(예: "gid://gitlab/MyObject/123")

그리고 데이터베이스 기본 키 ID를 사용하지 않습니다.

글로벌 ID는 클라이언트 측 라이브러리에서 캐싱 및 가져오기를 위해 사용되는 관례입니다.

또한 참조하세요:

우리는 입력 및 출력 인수의 값이 GlobalID일 때 사용해야 하는 사용자 정의 스칼라 유형(Types::GlobalIDType)을 가지고 있습니다. 이 유형을 ID 대신 사용하는 이점은 다음과 같습니다:

  • 값이 GlobalID인지 확인합니다.
  • 사용자 코드에 전달하기 전에 GlobalID로 구문 분석합니다.
  • 객체의 유형에 따라 매개변수화할 수 있습니다(예: GlobalIDType[Project]) 이는 더 나은 검증과 보안을 제공합니다.

모든 새로운 인수 및 결과 유형에 이 유형을 사용하는 것을 고려하세요. 더 넓은 범위의 객체를 수용하려면 이 유형을 관심사나 수퍼타입으로 매개변수화하는 것이 완벽하게 가능하다는 점을 기억하세요(예: GlobalIDType[Issuable]GlobalIDType[Issue]).

최적화

기본적으로 GraphQL은 N+1 문제를 유발하는 경향이 있습니다. 이를 최소화하려고 적극적으로 노력하지 않는 한.

안정성과 확장성을 위해, 우리의 쿼리가 N+1 성능 문제로 고통받지 않도록 해야 합니다.

다음은 GraphQL 코드를 최적화하는 데 도움이 되는 도구 목록입니다:

개발에서 N+1 문제를 확인하는 방법

N+1 문제는 다음과 같이 기능 개발 중에 발견될 수 있습니다:

  • 데이터를 반환하는 GraphQL 쿼리를 실행하는 동안 development.log를 태일링합니다. Bullet가 도움이 될 수 있습니다.
  • GitLab UI에서 쿼리를 실행할 때 성능 바를 관찰합니다.
  • 기능에 N+1 문제가 없거나 제한적이라는 것을 확인하는 요청 사양을 추가합니다.

필드

유형

우리는 코드 우선 스키마를 사용하며, 모든 유형이 Ruby에서 무엇인지 선언합니다.

예를 들어, 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입니다 (자세한 내용은 그래프QL::Types::ID를 사용할 때 참조). name은 일반 GraphQL::Types::String 유형입니다.

스칼라 데이터 유형을 위해 사용자 정의 GraphQL 데이터 유형을 선언할 수도 있습니다(예: TimeType).

GraphQL API를 통해 모델을 노출할 때 app/graphql/types에 새로운 유형을 생성합니다.

유형에서 속성을 노출할 때, 정의 내의 논리를 가능한 한 최소화해야 합니다. 대신 로직을 프레젠터로 이동하는 것을 고려하세요:

class Types::MergeRequestType < BaseObject
  present_using MergeRequestPresenter

  name 'MergeRequest'
end

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

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

Nullable 필드

GraphQL은 필드를 “nullable” 또는 “non-nullable”로 설정할 수 있습니다. 전자는 지정된 유형의 값 대신 null이 반환될 수 있음을 의미합니다. 일반적으로, 다음과 같은 이유로 nullable 필드를 non-nullable 필드보다 사용하는 것을 선호해야 합니다:

  • 데이터가 필수에서 비필수로 전환되는 것이 일반적이며, 다시 필수가 될 수 있습니다.

  • 필드가 선택적으로 변할 가능성이 없더라도, 쿼리 시간에 사용 가능하지 않을 수 있습니다.

    • 예를 들어, blob의 content는 Gitaly에서 조회해야 할 수도 있습니다.

    • 만약 content가 nullable이라면, 전체 쿼리가 실패하는 대신 부분적인 응답을 반환할 수 있습니다.

  • non-nullable 필드에서 nullable 필드로의 변경은 버전 없는 스키마로는 어렵습니다.

non-nullable 필드는 필수가 필요하고, 앞으로 선택적이 될 가능성이 매우 낮으며, 계산하기 간단한 경우에만 사용해야 합니다. 예를 들어 id 필드가 그렇습니다.

non-nullable GraphQL 스키마 필드는 느낌표(bang) !가 뒤따르는 객체 유형입니다. 다음은 gitlab_schema.graphql 파일의 예입니다:

  id: ProjectID!

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


  errors: [String!]!

추가 읽기:

전역 ID 노출

GitLab에서 Global IDs를 사용하는 것에 맞춰, 데이터베이스 기본 키 ID를 노출할 때 항상 Global ID로 변환해야 합니다.

id라는 이름의 모든 필드는 자동으로 변환됩니다 객체의 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입니다. 예를 들어 Project 또는 Issue입니다.

따라서 다음과 같습니다:

  • Project.fullPath는 API 전반에 걸쳐 동일한 fullPath를 가진 다른 Project가 없기 때문에 ID여야 하며, 필드 또한 식별자입니다.

  • Issue.iid는 API 전반에 걸쳐 동일한 iid를 가진 여러 Issue 유형이 있을 수 있기 때문에 ID가 되어서는 안 됩니다. 이를 ID로 처리하는 것은 클라이언트가 다양한 프로젝트에서 Issue의 캐시를 가지고 있는 경우 문제가 될 것입니다.

  • Project.id는 통상적으로 ID가 되어야 할 자격이 있지만, 데이터베이스 ID 값을 위해 ID 유형 대신 Global ID 유형을 사용하므로 Global ID로 타입을 지정해야 합니다.

이는 다음 표에서 요약됩니다:

필드 목적 GraphQL::Types::ID 사용?
전체 경로
데이터베이스 ID 아니요
IID 아니요

markdown_field

markdown_fieldfield를 래핑하는 헬퍼 메서드이며, 항상 렌더링된 Markdown을 반환하는 필드에 사용해야 합니다.

이 헬퍼는 GraphQL 쿼리의 컨텍스트를 사용하여 모델의 Markdown 필드를 렌더링합니다.

헬퍼에 컨텍스트가 제공되는 것은 현재 사용자가 볼 수 없는 리소스에 대한 링크를 수정하는 데 필요합니다.

HTML 렌더링이 쿼리를 유발할 수 있기 때문에 이러한 필드의 복잡성은 기본 값보다 5만큼 증가합니다.

Markdown 필드 헬퍼는 다음과 같이 사용할 수 있습니다:

markdown_field :note_html, null: false

이것은 모델의 Markdown 필드 note를 렌더링하는 필드를 생성합니다. method: 인수를 추가하여 오버라이드할 수 있습니다.

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

이 필드는 기본적으로 다음과 같은 설명을 갖습니다:

note의 GitLab Flavored Markdown 렌더링

description: 인수를 전달하여 오버라이드할 수 있습니다.

연결 유형

참고:

구현의 세부사항은 페이징 구현을 참조하세요.

GraphQL은 커서 기반 페이징을 사용하여 항목 컬렉션을 노출합니다. 이는 클라이언트에게 많은 유연성을 제공하며 백엔드가 다양한 페이징 모델을 사용할 수 있도록 합니다.

리소스 컬렉션을 노출하기 위해 연결 유형을 사용할 수 있습니다. 이는 기본 페이징 필드로 배열을 래핑합니다. 예를 들어 프로젝트 파이프라인에 대한 쿼리는 다음과 같이 생길 수 있습니다:

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

이 쿼리는 프로젝트의 첫 번째 2개의 파이프라인과 관련된 페이징 정보를 반환하며, ID의 내림차순으로 정렬됩니다. 반환된 데이터는 다음과 같습니다:

{
  "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는 연결당 app/graphql/gitlab_schema.rb에서 정의된 최대 레코드 수를 반환하며, 클라이언트가 제한 인수(first: 또는 last:)를 제공하지 않을 경우 페이지당 반환되는 기본 레코드 수입니다.

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

caution

기본값이 GraphQL API의 성능을 유지하기 위해 설정되어 있으므로, max_page_size를 올리는 것보다 페이지당 많은 양의 레코드가 필요하지 않도록 프론트엔드 클라이언트나 제품 요구 사항을 변경하는 것이 더 좋습니다.

예를 들어:

field :tags,
  Types::ContainerRepositoryTagType.connection_type,
  null: true,
  description: '컨테이너 리포지토리의 태그',
  max_page_size: 20

필드 복잡성

GitLab GraphQL API는 과도하게 복잡한 쿼리가 수행되는 것을 제한하기 위해 복잡성 점수를 사용합니다.

복잡성에 대한 설명은 우리의 클라이언트 문서에서 확인할 수 있습니다.

복잡성 제한은 app/graphql/gitlab_schema.rb에서 정의됩니다.

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

개발자는 데이터를 반환하기 위해 서버에서 더 많은 _작업_을 수행해야 하는 필드에 대해 더 높은 복잡성을 지정해야 합니다. 대부분의 경우와 같이, idtitle과 같은 필드는 0의 복잡성을 가질 수 있습니다.

calls_gitaly

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

예를 들어:

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

이는 필드의 complexity 점수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에서 상속받으며, 이는 권한을 비 null 불리언으로 노출할 수 있는 일부 도우미 메서드를 포함합니다:

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: 기본 설명과 유형을 설정하고 그들을 비 null로 만들어 graphql-rubyfield 메서드와 같은 방식으로 작동합니다. 이러한 옵션은 여전히 인수로 추가하여 재정의할 수 있습니다.

  • ability_field: 정책에서 정의된 능력을 노출합니다. 이는 permission_field와 동일한 방식으로 작동하며 동일한 인수를 재정의할 수 있습니다.

  • abilities: 정책에서 정의된 여러 능력을 한 번에 노출할 수 있게 해줍니다. 이러한 필드는 모두 비 null 불리언이며 기본 설명이 있어야 합니다.

기능 플래그

GraphQL에서 기능 플래그를 구현하여 다음을 전환할 수 있습니다:

  • 필드의 반환 값.
  • 인수 또는 변형의 동작.

이는 리졸버, 타입 또는 심지어 모델 메소드에서, 개인의 선호도와 상황에 따라 수행할 수 있습니다.

참고: 기능 플래그 뒤에 있는 동안 항목을 Alpha로 표시하는 것이 권장됩니다. 이는 공개 GraphQL API의 소비자에게 해당 필드가 아직 사용되지 않을 것임을 알리는 신호입니다. 또한 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

기능 플래그가 적용된 인수

인수는 기능 플래그 상태에 따라 무시되거나 값이 변경될 수 있습니다. 기능 플래그가 비활성화된 경우 인수를 무시하는 것이 일반적인 사용입니다:

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

기능 플래그가 적용된 변형

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

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와의 하위 호환성을 유지함을 의미합니다.

필드, 인수, 열거형 값 또는 변형을 제거하기보다, 대신 사용 중단 해야 합니다.

스키마의 사용 중단된 부분은 GitLab 사용 중단 프로세스에 따라 향후 릴리스에서 제거될 수 있습니다.

GraphQL에서 스키마 항목을 사용 중단하려면:

  1. 해당 항목에 대한 사용 중단 문제를 생성합니다.
  2. 스키마에서 해당 항목을 사용 중단으로 표시합니다.

또한 참조:

사용 중지 이슈 생성

모든 GraphQL 사용 중지는 사용 중지 문제를 생성해야 하며, Deprecations 문제 템플릿을 사용하여 사용 중지 및 제거를 추적합니다.

사용 중지 문제에 다음 두 개의 레이블을 적용합니다:

  • ~GraphQL
  • ~deprecation

항목을 사용 중지로 표시

필드, 인수, 열거형 값 및 뮤테이션은 deprecated 속성을 사용하여 사용 중지됩니다. 속성의 값은 다음의 Hash입니다:

  • reason - 사용 중지 사유입니다.
  • milestone - 필드가 사용 중지된 이정표입니다.

예시:

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

사용 중지되는 항목의 원래 description은 유지되어야 하며,

사용 중지에 대한 언급을 위해 _업데이트_되어서는 안됩니다. 대신, reasondescription에 추가됩니다.

사용 중지 사유 스타일 가이드

사용 중지 사유가 필드, 인수 또는 열거형 값이 대체될 때에는 reason이 대체를 나타내야 합니다. 예를 들어, 다음은 대체된 필드에 대한 reason입니다:

Use `otherFieldName`

예제:

field :designs, ::Types::DesignManagement::DesignCollectionType, null: true,
      deprecated: { reason: 'Use `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도 변경됩니다.

스키마 내 어디든 Global ID가 인수 타입으로 사용되는 경우, 글로벌 ID 변경은 일반적으로 파괴적인 변경 사항을 구성합니다.

구형 Global ID 인수를 사용하는 클라이언트를 계속 지원하기 위해 Gitlab::GlobalId::Deprecations에 사용 중지를 추가합니다.

참고: 글로벌 ID가 오직 필드로暴露되는 경우 및 사용 중지할 필요가 없습니다. 우리는 필드 내 글로벌 ID의 표현 방식 변경을 하위 호환 가능한 것으로 간주합니다. 클라이언트가 이러한 값을 파싱하지 않을 것으로 예상합니다: 이 값은 불투명한 토큰으로 취급되어야 하며, 그 안의 구조는 부수적이며 의존해서는 안됩니다.

예시 시나리오:

이 예시 시나리오는 다음 머지 요청을 기반으로 합니다.

PrometheusService라는 모델이 Integrations::Prometheus로 이름이 변경될 예정입니다. 이전 모델 이름은 뮤테이션의 인수로 사용되는 글로벌 ID 타입을 생성하는 데 사용됩니다:

# Mutations::UpdatePrometheus:

argument :id, Types::GlobalIDType[::PrometheusService],
              required: true,
              description: "변경할 통합의 ID입니다."

클라이언트는 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"로 인수로 전달된 id를 거부합니다. 또는 쿼리 서명에서 인수 유형을 PrometheusServiceID로 지정하게 됩니다.

클라이언트가 변화를 겪지 않고 뮤테이션과 상호 작용할 수 있도록 하려면 Gitlab::GlobalId::Deprecations에서 DEPRECATIONS 상수를 편집하고 배열에 새 Deprecation을 추가하세요:

DEPRECATIONS = [
  Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(old_name: 'PrometheusService', new_name: 'Integrations::Prometheus', milestone: '14.0')
].freeze

그런 다음 일반적인 사용 중지 프로세스를 따릅니다. 이전 인수 스타일에 대한 지원을 제거할 때는 Deprecation을 제거합니다:

DEPRECATIONS = [].freeze

사용 중지 기간 동안 API는 인수 값에 대해 다음 형식을 수용합니다:

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

API는 또한 인수에 대한 쿼리 서명에서 이러한 유형을 수용합니다:

  • PrometheusServiceID
  • IntegrationsPrometheusID

참고: 이전 유형(PrometheusServiceID가 예시)의 쿼리는 API에 의해 유효하고 실행 가능한 것으로 간주되지만, 검증 도구는 이를 유효하지 않은 것으로 간주합니다. 이는 우리가 @deprecated 지시문 외부의 맞춤형 방법을 사용 중지하고 있기 때문에 발생하며, 검증자는 지원 여부를 인식하지 못합니다.

문서에서는 이전 글로벌 ID 스타일이 이제 사용 중지되었다고 언급하고 있습니다.

스키마 항목을 Alpha로 표시하기

GraphQL 스키마 항목(필드, 인수, 열거형 값, 변이)을 Alpha로 표시할 수 있습니다.

Alpha로 표시된 항목은 사용 중단 프로세스에서 면제되며 통고 없이 언제든지 삭제될 수 있습니다. 항목이 변경될 가능성이 있고 공개 사용에 준비되지 않았을 때 Alpha로 표시하세요.

주의: 새로운 항목만 Alpha로 표시하세요. 기존 항목은 이미 공개되었기 때문에 Alpha로 표시하지 마세요.

스키마 항목을 Alpha로 표시하려면 alpha: 키워드를 사용하세요. Alpha 항목을 도입한 milestone:을 제공해야 합니다.

예를 들면:

field :token, GraphQL::Types::String, null: true,
      alpha: { milestone: '10.0' },
      description: '로그인을 위한 토큰입니다.'

유사하게, 변이를 Alpha로 표시하려면 app/graphql/types/mutation_type.rb에 변이가 설치된 위치를 업데이트해야 합니다:

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

Alpha GraphQL 항목은 GraphQL 사용 중단을 활용하는 GitLab의 사용자 정의 기능입니다. Alpha 항목은 GraphQL 스키마에서 사용 중단된 것으로 표시됩니다. 모든 사용 중단된 스키마 항목과 마찬가지로 대화형 GraphQL 탐색기 (GraphiQL)에서 Alpha 필드를 테스트할 수 있습니다. 그러나 GraphiQL 자동완성 편집기는 사용 중단된 필드를 제안하지 않는다는 점에 유의하세요.

항목은 생성된 GraphQL 문서와 그 GraphQL 스키마 설명에서 Alpha로 표시됩니다.

열거형

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

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

예를 들면:

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

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

열거형이 루비 클래스 속성으로 사용되고 대문자 문자열이 아닐 경우, 대문자 값을 조정하는 value: 옵션을 제공할 수 있습니다.

다음 예에서:

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

    value 'OPENED', value: 'opened', description: '열린 에픽입니다.'
    value 'CLOSED', value: 'closed', description: '닫힌 에픽입니다.'
  end
end

열거형 값은 deprecated 키워드를 사용하여 사용 중단될 수 있습니다.

Rails 열거형에서 GraphQL 열거형을 동적으로 정의하기

GraphQL 열거형이 Rails 열거형에 의해 지원되는 경우, Rails 열거형을 사용하여 GraphQL 열거형 값을 동적으로 정의하는 것을 고려하세요. 이렇게 하면 GraphQL 열거형 값이 Rails 열거형 정의에 바인딩되어, 값이 Rails 열거형에 추가될 경우 GraphQL 열거형이 자동으로 변경사항을 반영합니다.

예:

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

그래프QL에서 반환할 데이터가 JSON으로 저장될 때, 가능한 한 그래프QL 타입을 계속 사용해야 합니다. JSON 데이터가 진정으로 비구조적이지 않은 한 GraphQL::Types::JSON 타입 사용을 피하십시오.

JSON 데이터의 구조가 다양하지만 알려진 가능한 구조 집합 중 하나일 경우, union을 사용하십시오. 이 목적을 위한 union 사용의 예는 !30129입니다.

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

다음 JSON 데이터가 주어졌을 때:

{
  "title": "My chart",
  "data": [
    { "x": 0, "y": 1 },
    { "x": 1, "y": 1 },
    { "x": 2, "y": 2 }
  ]
}

다음과 같이 그래프QL 타입을 사용할 수 있습니다:

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} of the {y} 형식을 사용하십시오. 여기서 {x}는 설명하고자 하는 항목이고, {y}는 적용되는 리소스입니다. 예를 들면:

이슈의 ID입니다.
에픽의 작성자입니다.

정렬 또는 검색 인수는 적절한 동사로 시작하십시오.

지정된 값을 나타내기 위해, 간결성을 위해 this를 사용할 수 있습니다. 예를 들면:

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

일관성과 간결성을 위해 설명을 TheA로 시작하지 마십시오.

모든 설명은 마침표(.)로 끝내십시오.

불리언

불리언 필드(GraphQL::Types::Boolean)의 경우, 무엇을 하는지를 설명하는 동사로 시작하십시오. 예를 들면:

이슈가 기밀임을 나타냅니다.

필요한 경우 기본값을 제공하십시오. 예를 들면:

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

Enum 정렬

정렬을 위한 Enums의 설명은 'Values for sorting {x}.'이어야 합니다. 예를 들어:

컨테이너 리포지토리 정렬을 위한 값들입니다.

Types::TimeType 필드 설명

Types::TimeType GraphQL 필드에 대해 timestamp라는 단어를 포함해야 합니다. 이는 독자가 속성의 형식이 단순히 Date가 아니라 Time임을 알 수 있게 합니다.

예를 들어:

field :closed_at, Types::TimeType, description: '이슈가 닫힌 시간의 타임스탬프입니다.'

copy_field_description 헬퍼

때때로 두 설명을 항상 동일하게 유지하고 싶습니다. 예를 들어, 동일한 속성을 나타내는 경우 타입 필드 설명과 변형 인수가 같도록 유지하는 것입니다.

설명을 제공하는 대신 copy_field_description 헬퍼를 사용하여 설명을 복사할 타입과 필드 이름을 전달할 수 있습니다.

예시:

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입니다.

구독 티어 배지

필드나 인수가 다른 필드보다 더 높은 구독 티어에서 사용할 수 있는 경우, 가용성 세부정보를 인라인으로 추가하세요.

예를 들어:

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

권한

참조: GraphQL 권한

리졸버

응용 프로그램이 어떻게 응답을 제공하는지 _리졸버_를 사용하여 정의합니다.

리졸버는 문제의 객체를 검색하는 실제 구현 논리를 제공합니다.

필드에 표시할 객체를 찾기 위해, 우리는 app/graphql/resolvers에 리졸버를 추가할 수 있습니다.

리졸버에서 인수를 정의할 수 있는 방법은 변형에서와 동일합니다. 인수 섹션을 참조하세요.

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

리졸버 작성

우리 코드의 목적은 찾기 및 서비스를 감싸는 얇은 선언적 래퍼가 되는 것입니다. 인수 목록을 반복하거나 이를 Concern으로 추출할 수 있습니다. 대부분의 경우 조합이 상속보다 선호됩니다. 리졸버는 컨트롤러처럼 취급해야 합니다: 리졸버는 다른 응용 프로그램 추상화를 구성하는 DSL이어야 합니다.

예를 들어:

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

같은 리졸버 클래스를 동일한 객체가 노출되는 두 개의 서로 다른 필드에서 사용할 수 있지만, 리졸버 객체를 직접 재사용해서는 안 됩니다. 리졸버는 권한 부여, 준비 상태 및 해상도가 프레임워크에 의해 조정되는 복잡한 생명 주기를 가지며, 각 단계에서 지연 값을 반환하여 배칭 기회를 활용할 수 있습니다. 응용 프로그램 코드에서 리졸버나 변형을 인스턴스화해서는 안 됩니다.

대신, 코드 재사용 단위는 응용 프로그램의 나머지 부분과 거의 동일합니다:

  • 데이터를 조회하기 위한 쿼리의 찾기기.
  • 작업을 수행하기 위한 변형의 서비스.
  • 쿼리별로 특정한 로더(배칭 인지 찾기기).

변형에서 배칭을 사용할 이유는 결코 없습니다. 변형은 시리즈로 실행되므로 배칭 기회가 없습니다. 모든 값은 요청되자마자 즉시 평가되므로 배칭이 불필요한 오버헤드입니다. 다음을 작성하는 경우:

  • Mutation, 개체를 직접 조회하는 것이 자유롭습니다.
  • Resolver 또는 BaseObject의 메서드를 작성하는 경우, 배칭을 허용해야 합니다.

오류 처리

해결자는 오류를 발생시킬 수 있으며, 이는 적절하게 최상위 오류로 변환됩니다.

모든 예상되는 오류는 포착되어 적절한 GraphQL 오류로 변환되어야 합니다(참고: Gitlab::Graphql::Errors).

포착되지 않은 오류는 억제되며 클라이언트는 Internal service error라는 메시지를 받게 됩니다.

특별한 경우는 권한 오류입니다. REST API에서는 사용자가 접근할 권한이 없는 리소스에 대해서는 404 Not Found를 반환합니다. GraphQL에서의 동등한 동작은 모든 누락되거나 권한이 없는 리소스에 대해 null을 반환하는 것입니다.

쿼리 해결자는 권한이 없는 리소스에 대해 오류를 발생시켜서는 안 됩니다.

이는 클라이언트가 기록의 부재와 접근할 수 없는 기록의 존재를 구분할 수 없어야 하기 때문입니다. 그렇게 하는 것은 보안 취약점이 될 수 있으며, 숨기고자 하는 정보를 누설하게 됩니다.

대부분의 경우 이 문제에 대해 걱정할 필요는 없습니다. 이는 authorize DSL 호출로 선언한 해결자 필드 권한 부여에 의해 올바르게 처리됩니다. 그러나 더 사용자 정의된 작업이 필요한 경우, 필드를 해결할 때 current_user가 접근할 수 없는 객체를 발견하면 전체 필드는 null로 해결되어야 한다는 것을 기억하세요.

해결자 파생

(BaseResolver.singleBaseResolver.last 포함)

일부 용도에 대해 해결자를 다른 해결자에서 파생할 수 있습니다.

이의 주요 용도는 모든 항목을 찾기 위한 하나의 해결자와 특정 항목을 찾기 위한 또 다른 해결자입니다. 이를 위해 편의 메서드를 제공합니다:

  • BaseResolver.single, 첫 번째 항목을 선택하는 새로운 해결자를 생성합니다.
  • BaseResolver.last, 마지막 항목을 선택하는 해결자를 생성합니다.

올바른 단수 유형은 컬렉션 유형에서 유추되므로, 여기에서 type를 정의할 필요가 없습니다.

이러한 메서드를 사용하기 전에, 다음 중 더 간단할 수 있는지 고려하세요:

  • 고유한 인수를 정의하는 또 다른 해결자를 작성합니다.
  • 쿼리를 추상화하는 컨cern을 작성합니다.

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

그런 다음 이러한 해결자를 필드에서 사용할 수 있습니다:

# In PipelineType

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

리졸버 최적화

Look-Ahead

전체 쿼리는 실행 중에 미리 알려져 있으므로, 쿼리를 최적화하고 필요한 연관 관계를 배치 로드하기 위해 lookahead를 사용할 수 있습니다. N+1 성능 문제를 피하기 위해 리졸버에 lookahead 지원을 추가하는 것을 고려하세요.

일반적인 lookahead 사용 사례(자식 필드가 요청될 때 연관 관계를 미리 로드)에 대한 지원을 활성화하려면 LooksAhead를 포함할 수 있습니다. 예를 들어:

# `MyThing` 모델이 `[child_attribute, other_attribute, nested]` 속성을 갖고,
# nested가 `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

배치 로딩

GraphQL BatchLoader를 참조하세요.

Resolver#ready?의 올바른 사용

Resolvers는 프레임워크의 일부로 두 가지 공개 API 메서드를 가지고 있습니다: #ready?(**args)#resolve(**args).

#ready?를 사용하여 설정을 수행하거나 #resolve를 호출하지 않고 조기 반환할 수 있습니다.

#ready?를 사용할 좋은 이유는 다음과 같습니다:

  • 결과가 불가능하다는 것을 미리 알고 있다면 Relation.none을 반환합니다.
  • 인스턴스 변수를 초기화하는 것과 같은 설정을 수행합니다 (이를 위해 지연 초기화 메서드를 고려하십시오).

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

def ready?(**args)
  [false, 'have this instead']
end

이러한 이유로, resolver를 호출할 때 (주로 테스트에서 프레임워크 추상화를 재사용 가능하지 않다고 생각하므로, finder를 선호합니다), ready? 메서드를 호출하고 boolean 플래그를 확인한 후 resolve를 호출하는 것을 기억하세요! 예시는 우리의 GraphqlHelpers에서 볼 수 있습니다.

인수를 검증할 때는 validators#ready?를 사용하는 것보다 선호됩니다.

부정 논자

부정 필터는 일부 리소스를 필터링할 수 있습니다 (예를 들어, bug 라벨이 할당된 모든 이슈를 찾되, bug2 라벨이 할당되지 않은 경우). not 인자는 부정 논자를 전달하기 위한 선호 구문입니다:

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

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

extend ::Gitlab::Graphql::NegatableArguments

negated do
  argument :labels, [GraphQL::STRING_TYPE],
            required: false,
            as: :label_name,
            description: '해결된 모든 병합 요청은 이러한 레이블이 없습니다.'
end

메타데이터

resolvers를 사용할 때, 필드 메타데이터의 SSoT로 작용할 수 있어야 합니다. 필드 이름을 제외한 모든 필드 옵션은 resolver에서 선언할 수 있습니다. 여기에는 다음이 포함됩니다:

  • type (필수 - 모든 resolver에는 타입 주석이 포함되어야 합니다)
  • extras
  • description
  • Gitaly 주석 (calls_gitaly! 포함)

예시:

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

자식 Presenter에 부모 객체 전달

때때로 필드를 계산하기 위해 자식 컨텍스트에서 해결된 쿼리 부모에 접근해야 합니다. 일반적으로 부모는 Resolver 클래스에서 parent로만 사용할 수 있습니다.

Presenter 클래스에서 부모 객체를 찾으려면:

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

      def resolve(**args)
        context[:parent_object] = parent
      end
    
  2. resolver 또는 필드가 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. Presenter 클래스에서 필드의 메서드를 선언하고 parent 키워드 인수를 받도록 합니다. 이 인수는 부모 GraphQL 컨텍스트를 포함하므로, Resolver에서 사용한 키로 parent[:parent_object]로 부모 객체에 접근해야 합니다:

      # ChildPresenter에서
      def my_computing_method(parent:)
        # 여기서 `parent[:parent_object]`로 무언가를 합니다
      end
    
      # SomeTypeResolver에서
    
      def resolve(parent:)
        # ...
      end
    

실제 사용 예는 IterationPresenterscopedPathscopedUrl을 추가한 이 MR을 확인하세요.

변형

변형은 저장된 값을 변경하거나 동작을 트리거하는 데 사용됩니다.

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 변형 이름은 역사적으로 일관성이 없지만, 새로운 변형 이름은 '{Resource}{Action}' 또는 '{Resource}{Action}{Attribute}' 규칙을 따라야 합니다.

새로운 리소스를 생성하는 변형은 동사 Create를 사용해야 합니다.

예시:

  • CommitCreate

데이터를 업데이트하는 변형은 다음을 사용해야 합니다:

  • 동사 Update.
  • 더 적절한 경우 도메인 특정 동사인 Set, Add 또는 Toggle.

예시:

  • EpicTreeReorder
  • IssueSetWeight
  • IssueUpdate
  • TodoMarkDone

데이터를 제거하는 변형은:

  • Destroy가 아닌 동사 Delete를 사용해야 합니다.
  • 더 적절한 경우 도메인 특정 동사인 Remove.

예시:

  • AwardEmojiRemove
  • NoteDelete

변형 이름에 대해 조언이 필요한 경우, Slack #graphql 채널에서 피드백을 요청하십시오.

필드

가장 일반적인 상황에서, 변형(mutation)은 두 개의 필드를 반환합니다:

  • 수정된 리소스
  • 작업이 수행될 수 없는 이유를 설명하는 오류 목록. 변형이 성공하면 이 목록은 비어 있습니다.

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

resolve 메서드

리졸버 작성하기와 유사하게, 변형의 resolve 메서드는 서비스 주위에 얇은 선언적 래퍼를 목표로 해야 합니다.

resolve 메서드는 변형의 인수를 키워드 인수로 수신합니다. 여기에서 우리는 리소스를 수정하는 서비스를 호출할 수 있습니다.

resolve 메서드는 변형에 정의된 것과 동일한 필드 이름을 가진 해시를 반환해야 하며, errors 배열을 포함해야 합니다. 예를 들어, Mutations::MergeRequests::SetDraftmerge_request 필드를 정의합니다:

field :merge_request,
      Types::MergeRequestType,
      null: true,
      description: "변형 후의 병합 요청입니다."

이는 이 변형의 resolve에서 반환되는 해시가 다음과 같아야 함을 의미합니다:

{
  # 수정된 병합 요청, 이는 필드에 정의된 유형으로 래핑됩니다
  merge_request: merge_request,
  # 권한 부여 후 변형이 실패한 경우 문자열 배열입니다.
  # `errors_on_object` 헬퍼는 `errors.full_messages`를 수집합니다
  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: ["you cannot touch the thing"],
      thing: { .. }
    }
  }
}

여기에는 다음과 같은 예가 포함됩니다:

  • 모델 검증 오류: 사용자가 입력값을 변경해야 할 수 있습니다.
  • 권한 오류: 사용자는 이를 수행할 수 없다는 것을 알아야 하며, 권한 요청이나 로그인이 필요할 수 있습니다.
  • 사용자의 행동을 방해하는 애플리케이션 상태 문제 (예: 병합 충돌 또는 잠긴 리소스).

이론적으로, 우리는 사용자가 이 단계까지 도달하는 것을 방지해야 하지만, 만약 도달하게 된다면,

무엇이 잘못되었는지 전달해야 하며, 실패의 원인을 이해하고, 의도를 달성하기 위해 무엇을 할 수 있는지 알아야 합니다. 예를 들어, 단순히 요청을 재시도해야 할 수 있습니다.

복구 가능한 오류를 변형 데이터와 함께 반환할 수 있습니다. 예를 들어,

사용자가 10개의 파일을 업로드하고 그 중 3개가 실패하고 나머지가 성공하는 경우, 실패에 대한 오류를 사용자가 보고,

성공에 대한 정보와 함께 제공할 수 있습니다.

실패 (사용자와 무관함)

하나 이상의 복구 불가능한 오류가 _상위 수준_에서 반환될 수 있습니다. 이는 사용자가 거의 제어할 수 없는 문제이며,

주로 시스템 또는 프로그래밍 문제여야 하며, 개발자가 알아야 합니다.

이 경우 data는 없습니다:

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

이는 변형 동안 오류가 발생하여 발생한 것입니다. 우리의 구현에서는

인수 오류 및 검증 오류의 메시지가 클라이언트에 반환되며, 그 외의 모든

StandardError 인스턴스는 포착되어 기록되고 클라이언트에 "Internal server error"라는 메시지가 설정된 채로 제공됩니다.

자세한 내용은 GraphqlController를 참조하세요.

이들은 프로그래밍 오류를 나타냅니다, 예를 들어:

  • IntString 대신 전달되었거나, 필수 인수가 누락된 경우의 GraphQL 구문 오류.
  • 비 NULL 필드에 대한 값을 제공할 수 없는 스키마 오류.
  • 시스템 오류: 예를 들어, Git 저장소 예외 또는 데이터베이스 사용 불가.

사용자는 일반적인 사용 중에 이러한 오류를 일으킬 수 없어야 합니다. 이 오류 범주는 내부적으로 처리되어야 하며,

사용자에게 구체적인 세부정보가 표시되지 않아야 합니다.

변형이 실패할 때 사용자에게 알릴 필요가 있지만,

왜 그런지 알릴 필요는 없습니다, 왜냐하면 그들은 이를 유발할 수 없고,

그들이 할 수 있는 방법으로는 수정할 수 없기 때문입니다. 다만 변형을 재시도하자고 제안할 수 있습니다.

오류 분류

변이를 작성할 때, 오류 상태가 두 가지 범주 중 어느 하나에 속하는지 인식하고, 이를 프론트엔드 개발자와 소통하여 우리의 가정을 검증해야 합니다. 이는 _사용자_의 요구와 _클라이언트_의 요구를 구별하는 것을 의미합니다.

사용자가 알아야 할 필요가 없으면 오류를 잡지 마십시오.

사용자가 알아야 할 필요가 있는 경우, 프론트엔드 개발자와 소통하여 우리가 전달하는 오류 정보가 관련성이 있고 목적에 부합하는지 확인해야 합니다.

또한 프론트엔드 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 변이 사용 중단

EE 변이도 동일한 프로세스를 따라야 합니다. 병합 요청 프로세스의 예를 보려면 병합 요청 !42588을 읽어보세요.

구독

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

클라이언트가 구독에 가입하면, 우리는 Puma 작업자에서 그들의 쿼리를 메모리에 저장합니다. 그런 다음 구독이 트리거되면, Puma 작업자는 저장된 GraphQL 쿼리를 실행하고 결과를 클라이언트에 푸시합니다.

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

구독 구축

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이 아닐 수 있음을 의미합니다.

필수 인수의 값이 null일 수 있는 경우, required: :nullable 선언을 사용하세요.

예:

argument :due_date,
         Types::TimeType,
         required: :nullable,
         description: '문제에 대한 원하는 기한입니다. 기한이 null이면 제거됩니다.'

위의 예에서 due_date 인수는 제공되어야 하지만, GraphQL 사양과는 달리 값이 null일 수 있습니다.

이렇게 하면 기한을 제거하기 위해 새로운 변형을 만드는 대신 단일 변형에서 기한을 ‘unset’할 수 있습니다.

{ due_date: null } # => OK
{ due_date: "2025-01-10" } # => OK
{  } # => 잘못됨 (제공되지 않음)

널 가능성과 required: false

인수가 required: false로 표시된 경우, 클라이언트는 값을 null로 보낼 수 있습니다.

이는 종종 바람직하지 않습니다.

인수가 선택적이지만 null이 허용되지 않는 값인 경우, 유효성을 검증하여 null을 전달하면 오류가 발생하도록 해야 합니다:

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

또는 허용되지 않는 값일 때 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

이러한 인수는 자동으로 MergeRequestSetDraftInput이라는 입력 유형을 생성하며, 여기에는 우리가 지정한 3개의 인수와 clientMutationId가 포함됩니다.

객체 식별자 인수

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

  • 객체가 전체 경로나 IID를 가질 경우 전체 경로 또는 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 객체와 관련된 모든 필드 및 인수의 유형으로 사용해야 합니다.

이 유형은
사용자 정의 스칼라로:

  • Ruby의 TimeDateTime 객체를 표준화된 ISO-8601 형식의 문자열로 변환합니다. 이는 그래프QL 필드의 유형으로 사용될 때 적용됩니다.

  • ISO-8601 형식의 시간 문자열을 Ruby Time 객체로 변환합니다. 이는 그래프QL 인수의 유형으로 사용될 때 적용됩니다.

이것은 우리의 GraphQL API가 시간을 표준화된 방식으로 표시하고 시간 입력을 처리할 수 있도록 합니다.

예시:

field :created_at, Types::TimeType, null: true, description: '이슈가 생성된 시간의 타임스탬프.'

전역 ID 스칼라

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

테스트

변형 및 해결자를 테스트하려면 테스트 단위를 전체 GraphQL 요청으로 고려하십시오. 해결자에 대한 호출이 아닙니다. 이는 종속성 업그레이드를 훨씬 더 어렵게 만드는 긴밀한 결합을 피할 수 있게 해줍니다.

당신은:

  • 해결자 및 변형에 대한 단위 스펙보다 전체 API 엔드포인트를 사용하거나
    GitlabSchema.execute를 통하여 요청 스펙을 선호해야 합니다.

  • GraphqlHelpers#execute_queryGraphqlHelpers#run_with_clean_state
    GraphqlHelpers#resolveGraphqlHelpers#resolve_field보다 선호해야 합니다.

예시:

# 올바른 예:
gql_query = %q(어떤 쿼리 텍스트...)
post_graphql(gql_query, current_user: current_user)
# 또는:
GitlabSchema.execute(gql_query, context: { current_user: current_user })

# 사용 중단: 피할 것
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 '성공적인 응답을 반환합니다' 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)}
    
  • double('query', schema: nil) 대신 GraphqlHelpers#query_double(schema: nil)을 사용하세요.
    예를 들어:

    # 좋음
    let(:query) { query_double(schema: GitlabSchema) }
    
    # 나쁨
    let(:query) { double('Query', schema: GitlabSchema) }
    
  • 허위 긍정 방지:

    post_graphqlcurrent_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 'N+1 쿼리를 피합니다' 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)  # 인증 쿼리로 인한 허위 긍정을 피하기 위해 다른 사용자를 사용합니다
        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%의 리졸버 사양은 Ruby 객체인 인수를 사용합니다.
      GraphQL API를 사용할 때는 문자열과 정수만 사용됩니다. 대부분의 경우 이 방식이 잘 작동합니다.

    2. 리졸버가 시간 범위 인수(TimeFrameArguments)를 사용하는 prepare 프로크를 사용하는 경우,
      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 업그레이드를 위한 다리로 추가되었습니다.
    리졸버를 직접 테스트하는 것은 결국 제거될 것입니다,
    리졸버/변경 작업에 대한 단위 테스트 작성은 이미 사용 중단되었습니다

쿼리 흐름 및 GraphQL 인프라에 대한 노트

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

Instrumentation은 실행 중인 쿼리를 감싸는 기능입니다. 이는 Instrumentation 클래스를 사용하는 모듈로 구현됩니다.

예제: Present

module Gitlab
  module Graphql
    module Present
      #... some code above...

      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 documentation.

문서 및 스키마

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

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

독자를 돕기 위해, 우리의 GraphQL API 문서에 새 페이지를 추가해야 합니다. 안내를 원하시면 GraphQL API 페이지를 참조하세요.

변경 로그 항목 포함

모든 클라이언트 대면 변경 사항은 반드시 변경 로그 항목을 포함해야 합니다.

지연

성능 관리를 위한 GraphQL의 고유한 중요한 기술 중 하나는 lazy 값을 사용하는 것입니다. Lazy 값은 결과의 약속을 나타내며, 나중에 실행할 수 있도록 하여 쿼리 트리의 다양한 부분에서 쿼리를 배치할 수 있습니다. 우리 코드에서 lazy 값의 주요 예는 GraphQL BatchLoader입니다.

lazy 값을 직접 관리하려면 Gitlab::Graphql::Lazy를 읽고, 특히 Gitlab::Graphql::Laziness를 참고하세요. 여기에는 필요한 경우 생성 및 지연 제거의 기본 작업을 구현하는 데 도움이 되는 #force#delay가 포함되어 있습니다.

강제로 lazy 값을 처리하지 않으려면 Gitlab::Graphql::Lazy.with_value를 사용하세요.