GraphQL APIスタイルガイド

このドキュメントでは、GitLabGraphQL APIのスタイルガイドの概要を説明します。

GitLabがGraphQLを実装する方法

私たちはRobert Mosolgoによって書かれたGraphQL Ruby gemを使っています。加えて、私たちはGraphQL Proを購読しています。詳細はGraphQL Proのサブスクリプションをご覧ください。

すべてのGraphQLクエリは単一のエンドポイント(app/controllers/graphql_controller.rb#execute)に向けられます。このエンドポイントはAPIエンドポイントとして/api/graphqlで公開されています。

ディープダイブ

2019年3月、Nick ThomasがGitLabGraphQL APIのDeep Dive(GitLabチームメンバー限定:https://gitlab.com/gitlab-org/create-stage/issues/1 )を開催し、将来コードベースのこの部分で働く可能性のある人とドメイン固有の知識を共有しました。録画はYouTubeで、スライドはGoogleスライドと PDFでご覧いただけます。このディープダイブで取り上げた内容はすべてGitLab 11.9の時点でのものであり、そのリリース後に具体的な詳細が変更されているかもしれませんが、それでも良い入門書として役立つはずです。

GraphiQL

GraphiQLはインタラクティブなGraphQL APIエクスプローラで、既存のクエリで遊ぶことができます。どのGitLab環境でも、https://<your-gitlab-site.com>/-/graphql-explorer 。例えば、GitLab.com用のものです。

GraphQL による変更を含むマージリクエストのレビュー

GraphQLフレームワークには注意すべき特有のゴチャがいくつかあり、それらを確実に満たすためにはドメインの専門知識が必要です。

GraphQLファイルを変更したりエンドポイントを追加したりするマージリクエストのレビューを依頼された場合は、GraphQLレビューガイドをご覧ください。

GraphQL ログの読み取り

GraphQL リクエストのログを検査し、GraphQL クエリのパフォーマンスを監視する方法については、「Reading GraphQL logs」ガイドを参照してください。

認証

認証はGraphqlController 、現在はRailsアプリケーションと同じ認証を使っています。そのため、セッションを共有することができます。

クエリ文字列にprivate_token 、またはHTTP_PRIVATE_TOKEN ヘッダを追加することも可能です。

限界

GraphQL APIにはいくつかの制限が適用され、そのうちのいくつかは開発者がオーバーライドすることができます。

最大ページサイズ

デフォルトでは、接続は1ページあたりapp/graphql/gitlab_schema.rb で定義された最大レコード数までしか返すことができません。

開発者は、接続を定義する際にカスタムの最大ページサイズを指定することができます。

最大複雑度

複雑さについては、クライアント向けAPIのページで説明しています。

フィールドのデフォルトは、クエリの複雑さのスコアに1 を追加するようになっていますが、開発者はフィールドを定義する際にカスタムの複雑さを指定することができます。

クエリのcomplexスコアは、それ自体でクエリすることができます。

リクエストタイムアウト

リクエストは30秒でタイムアウトします。

最大フィールドコール数の制限

N+1クエリ問題になり最適解がないため、複数の親ノードで特定のフィールドを評価しないようにしたい場合があります。これは最後の手段であり、ルックアヘッドによる関連付けの事前ロードや バッチ処理の使用などの方法が検討された場合にのみ使用されます。

使用例:

# This usage is expected.
query {
  project {
    environments
  }
}

# This usage is NOT expected.
# It results in N+1 query problem. EnvironmentsResolver can't use GraphQL batch loader in favor of GraphQL pagination.
query {
  projects {
    nodes {
      environments
    }
  }
}

これを防ぐには、フィールドにGitlab::Graphql::Limit::FieldCallCount 拡張子を使用します:

# This allows maximum 1 call to the `environments` field. If the field is evaluated on more than one node,
# it raises an error.
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: 'Environments of the project. This field can only be resolved for one project in any single request.'

破壊的な変更

GitLab GraphQL APIはバージョンレスであるため、開発者は私たちのDeprecation and Removalプロセスを熟知しておく必要があります。

変更点は以下の通りです:

  • フィールド、引数、列挙値、変異の削除や名前の変更。
  • フィールド、引数、列挙値の型の変更。
  • リゾルバにおけるフィールドの複雑度や複雑度乗数を上げること。
  • Nullableフィールドで説明したように、フィールドをNULL_不可_(null: false)からNULL可能(null: true)に変更します。
  • 引数を省略可能 (required: false) から必須 (required: true) に変更。
  • 接続の最大ページ・サイズの変更。
  • クエリの複雑さと深さに関するグローバル制限の引き下げ。
  • 以前は許可されていた制限にクエリがヒットする可能性があるその他のもの。

アイテムを非推奨にする方法については、スキーマ・アイテムの非推奨セクションを参照してください。

変更免除の廃止

alphaとマークされたスキーマ項目は非推奨のプロセスから除外され、いつでも予告なしに削除または変更することができます。

グローバルID

GitLab GraphQL APIはグローバルID(すなわち"gid://gitlab/MyObject/123" )を使用し、決してデータベースの主キーIDを使用しません。

グローバルIDは、クライアント側ライブラリでキャッシュやフェッチのために使用される規約です。

こちらも参照してください。

私たちは、値がGlobalID である場合に、入出力引数の型として使用すべきカスタムスカラー型 (Types::GlobalIDType) を持っています。ID の代わりにこの型を使用する利点は以下の通りです:

  • 値がGlobalID
  • ユーザーコードに渡す前に、GlobalID にパースします。
  • オブジェクトの型(例えばGlobalIDType[Project])をパラメータ化することができ、より優れたバリデーションとセキュリティを提供します。

すべての新しい引数や結果の型にこの型を使うことを検討してください。より広い範囲のオブジェクトを受け入れたい場合は、この型を懸念やスーパータイプでパラメータ化することが完全に可能であることを覚えておいてください(GlobalIDType[Issuable] vsGlobalIDType[Issue] のように)。

Rubyではコード・ファーストのスキーマを使い、すべてのものがどの型であるかを宣言します。

例えば、app/graphql/types/issue_type.rb

graphql_name 'Issue'

field :iid, GraphQL::Types::ID, null: true
field :title, GraphQL::Types::String, null: true

# we also have a method here that we've defined, that extends `field`
markdown_field :title_html, null: true
field :description, GraphQL::Types::String, null: true
markdown_field :description_html, null: true

それぞれの型に名前を付けます(この場合はIssue )。

iidtitledescription は_スカラー_GraphQL 型です。iidGraphQL::Types::ID、一意の ID を意味する特殊な文字列型です。titledescription は通常のGraphQL::Types::String 型です。

古いスカラー型GraphQL:ID,GraphQL::INT_TYPE,GraphQL::STRING_TYPE,GraphQL:BOOLEAN_TYPE,GraphQL::FLOAT_TYPE は使用できなくなりました。GraphQL::Types::ID,GraphQL::Types::Int,GraphQL::Types::String,GraphQL::Types::Boolean,GraphQL::Types::Floatを使用してください。

GraphQL API を通じてモデルを公開する場合は、app/graphql/types で新しい型を作成することによって行います。スカラーデータ型に対してカスタム GraphQL データ型を宣言することもできます(たとえば、TimeType )。

型のプロパティを公開する場合は、定義内のロジックをできる限り最小限に保つようにしてください。代わりに、あらゆるロジックをプレゼンターに移動することを検討してください:

class Types::MergeRequestType < BaseObject
  present_using MergeRequestPresenter

  name 'MergeRequest'
end

既存のプレゼンターを使用することもできますが、GraphQL専用の新しいプレゼンターを作成することも可能です。

プレゼンターは、フィールドによって解決されたオブジェクトとコンテキストを使用して初期化されます。

ヌル可能なフィールド

GraphQLでは、フィールドを「nullable」または「non-nullable」にすることができます。前者は、指定された型の値の代わりにnull が返される可能性があることを意味します。一般的に、以下の理由から、非 nullable フィールドよりも nullable フィールドを使用することをお勧めします:

  • データが必須から非必須に切り替わったり、また元に戻ったりするのはよくあることです。
  • フィールドがオプションになる見込みがない場合でも、クエリ時に利用できないことがあります。
    • 例えば、ブロブのcontent 、Gitalyから検索する必要があるかもしれません。
    • content がNULL可能な場合、クエリ全体を失敗させるのではなく、部分的なレスポンスを返すことができます。
  • null可能でないフィールドからnull可能なフィールドへの変更はバージョンレススキーマでは困難です。

nullableでないフィールドは、フィールドが必須で、将来オプションになる可能性が非常に低く、計算が簡単な場合にのみ使用すべきです。例えば、id フィールドです。

非NULL可能なGraphQLスキーマフィールドは、オブジェクト型の後に感嘆符(bang)が続くものです! 。以下はgitlab_schema.graphql ファイルの例です:

  id: ProjectID!

以下は、非NULL可能なGraphQL配列の例です:


  errors: [String!]!

さらに読む

グローバルIDの公開

GitLabのグローバルIDの使い方に沿って、データベースの主キーIDを公開するときは常にグローバルIDに変換してください。

id という名前のフィールドはすべて、自動的にオブジェクトのグローバル 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

接続タイプ

note
実装の詳細については、ページネーションの実装を参照してください。

GraphQLはカーソルベースのページネーションを使ってアイテムのコレクションを公開します。これはクライアントに多くの柔軟性を提供すると同時に、バックエンドが異なるページネーションモデルを使用することを可能にします。

リソースのコレクションを公開するには、接続タイプを使用します。これはデフォルトのページネーションフィールドで配列をラップします。たとえば project-pipelines に対するクエリは次のようになります:

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は接続の1ページあたりapp/graphql/gitlab_schema.rb で定義された最大数のレコードを返します。また、クライアントによって制限引数(first: またはlast:)が提供されない場合、これは1ページあたり返されるレコードのデフォルト数でもあります。

max_page_size 引数を使用して、接続に対して異なるページ・サイズ制限を指定することができます。

max_page_size デフォルトはGraphQL APIのパフォーマンスを維持するように設定されているため、max_page_size を上げるよりも、フロントエンドのクライアントや製品の要件を変更して、ページごとに大量のレコードを必要としないようにする方がよいでしょう。

使用例:

field :tags,
  Types::ContainerRepositoryTagType.connection_type,
  null: true,
  description: 'Tags of the container repository',
  max_page_size: 20

フィールドの複雑さ

GitLab GraphQL APIは、複雑すぎるクエリの実行を制限するために_複雑さ_スコアを使用します。複雑さについては、このトピックに関する私たちのクライアントドキュメントで説明されています。

複雑さの上限はapp/graphql/gitlab_schema.rb で定義されています。

デフォルトでは、フィールドはクエリの複雑さスコアに1 。これは、フィールドにカスタム値complexity を指定することでオーバーライドできます。

開発者は、データを返すためにサーバでより多くの_作業が_発生するフィールドに対して、より高い複雑度を指定する必要があります。例えば、ほとんどの場合、idtitle のように、ほとんど_何もしなくても_データを返すことができるフィールドには、0 の複雑度を指定することができます。

calls_gitaly

解決時にGitalyコールを実行する可能性があるフィールドは、定義時にcalls_gitaly: truefield に渡すことで、そのようにマークする_必要が_あります。

使用例:

field :blob, type: Types::Snippets::BlobType,
      description: 'Snippet blob',
      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 を継承し、いくつかのヘルパー・メソッドを含んでいます:

class MergeRequestPermissionsType < BasePermissionType
  graphql_name 'MergeRequestPermissions'

  present_using MergeRequestPresenter

  abilities :admin_merge_request, :update_merge_request, :create_note

  ability_field :resolve_note,
                description: 'Indicates the user can resolve discussions on the merge request.'
  permission_field :push_to_source_branch, method: :can_push_to_source_branch?
end
  • permission_field:graphql-rubyfield メソッドと同じ動作をしますが、デフォルトの説明とタイプを設定し、それらを NULL 以外にすることができます。これらのオプションは、引数として追加することでオーバーライドすることができます。
  • ability_field:ポリシーで定義された能力を公開します。これはpermission_field と同じように動作し、同じ引数をオーバーライドできます。
  • abilities:ポリシーで定義された複数の能力を一度に公開できます。これらのフィールドはすべて、デフォルトの説明を持つヌルではないブール値でなければなりません。

フィーチャーフラグ

GraphQLでは、機能フラグを実装して切り替えることができます:

  • フィールドの返り値。
  • 引数や突然変異の動作。

これは、好みや状況に応じて、リゾルバや型、あるいはモデルメソッドで行うことができます。

note
機能フラグの後ろにある間は、項目をアルファとしてマークすることも推奨します。これは、公開GraphQL APIのコンシューマーに、そのフィールドがまだ使用されることを意図していないことを知らせます。また、Alpha項目は非推奨にしなくてもいつでも変更または削除できます。フラグが削除されたら、Alphaプロパティを削除してスキーマ項目を「リリース」し、公開します。

機能フラグ項目の説明

機能フラグを使ってスキーマ項目の値や動作を切り替える場合、description

  • 機能フラグによって値や動作が切り替えられることを明記してください。
  • 機能フラグに名前を付けます。
  • 機能フラグが無効(またはより適切であれば有効)であるとき、フィールドが何を返すか、または動作が何であるかを記述します。

機能フラグの使用例

機能フラグフィールド

フィールドの値は、機能フラグの状態に基づいてトグルされます。一般的な使用法は、機能フラグが無効な場合にnull を返すことです:

field :foo, GraphQL::Types::String, null: true,
      alpha: { milestone: '10.0' },
      description: 'Some test field. Returns `null`' \
                   'if `my_feature_flag` feature flag is disabled.'

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: 'Some test argument. Is ignored if ' \
                      '`my_feature_flag` feature flag is disabled.'

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

機能フラグによる突然変異

機能フラグの状態によって実行できない変異は、回復不可能な変異エラーとして処理されます。エラーはトップ・レベルで返されます:

description 'Mutates an object. Does not mutate the object if ' \
            '`my_feature_flag` feature flag is disabled.'

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

  raise_resource_not_available_error! '`my_feature_flag` feature flag is disabled.' \
    if Feature.disabled?(:my_feature_flag, object)
  # ...
end

非推奨スキーマ項目

GitLab GraphQL APIはバージョンレスです。つまり、変更のたびに古いバージョンのAPIとの後方互換性をメンテナーします。

フィールド、引数、列挙値、または変異を削除するのではなく、代わりに_非推奨に_する必要があります。

スキーマの非推奨部分は、GitLabの非推奨プロセスに従って将来のリリースで削除することができます。

GraphQLでスキーマ項目を非推奨にするには:

  1. 項目の非推奨イシューを作成します。
  2. スキーマで非推奨として項目をマークします。

こちらも参照してください。

非推奨イシューの作成

すべての GraphQL の非推奨事項には、その非推奨と削除を追跡するためにDeprecations issue テンプレート](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Deprecations) を使用して[非推奨イシューを作成する必要があります。

これら2つのラベルを非推奨イシューに適用します:

  • ~GraphQL
  • ~deprecation

非推奨とマーク

フィールド、引数、列挙値、および突然変異は、deprecated プロパティを使用して非推奨となります。プロパティの値はHash のものです:

  • reason - 非推奨の理由。
  • milestone - そのフィールドが非推奨となったマイルストーン。

使用例:

field :token, GraphQL::Types::String, null: true,
      deprecated: { reason: 'Login via token has been removed', milestone: '10.0' },
      description: 'Token for login.'

例: 非推奨になるもののオリジナルのdescription はメンテナーとすべきであり、非推奨に言及するために更新されるべきでは_ありません_。その代わりに、reasondescriptionに追加します。

非推奨の理由スタイルガイド

非推奨の理由がフィールド、引数、列挙値が置き換えられることによるものである場合、reason 置き換えを示さなければなりません reason。例えば、reason 以下は reason置換されたフィールドに対するものです:

Use `otherFieldName`

例:

field :designs, ::Types::DesignManagement::DesignCollectionType, null: true,
      deprecated: { reason: 'Use `designCollection`', milestone: '10.0' },
      description: 'The designs associated with this issue.',
module Types
  class TodoStateEnum < BaseEnum
    value 'pending', deprecated: { reason: 'Use PENDING', milestone: '10.0' }
    value 'done', deprecated: { reason: 'Use DONE', milestone: '10.0' }
    value 'PENDING', value: 'pending'
    value 'DONE', value: 'done'
  end
end

非推奨とされるフィールド、引数、列挙型の値が置き換えられない場合、説明的な非推奨reason

グローバルIDの非推奨

rails/globalid gemを使用してグローバルIDを生成・解析しているため、グローバルIDはモデル名と連動しています。モデル名を変更すると、グローバルIDも変更されます。

グローバルIDがスキーマのどこかで_引数の_型として使われている場合、グローバルIDの変更は通常、破壊的な変更を構成します。

古いグローバルID引数を使用するクライアントを引き続きサポートするために、Gitlab::GlobalId::Deprecations に非推奨を追加します。

note
グローバルIDがフィールドとしてのみ公開される場合、非推奨とする必要はありません。グローバルIDがフィールドで表現される方法への変更は、後方互換性があると考えます。私たちは、クライアントがこれらの値を解析しないことを期待しています。これらは不透明なトークンとして扱われることを意図しており、その中の構造は付随的なものであり、依存するものではありません。

シナリオの例

このシナリオ例は、このマージリクエストに基づいています。

PrometheusService という名前のモデルをIntegrations::Prometheus という名前に変更します。古いモデル名は、変異の引数として使用されるグローバル ID タイプを作成するために使用されます:

# Mutations::UpdatePrometheus:

argument :id, Types::GlobalIDType[::PrometheusService],
              required: true,
              description: "The ID of the integration to mutate."

クライアントは、input.id の引数として、PrometheusServiceID と名付けられた"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: "The ID of the integration to mutate."

この場合、id の引数を"gid://gitlab/PrometheusService/1" として渡したり、クエリシグネチャで引数の型をPrometheusServiceID として指定したりするクライアントをAPIが拒否するようになるため、変異が変更されることになります。

クライアントが変更されることなく変異とやりとりできるようにするには、Gitlab::GlobalId::DeprecationsDEPRECATIONS 定数を編集し、新しい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
note
古い型 ( この例ではPrometheusServiceID ) を使用したクエリは API では有効で実行可能ですが、 バリデータツールでは無効とみなされます。無効とみなされるのは、@deprecated ディレクティブの外部で特注のメソッドを使用して非推奨としているため、 バリデータがそのサポートに気づいていないからです。

ドキュメントでは、古いグローバル ID スタイルは非推奨であると述べています。

スキーマ項目をアルファとしてマーク

GraphQLスキーマ項目(フィールド、引数、列挙値、および変異)をアルファとしてマークすることができます。

Alphaとしてマークされた項目は非推奨のプロセスから除外され、予告なしにいつでも削除することができます。アイテムが変更される可能性があり、公開の準備が整っていない場合は、Alphaとしてマークしてください。

note
新しいアイテムにのみ Alpha としてマークしてください。既存のアイテムはすでに公開されているので、決して Alpha としてマークしないでください。

スキーマ項目を Alpha としてマークするには、alpha: キーワードを使用します。Alpha アイテムを導入したmilestone: を指定する必要があります。

使用例:

field :token, GraphQL::Types::String, null: true,
      alpha: { milestone: '10.0' },
      description: 'Token for login.'

同様に、app/graphql/types/mutation_type.rb で変異がマウントされている場所を更新することで、変異全体をアルファとしてマークすることもできます:

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

Alpha GraphQLアイテムは、GraphQLの非推奨を活用するGitLabのカスタム機能です。Alphaアイテムは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 'State of a traffic light'

    value 'RED', description: 'Drivers must stop.'
    value 'YELLOW', description: 'Drivers must stop when it is safe to.'
    value 'GREEN', description: 'Drivers can start or keep driving.'
  end
end

enumがRubyのクラスプロパティで大文字でない文字列のために使われる場合、大文字の値を適応するvalue: オプションを提供することができます。

以下の例をご覧ください:

  • OPENED の GraphQL 入力は'opened' に変換されます。
  • 'opened' の Ruby 値は、GraphQL レスポンスでは"OPENED" に変換されます。
module Types
  class EpicStateEnum < BaseEnum
    graphql_name 'EpicState'
    description 'State of a GitLab epic'

    value 'OPENED', value: 'opened', description: 'An open Epic.'
    value 'CLOSED', value: 'closed', description: 'A closed Epic.'
  end
end

列挙型の値は、deprecated キーワードを使用して非推奨にすることができます。

RailsのenumからGraphQLのenumを動的に定義する方法

GraphQL enumがRails enumによってバックアップされている場合は、Rails enumを使用してGraphQL enumの値を動的に定義することを検討してください。そうすることで、GraphQL enumの値がRails enumの定義にバインドされるため、Rails enumに値が追加された場合、GraphQL enumにその変更が自動的に反映されます。

使用例:

module Types
  class IssuableSeverityEnum < BaseEnum
    graphql_name 'IssuableSeverity'
    description 'Incident severity'

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

JSON

GraphQL によって返されるデータがJSON として格納されている場合、可能な限り GraphQL 型を使用し続ける必要があります。返される JSON データが_本当に_構造化されていない場合を除き、GraphQL::Types::JSON 型の使用は避けてください。

JSONデータの構造はさまざまですが、既知の可能な構造のセットの1つである場合は、ユニオンを使用します。この目的でユニオンを使用する例として、!30129があります。

フィールド名は、必要に応じてhash_key: キーワードを使用してハッシュデータのキーにマッピングできます。

例えば、以下のJSONデータがあるとします:

{
  "title": "My chart",
  "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: 'Title of the chart.'
    field :data, [Types::ChartDatumType], null: true, description: 'Data of the chart.'
  end
end

module Types
  class ChartDatumType < BaseObject
    field :x, GraphQL::Types::Int, null: true, description: 'X-axis value of the chart datum.'
    field :y, GraphQL::Types::Int, null: true, description: 'Y-axis value of the chart datum.'
  end
end

説明

すべてのフィールドと引数には説明が必要です

フィールドや引数の説明は、description: キーワードを使用して指定します。例えば

field :id, GraphQL::Types::ID, description: 'ID of the issue.'
field :confidential, GraphQL::Types::Boolean, description: 'Indicates the issue is confidential.'
field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed.'

でフィールドや引数の説明を見ることができます:

記述スタイルガイド

言語と句読点

フィールドや引数を記述するには、可能な限り{x} of the {y} を使用してください。{x} は記述する項目、{y} はそれが適用されるリソースです。例えば

ID of the issue.
Author of the epics.

並べ替えや検索を行う引数については、適切な動詞で始めてください。指定された値を示すには、the giventhe specified の代わりにthis を使うと簡潔です。例えば

Sort issues by this criteria.

一貫性と簡潔性のため、TheA で説明を始めないでください。

すべての説明の終わりはピリオド (.) で終わらせてください。

ブーリアン

真偽値フィールド(GraphQL::Types::Boolean)の場合は、それが何をするのかを表す動詞から始めます。例えば

Indicates the issue is confidential.

必要に応じて、デフォルトを指定します。例えば

Sets the issue to confidential. Default is false.

Types::TimeType フィールドの説明

Types::TimeType GraphQL フィールドには、timestamp という単語を含めます。これにより読み手は、プロパティの形式が単なるDate ではなくTime であることを知ることができます。

使用例:

field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed.'

copy_field_description ヘルパー

2つの記述が常に同じであることを保証したいことがあります。たとえば、同じプロパティを表す型フィールドの記述と、 変異の引数の記述を同じにしたい場合などです。

説明を指定する代わりに、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: 'A taxonomic genus.'
      see: { 'Wikipedia page on genera' => 'https://wikipedia.org/wiki/Genus' }

これはドキュメントで次のようにレンダリングされます:

A taxonomic genus. See: [Wikipedia page on genera](https://wikipedia.org/wiki/Genus)

複数のドキュメント参照を指定できます。このプロパティの構文はHashMap で、キーはテキスト記述で、値は URL です。

購読層バッジ

フィールドまたは引数が他のフィールドよりも高い購読ティアで利用可能な場合、ティアバッジをインラインに追加します。

使用例:

description: '**(ULTIMATE ALL)** Full path of a custom template.'

作成者

参照してください:GraphQL Authorizationを参照してください。

リゾルバ

app/graphql/resolvers ディレクトリに格納されている_リゾルバを_使用して、 アプリケーションがレスポンスを提供する方法を定義します。リゾルバは、問題のオブジェクトを取得するための実際の実装ロジックを提供します。

フィールドに表示するオブジェクトを見つけるには、app/graphql/resolvers にリゾルバを追加します。

リゾルバでは、変異と同じように引数を定義することができます。変異の引数のセクションを参照してください。

実行するクエリの量を制限するために、BatchLoaderを使用することができます。

リゾルバの記述

リゾルバのコードは、ファインダーやサービスの周りにある、薄い宣言的なラッパーであることを目指すべきです。引数のリストを繰り返すこともできますし、引数を抽出して 関数にすることもできます。ほとんどの場合、継承よりもコンポジションが優先されます。リゾルバをコントローラのように扱う: リゾルバは、他のアプリケーションを抽象化する DSLであるべきです。

使用例:

class PostResolver < BaseResolver
  type Post.connection_type, null: true
  authorize :read_blog
  description 'Blog posts, optionally filtered by name'

  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

同じリゾルバクラスを、同じオブジェクトが公開される 2 つのフィールドなど、異なる 2 つの場所で使用することはできますが、 リゾルバオブジェクトを直接再利用することは決してしてはいけません。リゾルバには複雑なライフサイクルがあり、 作成者、準備者、解決者がフレームワークによってオーケストレーションされ、 それぞれのステージで、バッチ処理の機会を利用するために遅延値を返すことができます。アプリケーションコード内でリゾルバや突然変異をインスタンス化してはいけません。

その代わり、コードの再利用の単位は、アプリケーションの他の部分とほとんど同じです:

  • データを検索するクエリのファインダーです。
  • オペレーションを適用するためのミューテーションにおけるサービス。
  • クエリに特化したローダー(バッチ対応ファインダー)。

突然変異でバッチを使用する理由はありません。変異は直列に実行されるため、バッチ処理の機会はありません。すべての値は要求されるとすぐに熱心に評価されるため、バッチ処理は不要なオーバーヘッドとなります。もしあなたが

  • Mutation を書いている場合は、オブジェクトを直接検索してください。
  • ResolverBaseObject のメソッドを使用する場合は、バッチ処理を許可する必要があります。

エラーハンドリング

リゾルバはエラーを発生させる可能性があり、そのエラーは必要に応じてトップレベルのエラーに変換されます。予測されるエラーはすべてキャッチされ、適切なGraphQLエラーに変換される必要があります(Gitlab::Graphql::Errors を参照)。捕捉されなかったエラーは抑制され、クライアントはメッセージInternal service error を受け取ります。

特殊なケースは権限エラーです。REST API では、ユーザーにアクセス権限がないリソースに対しては404 Not Found を返します。GraphQLにおける同等の動作は、不在または許可されていないすべてのリソースに対してnull を返すことです。クエリ・リゾルバは許可されていないリソースに対してエラーを発生させるべきではありません

その根拠は、クライアントがレコードが存在しないことと、アクセス権限のないレコードが存在することを区別してはならないからです。そうすることはセキュリティの脆弱性であり、 隠しておきたい情報が漏れてしまうからです。

これはauthorize DSLコールで宣言するリゾルバフィールドの作成者によって正しく処理されます。しかし、もっとカスタムなことをする必要がある場合は、フィールドを解決する際にcurrent_user がアクセスできないオブジェクトに遭遇した場合、フィールド全体をnull に解決しなければならないことを覚えておいてください。

リゾルバの派生

(BaseResolver.single およびBaseResolver.last を含む)

いくつかの使用例では、他のリゾルバからリゾルバを派生させることができます。主な使用例としては、すべての項目を見つけるリゾルバと、 特定の項目を見つけるリゾルバがあります。このために、便利なメソッドを用意しています:

  • BaseResolver.singleこれは、最初の項目を選択する新しいリゾルバを構築します。
  • BaseResolver.lastこれは最後の項目を選択するリゾルバを構築します。

正しい単数型はコレクション型から推測されるので、ここでtype を定義する必要はありません。

これらのメソッドを使用する前に、以下のどちらかの方が簡単かどうかを検討してください:

  • 独自の引数を定義する別のリゾルバを書くこと。
  • クエリを抽象化する心配事を書いてください。

BaseResolver.single を自由に使いすぎるのはアンチパターンです。引数が与えられなければ最初のMRを返すだけのProject.mergeRequest フィールドのような、非意味的なフィールドにつながる可能性があります。コレクション・リゾルバからシングル・リゾルバを派生させる場合は常に、より限定的な引数を持たなければなりません。

これを可能にするには、when_single ブロックを使用して単一リゾルバをカスタマイズ when_singleします。when_single すべての when_singleブロックは

  • 少なくとも1つの引数を定義(または再定義)。
  • オプションのフィルタを必須にします。

たとえば、既存のオプションの引数を再定義し、その型を変更して必須にすることで実現できます:

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: 'All jobs.'
field :job, resolver: JobsResolver.single, description: 'A single job.'

正しいResolver#ready?

リゾルバには、フレームワークの一部として、#ready?(**args)#resolve(**args) という2つの公開APIメソッドがあります。#resolveを呼び出すことなく、#ready? を使ってセットアップ、検証、アーリーリターンを行うことができます。

#ready? を使う理由は以下の通りです:

  • 相互に排他的な引数の検証(引数の検証を参照)
  • 結果が得られないことが事前に分かっている場合はRelation.none を返します。
  • インスタンス変数の初期化などのセットアップの実行(ただし、これには遅延的に初期化されたメソッドを考慮します)

Resolver#ready?(**args) の実装は、以下のように(Boolean, early_return_data) を返す必要があります:

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

このため、リゾルバを呼び出すときはいつでも(主にテストにおいて - フレームワークの抽象化として、リゾルバは再利用可能であると考えるべきではなく、ファインダを優先すべきです)、resolve を呼び出す前にready? メソッドを呼び出し、ブーリアンフラグをチェックすることを忘れないでください!例をGraphqlHelpersで見ることができます。

ルックアヘッド

つまり、ルックヘッドを使用してクエリを最適化し、必要だと分かっている関連付けを バッチロードすることができます。N+1 パフォーマンスの問題を回避するために、リゾルバにルックアヘッドサポートを追加することを検討してください。

一般的なルックヘッドの使用例(子フィールドが要求された時に関連付けを事前にロードする)のサポートを有効にするために、LooksAhead を含めることができます。例えば

# Assuming a model `MyThing` with attributes `[child_attribute, other_attribute, nested]`,
# where nested has an attribute named `included_attribute`.
class MyThingResolver < BaseResolver
  include LooksAhead

  # Rather than defining `resolve(**args)`, we implement: `resolve_with_lookahead(**args)`
  def resolve_with_lookahead(**args)
    apply_lookahead(MyThingFinder.new(current_user).execute)
  end

  # We list things that should always be preloaded:
  # For example, if child_attribute is always needed (during authorization
  # perhaps), then we can include it here.
  def unconditional_includes
    [:child_attribute]
  end

  # We list things that should be included if a certain field is selected:
  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を参照してください。

否定引数

否定フィルタは一部のリソースをフィルタリングすることができます(例えば、bug のラベルを持ち、bug2 のラベルが割り当てられていないイシューをすべて検索する)。not 引数は否定引数を渡すのに適した構文です:

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

引数定義の重複を避けるために、これらの引数を再利用可能なモジュール (または引数が入れ子になっている場合はクラス) に置くことができます。あるいは、ヘルパー・リゾルバ・メソッドを追加することもできます。

メタデータ

リゾルバを使う場合、リゾルバはフィールドのメタデータのための SSoTの役割を果たすことができますし、そうすべきです。フィールド名以外のすべてのフィールドオプションは、リゾルバ上で宣言することができます。これらには次のようなものがあります:

  • type (必須 - すべてのリゾルバは型アノテーションを含まなければなりません)
  • extras
  • description
  • Gitalyアノテーション (calls_gitaly!)

使用例:

module Resolvers
  MyResolver < BaseResolver
    type Types::MyType, null: true
    extras [:lookahead]
    description 'Retrieve a single MyType'
    calls_gitaly!
  end
end

親オブジェクトを子 Presenter に渡す例

フィールドを計算するために、子コンテキストで解決されたクエリの親オブジェクトにアクセスする必要があることがあります。通常、親オブジェクトはparent としてResolver クラスでのみ利用可能です。

Presenter クラスで親オブジェクトを見つけるには、以下の手順に従います:

  1. リゾルバのresolve メソッドから GraphQLcontext に親オブジェクトを追加します:

      def resolve(**args)
        context[:parent_object] = parent
      end
    
  2. リゾルバまたはフィールドがparent フィールドコンテキストを必要とすることを宣言します。例えば

      # in ChildType
      field :computed_field, SomeType, null: true,
            method: :my_computing_method,
            extras: [:parent], # Necessary
            description: 'My field description.'
       
      field :resolver_field, resolver: SomeTypeResolver
       
      # In SomeTypeResolver
       
      extras [:parent]
      type SomeType, null: true
      description 'My field description.'
    
  3. Presenter クラスでフィールドのメソッドを宣言し、parent キーワード引数を受け付けるようにします。この引数には親GraphQL コンテキストが含まれるため、parent[:parent_object] またはResolver で使用したキーを使用して親オブジェクトにアクセスする必要があります:

      # in ChildPresenter
      def my_computing_method(parent:)
        # do something with `parent[:parent_object]` here
      end
       
      # In SomeTypeResolver
       
      def resolve(parent:)
        # ...
      end
    

実際の使用例として、 scopedPathscopedUrl を追加した MR を確認してください。IterationPresenter

突然変異

変異は、保存されている値を変更したり、アクションをトリガーしたりするために使用されます。GETリクエストがデータを変更してはならないのと同じように、通常のGraphQLクエリではデータを変更することはできません。しかし、ミューテーションでは変更することができます。

変異の構築

変異はapp/graphql/mutations に格納されます。理想的には、サービスと同じように、変異させるリソースごとにグループ化されます。これらはMutations::BaseMutation を継承する必要があります。変異で定義されたフィールドは、変異の結果として返されます。

変異の粒度の更新

GitLab のサービス指向アーキテクチャでは、ほとんどの変異は例えばUpdateMergeRequestService のように、Create、Delete、Update サービスを呼び出します。Update 変異では、オブジェクトの一面だけを更新したいかもしれません。そのために、たとえばMergeRequest::SetDraft のような_細かい粒度の_変異が必要になります。

細かい変異と粗い変異の両方があってもかまいませんが、細かい変異が多すぎると、メンテナー性、コード理解性、テストにおいて組織的な問題につながる可能性があることに注意してください。変異のたびに新しいクラスが必要になるため、技術的負債を抱えることになります。また、スキーマが非常に大きくなり、ユーザーがスキーマをナビゲートするのが難しくなります。それぞれの新しい変異はテスト(より遅いリクエストのインテグレーションテストを含む)も必要とするので、変異を追加するとテストスイートが遅くなります。

変更を最小限にするために

  • MergeRequest::Update のような既存の変異が利用可能な場合は、それを使用してください。
  • 既存のサービスを粗視化変異として公開。

きめ細かい変異の方が適切な場合:

  • 特定の権限や特殊なロジックを必要とするプロパティの変更。
  • ステートマシンのような遷移の公開 (イシューのロック、MR のマージ、エピックのクローズなど)。
  • ネストしたプロパティの受け入れ(子オブジェクトのプロパティを受け入れます)。
  • 変異のセマンティクスは明確かつ簡潔に表現できます。

さらなる文脈についてはイシュー#233063を参照してください。

命名規則

各変異はgraphql_name を定義する必要があり、これは GraphQL スキーマにおける変異の名前です。

使用例:

class UserUpdateMutation < BaseMutation
  graphql_name 'UserUpdate'
end

graphql-ruby gemの1.13 バージョンでの変更により、型名が正しく生成されるように、graphql_name をクラスの最初の行にする必要があります。Graphql::GraphqlNamePosition copはこれを強制します。詳細はイシュー#27536を参照してください。

私たちの GraphQL 変異名は歴史的に一貫性がありませんが、新しい変異名は'{Resource}{Action}' または'{Resource}{Action}{Attribute}' という規約に従うべきです。

新しいリソースを作成する変異は動詞Create を使用する必要があります。

使用例:

  • CommitCreate

データを更新する変異は

  • 動詞Update
  • Set,Add,Toggle のようなドメイン固有の動詞が適切です。

例:

  • EpicTreeReorder
  • IssueSetWeight
  • IssueUpdate
  • TodoMarkDone

データを削除する変異を使用する必要があります:

  • Destroy ではなく、動詞Delete を使用してください。
  • Remove のようなドメイン固有の動詞の方が適切です。

例:

  • AwardEmojiRemove
  • NoteDelete

突然変異の命名についてアドバイスが必要な場合は、Slack#graphql チャンネルでフィードバックを求めてください。

引数

突然変異の引数はargument を使って定義します。

使用例:

argument :my_arg, GraphQL::Types::String,
         required: true,
         description: "A description of the argument."

無効性

引数はrequired: true としてマークすることができます。null必須引数の値が nullrequired: :nullable 宣言で指定された値でなければならないことを意味します。

使用例:

argument :due_date,
         Types::TimeType,
         required: :nullable,
         description: 'The desired due date for the issue. Due date is removed if null.'

上記の例では、due_date 引数を指定する必要がありますが、GraphQL 仕様とは異なり、値はnull にすることができます。これにより、期日を削除するために新しい変異を作成するのではなく、単一の変異で期日を「設定解除」することができます。

{ due_date: null } # => OK
{ due_date: "2025-01-10" } # => OK
{  } # => invalid (not given)

キーワード

定義された各 GraphQLargument は、キーワード引数として変異の#resolve メソッドに渡されます。

使用例:

def resolve(my_arg:)
  # Perform mutation ...
end

入力タイプ

graphql-ruby は引数を入力型にまとめます。

例えば、mergeRequestSetDraft mutation 、これらの引数を定義しています(継承によるものもあります):

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: "The project the merge request to mutate is in."

argument :iid, GraphQL::Types::String,
         required: true,
         description: "The IID of the merge request to mutate."

argument :draft,
         GraphQL::Types::Boolean,
         required: false,
         description: <<~DESC
           Whether or not to set the merge request as a draft.
         DESC

これらの引数は、私たちが指定した3つの引数とclientMutationId を持つMergeRequestSetDraftInput という入力型を自動的に生成します。

オブジェクト識別子の引数

GitLab ではグローバル ID を使っています。変異の引数ではオブジェクトを識別するためにグローバル ID を使うべきで、データベースの主キー ID を使うべきではありません。

オブジェクトがiid を持つ場合、オブジェクトを識別するための引数にはidではなく、その親のfull_pathgroup_pathiid を組み合わせて使うことをお勧めします。

グローバルIDの廃止も参照してください。

フィールド

最も一般的な状況では、突然変異は2つのフィールドを返します:

  • 変更されるリソース
  • アクションを実行できなかった理由を説明するエラーのリスト。変異が成功した場合、このリストは空になります。

Mutations::BaseMutation から新しい変異を継承することで、errors フィールドが自動的に追加されます。また、clientMutationId フィールドも追加されます。これは、1つのリクエストで複数の変異が実行されたときに、クライアントが1つの変異の結果を識別するために使うことができます。

resolve メソッド

リゾルバを書くのと同様に、突然変異のresolve メソッドは、サービスの周りの薄い宣言的なラッパーであることを目指すべきです。

resolve メソッドは変異の引数をキーワード引数として受け取ります。ここから、リソースを変更するサービスを呼び出すことができます。

resolve メソッドは、errors 配列を含む、変異で定義されたものと同じフィールド名を持つハッシュを返す必要があります。例えば、Mutations::MergeRequests::SetDraftmerge_request フィールドを定義しています:

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

つまり、この突然変異でresolve から返されるハッシュは以下のようになります:

{
  # The merge request modified, this will be wrapped in the type
  # defined on the field
  merge_request: merge_request,
  # An array of strings if the mutation failed after authorization.
  # The `errors_on_object` helper collects `errors.full_messages`
  errors: errors_on_object(merge_request)
}

変異のマウント

変異を利用できるようにするには、graphql/types/mutation_type に格納されている変異型に定義する必要があります。mount_mutation ヘルパーメソッドは、変異の GraphQL-name に基づいてフィールドを定義します:

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

2つのフィールドを持つレスポンスを返す変異doTheThing の例を考えてみましょう:errors: [String]thing: ThingType 。これらの例ではエラーについて考えているので、thing そのものの具体的な性質は関係ありません。

突然変異のレスポンスが取り得る3つの状態は次のとおりです:

成功

ハッピーパスでは、予想されるペイロードとともにエラーが返されるかもしれません。しかし、すべてが成功した場合は、errors は空の配列になるはずです。なぜなら、ユーザーに知らせる必要のある問題がないからです。

{
  data: {
    doTheThing: {
      errors: [] // if successful, this array will generally be empty.
      thing: { .. }
    }
  }
}

失敗 (ユーザーに関係する)

ユーザーに影響するエラーが発生しました。これを_変異エラーと_呼びます。

_create_mutationでは通常、thing

_update_mutation では、thingの現在の真の状態を返します。開発者は、thing インスタンスで#reset を呼び出して、これが確実に起こるようにする必要があるかもしれません。

{
  data: {
    doTheThing: {
      errors: ["you cannot touch the thing"],
      thing: { .. }
    }
  }
}

この例には以下が含まれます:

  • モデルの検証エラー:ユーザーは入力を変更する必要があるかもしれません。
  • 権限エラー: ユーザーはこれを実行できないことを知る必要があり、権限を要求するかサインインする必要があるかもしれません。
  • ユーザのアクションを妨げるアプリケーションの状態の問題(例えば、マージ競合やロックされたリソース)。

理想的には、ユーザーがここまで到達するのを防ぐべきですが、もし到達した場合、何が問題なのかをユーザーに伝える必要があります。例えば、リクエストを再試行するだけでよいかもしれません。

変異データと一緒に回復可能なエラーを返すことも可能です。例えば、ユーザーが10個のファイルをアップロードし、そのうちの3個が失敗し、残りが成功した場合、成功したファイルの情報と一緒に、失敗したファイルのエラーもユーザーに提供することができます。

失敗(ユーザーには無関係)

トップレベルでは、1つ以上の回復不可能なエラーが返されることがあります。これらはユーザーがほとんどコントロールできないもので、主にシステムやプログラミングの問題で、開発者が知っておく必要があるものです。この場合、dataはありません:

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

これは突然変異の際にエラーが発生するためです。私たちの実装では、引数エラーと検証エラーのメッセージはクライアントに返され、それ以外のStandardError インスタンスはすべてキャッチされ、ログに記録され、メッセージが"Internal server error" に設定された状態でクライアントに提示されます。詳細はGraphqlController を参照してください。

これらは以下のようなプログラミングエラーを表します:

  • String の代わりにInt が渡された、または必須引数が存在しないなどの GraphQL 構文エラー。
  • スキーマのエラー(nullableでないフィールドに値を指定できないなど)。
  • システムエラー:Gitストレージの例外やデータベースの使用不能など。

通常の使い方では、ユーザーがこのようなエラーを引き起こすことはできないはずです。このカテゴリのエラーは内部エラーとして扱われるべきで、ユーザーに具体的な詳細を示すべきではありません。

突然変異が失敗したときにはユーザーに知らせる必要がありますが、その理由を伝える必要はありません。なぜなら、ユーザーがその原因を作ったとは考えられず、ユーザーができることは何もないからです。

エラーの分類

私たちが変異を書くとき、エラーの状態がこれら2つのカテゴリのどちらに分類されるかを意識する必要があります(そして、私たちの仮定を検証するために、フロントエンド開発者とこのことについてコミュニケーションをとる必要があります)。これは_ユーザーの_ニーズと_クライアントの_ニーズを区別することを意味します。

ユーザーがエラーについて知る必要がない限り、エラーをキャッチしてはいけません。

ユーザーがエラーについて知る必要がある場合は、フロントエンド開発者とコミュニケーションをとり、エラー情報が適切であり、目的を果たすものであることを確認してください。

フロントエンドの 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をお読みください。

サブスクリプション

サブスクリプションを使用して、クライアントに更新をプッシュします。アクションケーブルの実装を使用して、ウェブソケット経由でメッセージを配信します。

クライアントがサブスクリプションを購読すると、そのクエリを Puma ワーカーにメモリに保存します。そしてサブスクリプションがトリガーされると、Puma ワーカーが保存された GraphQL クエリを実行し、結果をクライアントにプッシュします。

note
サブスクリプションにはアクションケーブルクライアントが必要なため、GraphiQL を使用してサブスクリプションをテストすることはできません。

サブスクリプションの構築

Types::SubscriptionType の下のすべてのフィールドはクライアントがサブスクライブできるサブスクリプションです。これらのフィールドはSubscriptions::BaseSubscription の子孫であり、app/graphql/subscriptions の下に格納されるサブスクリプションクラスを必要とします。

サブスクライブに必要な引数と返されるフィールドはサブスクリプションクラスで定義されます。サブスクライブに必要な引数と返されるフィールドはサブスクリプションクラスで 定義されます。複数のフィールドが同じ引数を持ち、同じフィールドを返す場合、 同じサブスクリプションクラスを共有することができます。

このクラスは最初のサブスクリプションリクエストとその後の更新の間に実行されます。これについてはGraphQL Rubyガイドを参照してください。

作成者

最初のサブスクリプションとその後の更新が認証されるように、サブスクリプションクラスの#authorized? メソッドを実装する必要があります。

ユーザーが認証されていない場合、unauthorized! ヘルパーを呼び出し、実行を停止してユーザーの購読を解除する必要があります。false を返すとレスポンスが再編集されますが、更新が行われているという情報が漏れます。この情報漏えいはGraphQL gemのバグによるものです。

サブスクリプションのトリガー

サブスクリプションをトリガーするために、GraphqlTriggers モジュールの下でメソッドを定義します。異なる引数やオブジェクトでサブスクリプションをトリガーしないように、GitlabSchema.subscriptions.trigger をアプリケーションコードで直接呼び出さないでください。

ページネーションの実装

詳細については、GraphQL paginationを参照してください。

引数の検証

単一引数の検証には、通常通りprepare オプション を使用します。

ミューテーションやリゾルバがいくつかのオプション引数を受け付けることがありますが、 そのオプション引数のうち少なくともひとつが指定されていることを検証したい場合があります。このような場合は、#ready? 変異やリゾルバのメソッドを使用して検証を #ready?行うことを検討しましょう。#ready? この #ready?メソッドは、#resolve メソッドで何らかの処理が行われる前にコールされます。

使用例:

def ready?(**args)
  if args.values_at(:body, :position).compact.blank?
    raise Gitlab::Graphql::Errors::ArgumentError,
          'body or position arguments are required'
  end

  # Always remember to call `#super`
  super
end

将来、このRFCがマージされれば、OneOf Input Objects

GitLab カスタムスカラー

Types::TimeType

Types::TimeType は、RubyTimeDateTime オブジェクトを扱うすべてのフィールドや引数の型として使わなければなりません。

この型は

  • RubyのTimeDateTime オブジェクトを標準化されたISO-8601形式の文字列に変換し、GraphQLフィールドの型として使用します。
  • GraphQLの引数の型として使用する場合に、ISO-8601形式の時間文字列をRubyのTime オブジェクトに変換します。

これにより、私たちのGraphQL APIは、時刻を提示し、時刻入力を処理する標準化された方法を持つことができます。

使用例:

field :created_at, Types::TimeType, null: true, description: 'Timestamp of when the issue was created.'

テスト

変異とリゾルバのテストでは、テストの単位はリゾルバへの呼び出しではなく、完全なGraphQLリクエストと考えます。これにより、フレームワークとの緊密な結合を避けることができます。

そうすべきです:

  • リゾルバや変異のためのユニット仕様よりも、リクエスト仕様(完全なAPIエンドポイントを使用するか、GitlabSchema.execute )を優先してください。
  • GraphqlHelpers#resolveGraphqlHelpers#resolve_fieldよりもGraphqlHelpers#execute_queryGraphqlHelpers#run_with_clean_state を優先してください。

使用例:

# Good:
gql_query = %q(some query text...)
post_graphql(gql_query, current_user: current_user)
# or:
GitlabSchema.execute(gql_query, context: { current_user: current_user })

# Deprecated: avoid
resolve(described_class, obj: project, ctx: { current_user: current_user })

ユニットテストの書き方(非推奨)

caution
同じことが完全な GraphQL リクエストでテストできるのであれば、ユニットテストを書くのは避けましょう。

ユニットテストを作成する前に、以下の例をレビューしてください:

インテグレーションテストの書き方

インテグレーションテストは、GraphQL クエリまたは変異のスタック全体をチェックし、spec/requests/api/graphql に保存されます。

スピードを上げるには、GitlabSchema.execute を直接呼び出すか、テスト対象の型のみを含む小さなテストスキーマを利用することを検討してください。

しかし、データが返されるかどうかをチェックするフルリクエストのインテグレーションテストでは、以下の追加項目を検証します:

  • 変異がスキーマで実際にクエリ可能であること(MutationType にマウントされていること)。
  • リゾルバや変異が返すデータはフィールドの戻り値の型と正しく一致し、エラーなしで解決されます。
  • 引数は入力時に正しく強制され、フィールドは出力時に正しくシリアライズされます。

インテグレーションテストでは、フルスタックを呼び出すため、以下の項目も検証できます:

  • 引数またはスカラーのprepare が正しく適用されること。
  • リゾルバまたは突然変異の#ready? メソッド の論理が正しく適用されます。
  • 引数のdefault_value は正しく適用されます。
  • オブジェクトは正常に解決され、N+1の問題はありません。

クエリを追加する際には、a working graphql query that returns dataa working graphql query that returns no data の共有サンプルを使用して、クエリが有効な結果をレンダリングするかどうかをテストできます。

GraphqlHelpers#all_graphql_fields_for ヘルパーを使用すると、使用可能なすべてのフィールドを含むクエリを作成できます。これにより、クエリで使用できるすべてのフィールドをレンダリングするテストを追加するのがより簡単になります。

ページ分割やソートをサポートしているクエリにフィールドを追加する場合は、 詳細はTestingを参照ください。

GraphQL 変異リクエストをテストするために、GraphqlHelpers は 2 つのヘルパーを提供しています。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)
       
     # checks that it is a hash containing { id => global_id_of(issue) }
     expect(graphql_data_at(:project, :issues, :nodes))
       .to contain_exactly(a_graphql_entity_for(issue))
       
     # Additional fields can be passed, either as names of methods, or with values
     expect(graphql_data_at(:project, :issues, :nodes))
       .to contain_exactly(a_graphql_entity_for(issue, :iid, :title, created_at: some_time))
    
  • GraphqlHelpers#empty_schema を使って空のスキーマを作成してください。例えば

     # good
     let(:schema) { empty_schema }
       
     # bad
     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) を使用してください:

     # good
     let(:query) { query_double(schema: GitlabSchema) }
       
     # bad
     let(:query) { double('Query', schema: GitlabSchema) }
    
  • 偽陽性を避ける

    post_graphqlcurrent_user: 引数でユーザーを認証すると、最初のリクエストで、同じユーザーに対するその後のリクエストよりも多くのクエリが生成されます。QueryRecorderを使って N+1 クエリをテストする場合は、リクエストごとに異なるユーザーを使ってください。

    以下の例は、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)  # use a different user to avoid a false positive from authentication queries
         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 に格納されるべきです。

  • GraphQL の型には循環的な依存関係が存在する可能性があります。Typeにリゾルバを使用してフィールドを追加すると、別のTypeで “Can’t determine the return type “ エラーが発生します。

    特に、これはconnection_type で発生する可能性があります。通常、リゾルバでは以下のように使用します:

     type Types::IssueType.connection_type, null: true
    

    しかし、これでは定義が循環してしまい、次のようなエラーが発生します:

     NameError: uninitialized constant Resolvers::GroupIssuesResolver
       
     or
       
     GraphQL::Pagination::Connections::ImplementationMissingError
    

    というようなエラーが発生する可能性があります。

    この問題を解決するには、接続タイプをカプセル化した新しいファイルを作成し、それを二重引用符で囲んで参照する必要があります。これにより、遅延解決と適切な接続タイプが得られます。例えば

    app/graphql/resolvers/base_issues_resolver.rbは元々以下の行を含んでいました。

     type Types::IssueType.connection_type, null: true
    

    このファイルの仕様をローカルで実行すると、NameError: uninitialized constant Resolvers::GroupIssuesResolver エラーが発生しました。

    修正方法は、app/graphql/types/issue_connection.rbという新しいファイルを作成することでした:

     Types::IssueConnection = Types::IssueType.connection_type
    

    そして、app/graphql/resolvers/base_issues_resolver.rbでは、以下の行を使用します。

     type "Types::IssueConnection", null: true
    

    このスタイルを使用するのは、仕様に失敗した場合のみです。通常、このパターンは使用すべきではありません。このイシューは、2.x にアップグレードした後に消えるはずです。

  • クラスが正しくロードされないために spec が失敗するインスタンスがあります。これは循環依存性の問題と関連しており、ある Type でリゾルバを使用してフィールドを追加すると、別の Type で “Can’t determine the return type “ エラーが発生します。

    残念ながら、生成されるエラーは、何が問題なのかを示していません。例えば、ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rbの Rspec.describe から引用符を外してください。そして、rspec ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb を実行してください。

    これは期待通りのエラーを生成します。例えば

     1) Resolvers::ComplianceManagement::MergeRequests::ComplianceViolationResolver#resolve user is authorized filtering the results when given an array of project IDs finds the filtered compliance violations
     Failure/Error: expect(subject).to contain_exactly(compliance_violation)
       
        expected collection contained:  [#<MergeRequests::ComplianceViolation id: 4, violating_user_id: 26, merge_request_id: 4, reason: "approved_by_committer", severity_level: "low">]
        actual collection contained:    [#<MergeRequests::ComplianceViolation id: 4, violating_user_id: 26, merge_request_id: 4, reason: "app...er_id: 27, merge_request_id: 5, reason: "approved_by_merge_request_author", severity_level: "high">]
        the extra elements were:        [#<MergeRequests::ComplianceViolation id: 5, violating_user_id: 27, merge_request_id: 5, reason: "approved_by_merge_request_author", severity_level: "high">]
     # ./ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb:55:in `block (6 levels) in <top (required)>'
    

    しかし、これは間違った結果が生成されるのではなく、ComplianceViolationResolver クラスの読み込み順序が原因です。

    これを修正する唯一の方法は、仕様書のクラス名を引用することです。例えば

     RSpec.describe Resolvers::ComplianceManagement::MergeRequests::ComplianceViolationResolver do
    

     RSpec.describe 'Resolvers::ComplianceManagement::MergeRequests::ComplianceViolationResolver' do
    

    いくつかの議論については、このマージリクエストを参照してください。

    このスタイルを使うのは、仕様に問題がある場合だけにしてください。通常はこのパターンを使うべきではありません。このイシューは2.x にアップグレードした後に消えるかもしれません。

  • GraphqlHelpers#resolve を使用してリゾルバをテストする場合、リゾルバの引数は2つの方法で処理できます。

    1. リゾルバ仕様の95%はRubyオブジェクトである引数を使用しますが、GraphQL APIを使用する場合は文字列と整数しか使用しません。これはほとんどの場合問題なく動作します。
    2. 時間枠引数を受け付けるリゾルバ (TimeFrameArguments) のように、prepare proc を使用する引数を受け付ける場合は、resolve メソッドにarg_style: :internal_prepared パラメータを渡す必要があります。これはコードに、引数を文字列と整数に変換して通常の引数処理に通すように指示し、prepare procが正しく呼び出されるようにします。例えば、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
      

      もう1つの注意点は、リゾルバの引数として列挙型を渡す場合、列挙型の内部表現ではなく外部表現を使わなければならないということです。例えば

      # good
      resolve_group_iterations({ search: search, in: ['CADENCE_TITLE'] })
            
      # bad
      resolve_group_iterations({ search: search, in: [:cadence_title] })
      

    :internal_prepared の使用は、GraphQL gem のアップグレードのための橋渡しとして追加されました。リゾルバを直接テストすることはいずれ削除されるでしょうし、リゾルバ/変異のユニットテストを書くことはすでに非推奨です。

クエリフローとGraphQLインフラに関する注意事項

GitLab GraphQL インフラストラクチャはlib/gitlab/graphql にあります。

インスツルメンテーションは、実行中のクエリをラップする機能です。これは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

クエリ・アナライザには、実行前にクエリを検証するための一連のコールバックが含まれています。各フィールドはアナライザーを通過することができ、最終的な値も利用可能です。

Multiplex クエリは、複数のクエリをひとつのリクエストで送信できるようにします。これにより、サーバに送られるリクエストの数を減らすことができます。(GraphQL Rubyが提供するカスタムMultiplexクエリーアナライザーとMultiplexインストゥルメンテーションがあります)。

クエリの制限

過度に野心的なクエリや悪意のあるクエリからサーバリソースを保護するために、クエリと変異は深さ、複雑さ、再帰によって制限されます。これらの値はデフォルトとして設定することができ、必要に応じて特定のクエリでオーバーライドすることができます。複雑さの値はオブジェクトごとに設定することもでき、最終的なクエリの複雑さは、返されるオブジェクトの数に基づいて評価されます。これは、(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ページを参照してください。

変更履歴エントリを含める

クライアント向けのすべての変更には、変更履歴エントリを含める必要があります。

怠惰

パフォーマンスを管理するためのGraphQL独自の重要なテクニックの1つは、遅延値を使用することです。遅延値は結果の約束を表し、そのアクションを後で実行できるようにすることで、クエリツリーの異なる部分でクエリのバッチ処理を可能にします。私たちのコードにおける遅延値の主な例はGraphQL BatchLoaderです。

遅延値を直接管理するには、Gitlab::Graphql::Lazy、特にGitlab::Graphql::Lazinessをお読みください。これには#force#delayが含まれており、必要に応じて遅延値の作成と削除の基本オペレーションを実装するのに役立ちます。

強制せずに遅延値を扱うには、Gitlab::Graphql::Lazy.with_valueを使ってください。