GraphQL APIスタイルガイド

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

GitLabがGraphQLを実装する方法

私たちはRobert Mosolgoによって書かれたGraphQLRubygemを使用しています。

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

ディープ・ダイブ

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

グラフィカルキューエル

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

認証

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

また、private_token をクエリストリングに追加したり、HTTP_PRIVATE_TOKEN ヘッダーを追加することも可能です。

種類

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

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

graphql_name 'Issue'

field :iid, GraphQL::ID_TYPE, null: true
field :title, GraphQL::STRING_TYPE, null: true

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

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

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

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

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

class Types::MergeRequestType < BaseObject
  present_using MergeRequestPresenter

  name 'MergeRequest'
end

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

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

ヌル可能なフィールド

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

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

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

さらに読む

グローバルIDの公開

型のID フィールドを公開する場合、デフォルトでは、レンダリングされるリソースでto_global_id を呼び出してグローバル ID を公開しようとします。

この動作をオーバーライドするには、ID を公開する型にid メソッドを実装します。カスタムメソッドを使用してGraphQL::ID_TYPE を公開する場合は、そのメソッドがグローバルに一意であることを確認してください。

full_pathID_TYPE として公開しているレコードは、この例外のひとつです。フルパスはProject またはNamespaceの一意な識別子なので。

接続タイプ

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)を使います。

型の権限の公開

現在のユーザがリソースに対して持つ権限を公開するには、リソースの権限を表す別の型を渡してexpose_permissions を呼び出します。

使用例:

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

パーミッション・タイプはBasePermissionType を継承しており、いくつかのヘルパー・メソッドを含んでおり、パーミッションをNULLでないブール値として公開することができます:

class MergeRequestPermissionsType < BasePermissionType
  present_using MergeRequestPresenter

  graphql_name 'MergeRequestPermissions'

  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フィールドに機能フラグを追加できます:

  • フィールドにfeature_flag プロパティを追加します。 これにより、フラグが無効なときにフィールドを GraphQL スキーマから_非表示に_することができます。
  • フィールドを解決する際の戻り値を切り替えます。

これらのガイドラインを参考にして、どの方法を使うかを決めてください:

  • フィールドが実験的なもので、その名前や型が変更される可能性がある場合は、feature_flag プロパティを使用してください。
  • フィールドが安定していて、フラグを削除しても定義が変わらない場合は、代わりにフィールドの戻り値をトグルします。 いずれにせよ、すべてのフィールドはnull可能であるべきであることに注意してください。

feature_flag プロパティ

feature_flag プロパティを使用すると、GraphQL スキーマ内でのフィールドの可視性を切り替えることができます。 このフラグが無効の場合、スキーマからフィールドが削除されます。

フィールドには、フィーチャーフラグの後ろにあることを示す説明が付加されます。

注意:機能フラグが無効になっているときにクライアントがフィールドのクエリを実行すると、クエリは失敗します。 本番環境で機能の表示/非表示を切り替える場合は、この点を考慮してください。

feature_flag プロパティでは、アクターに基づく機能ゲートを使用することはできません。これは、機能フラグを特定のプロジェクト、グループ、またはユーザーのみに切り替えることはできず、代わりに、すべての人に対してのみグローバルに切り替えることができることを意味します。

使用例:

field :test_field, type: GraphQL::STRING_TYPE,
      null: true,
      description: 'Some test field',
      feature_flag: :my_feature_flag

フィールドの値をトグル

フィールドの特徴フラグを使用するこの方法は、フィールドの戻り値をトグルすることです。 これは、好みや状況に応じて、リゾルバ、型、あるいはモデルメソッドで行うことができます。

フィールドの値をトグルする機能フラグを適用する場合、そのフィールドのdescription が必要です:

  • フィーチャーフラグによってフィールドの値を切り替えることができることを示します。
  • 機能フラグに名前を付けます。
  • 機能フラグが無効の場合(またはより適切な場合は有効の場合)、フィールドが返す内容を記述します。

使用例:

field :foo, GraphQL::STRING_TYPE,
      null: true,
      description: 'Some test field. Will always return `null`' \
                   'if `my_feature_flag` feature flag is disabled'

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

非推奨フィールド

GitLabのGraphQL APIはバージョンレスです。つまり、変更のたびに古いバージョンのAPIとの後方互換性を維持します。 フィールドを削除するのではなく、代わりにフィールドを_非推奨に_する必要があります。 将来、GitLabは非推奨のフィールドを削除する可能性があります

フィールドは、deprecated プロパティを使用して非推奨です。プロパティの値は、Hash

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

使用例:

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

そのフィールドのオリジナルのdescription: は維持されるべきであり、非推奨について言及するために更新されるべきでは_ありません_。

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

非推奨の理由が、そのフィールドが他のフィールドに置き換えられるためである場合、reason

Use `otherFieldName`

使用例:

field :designs, ::Types::DesignManagement::DesignCollectionType, null: true,
      deprecated: { reason: 'Use `designCollection`', milestone: '10.0' },
      description: 'The designs associated with this issue',

そのフィールドが他のフィールドに置き換えられない場合は、reason

列挙

GitLab GraphQL enum はapp/graphql/typesで定義されています。 新しい enum を定義する際には、以下のルールが適用されます:

  • 値は大文字でなければなりません。
  • クラス名は、文字列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'に変換されます。
  • Rubyの'opened' の値は、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: 'An closed Epic'
  end
end

内容

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

フィールドや引数の説明は、description:キーワードを使います:

field :id, GraphQL::ID_TYPE, description: 'ID of the resource'

フィールドと引数の説明は、ユーザーを通して見ることができます:

説明文スタイルガイド

一貫性を保つため、説明文の追加や更新を行う際は、必ず以下に従ってください:

  • 説明文にリソースの名前を記載します。 例:'Labels of the issue' (イシューがリソースです)。
  • 可能な場合は"{x} of the {y}" を使用してください。 例:'Title of the issue'.説明文はTheで始めないでください。
  • GraphQL::BOOLEAN_TYPE フィールドの説明は、「このフィールドは何をするのか」という質問に答えるものでなければなりません。 例:'Indicates project has a Git repository'.
  • Types::TimeType型の引数やフィールドを記述する際には、常に"timestamp" という単語を含めてください。 これにより読者は、プロパティの形式が単なるDateではなく、Timeになることを知ることができます。
  • 弦の端に.

使用例:

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

copy_field_description ヘルパー

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

説明を指定する代わりに、copy_field_description ヘルパーを使い、型と説明をコピーするフィールド名を渡します。

使用例:

argument :title, GraphQL::STRING_TYPE,
          required: false,
          description: copy_field_description(Types::MergeRequestType, :title)

作成者

作成者は、Railsアプリと同じ機能を使用して、型とフィールドの両方に適用できます。

もし

  • 現在認証されているユーザが認可に失敗した場合、認可されたリソースはnullとして返されます。
  • リソースがコレクションの一部である場合、コレクションはフィルタリングされ、ユーザの作成者チェックが失敗したオブジェクトを除外します。

変異におけるリソースの作成者も参照してください。

ヒント:レコードをフィルタリングするために作成者の権限に依存することなく、現在認証されているユーザが閲覧を許可されているものだけを既存のファインダで最初にロードするようにしてください。 これにより、データベース・クエリと、ロードされたレコードの不要な権限チェックを最小限に抑えることができます。

タイプ認証

authorize メソッドに能力を渡すことで、型をオーソライズします。同じ型を持つすべてのフィールドは、現在認証されているユーザーが必要な能力を持っていることをチェックすることでオーソライズされます。

た と えば、 以下の作成者は、 現在認証 さ れてい る ユーザーがread_project の能力を持つプ ロ ジ ェ ク ト のみを表示で き る よ う に し ます (プ ロ ジ ェ ク ト がTypes::ProjectTypeを使用す る フ ィ ール ド で返 さ れ る 限 り ):

module Types
  class ProjectType < BaseObject
    authorize :read_project
  end
end

複数の能力に対してオーソライズすることもでき、その場合はすべての能力チェックに合格しなければなりません。

例えば、以下の作成者は、現在認証されているユーザーがプロジェクトを見るにはread_projectanother_abilityの権限が必要であることを保証します:

module Types
  class ProjectType < BaseObject
    authorize [:read_project, :another_ability]
  end
end

作成者の許可

フィールドはauthorize オプションで作成者を許可することができます。

例えば、以下の作成者は、現在認証されているユーザーがowner_access でプロジェクトを見ることができることを保証します:

module Types
  class MyType < BaseObject
    field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, authorize: :owner_access
  end
end

フィールドは複数の能力に対して作成することもでき、その場合はすべての能力チェックに合格する必要があります。注:この場合、fieldに明示的にブロックを渡す必要があります:

module Types
  class MyType < BaseObject
    field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
      authorize [:owner_access, :another_ability]
    end
  end
end
注意:もしフィールドのタイプに既に特定の作成者がいる場合、そのフィールドに同じ作成者を追加する必要はありません。

タイプおよびフィールドの作成者を統合

作成者の権限は累積されるため、フィールドとフィールドのタイプに権限が定義されている場合、現在認証されているユーザはすべての能力チェックに合格する必要があります。

以下の単純化した例では、現在認証されているユーザがイシューの作成者を確認するには、first_permissionsecond_permission の両方の権限が必要です。

class UserType
  authorize :first_permission
end
class IssueType
  field :author, UserType, authorize: :second_permission
end

リゾルバ

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

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

引数をリゾルバ内で定義することができ、それらの引数はリゾルバを使用するフィールドで利用できるようになります。 内部ID (iid) を持つモデルを公開する場合、リゾルバの引数としてデータベースIDよりも名前空間パスと組み合わせて使用することを推奨します。 そうでない場合は、グローバルに一意なIDを使用します。

多くの依存オブジェクトを持つプロジェクトや名前空間を素早く見つけるために、他のリゾルバに含めることができるFullPathLoader

実行されるクエリーの量を制限するために、BatchLoader

の正しい使い方Resolver#ready?

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

#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

最後に必要なことは、このリゾルバを使うすべてのフィールドが、ルックアヘッドの必要性を宣伝する必要があるということです:

  # in ParentType
  field :my_things, MyThingType.connection_type, null: true,
        extras: [:lookahead], # Necessary
        resolver: MyThingResolver,
        description: 'My things'

実際の使用例については、ResolvesMergeRequestsをご覧ください。

突然変異

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

変異の対象となるオブジェクトを見つけるには、引数を指定する必要があります。リゾルバと同様、データベースIDではなく、内部IDか、必要であればグローバルIDを使用することをお勧めします。

突然変異の構築

ミューテーションはapp/graphql/mutations に存在し、理想的にはサービスと同じように、ミューテーションするリソースごとにグループ化されています。ミューテーションはMutations::BaseMutationを継承する必要があります。ミューテーションで定義されたフィールドは、ミューテーションの結果として返されます。

命名規則

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

使用例:

class UserUpdateMutation < BaseMutation
  graphql_name 'UserUpdate'
end

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

新しいリソースを作成する突然変異は、Createという動詞を使うべきです。

使用例:

  • CommitCreate

データの更新に使用する変異:

  • 動詞Update
  • より適切であれば、SetAddToggle のようなドメイン固有の動詞。

例:

  • EpicTreeReorder
  • IssueSetWeight
  • IssueUpdate
  • TodoMarkDone

データを削除する変異:

  • 動詞はDestroyではなくDelete
  • Remove のようなドメイン固有の動詞の方が適切です。

例:

  • AwardEmojiRemove
  • NoteDelete

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

議論

ミューテーションに必要な引数は、フィールドに必要な引数として定義することができます。 これらは、ミューテーションの入力タイプにラップされます。たとえば、Mutations::MergeRequests::SetWipwith GraphQL-nameMergeRequestSetWip 、これらの引数を定義します:

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

argument :iid, GraphQL::STRING_TYPE,
         required: true,
         description: "The iid of the merge request to mutate"

argument :wip,
         GraphQL::BOOLEAN_TYPE,
         required: false,
         description: <<~DESC
                      Whether or not to set the merge request as a WIP.
                      If not passed, the value will be toggled.
                      DESC

これにより、指定した3つの引数とclientMutationIdを持つMergeRequestSetWipInput という入力タイプが自動的に生成されます。

これらの引数は、キーワード引数として突然変異のresolve メソッドに渡されます。

フィールド

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

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

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

resolve メソッド

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

resolve メソッドは、errors 配列を含む、突然変異で定義されたのと同じフィールド名を持つハッシュを返す必要があります。例えば、Mutations::MergeRequests::SetWipmerge_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_typesにある変異タイプで定義する必要があります。mount_mutation ヘルパーメソッドは、変異の GraphQL-name に基づいてフィールドを定義します:

module Types
  class MutationType < BaseObject
    include Gitlab::Graphql::MountMutation

    graphql_name "Mutation"

    mount_mutation Mutations::MergeRequests::SetWip
  end
end

Mutations::MergeRequests::SetWip が解決されるmergeRequestSetWip というフィールドが生成されます。

リソースの作成者

変異内部でリソースの作成者を認可するには、まずこのように変異に必要な能力を与えます:

module Mutations
  module MergeRequests
    class SetWip < Base
      graphql_name 'MergeRequestSetWip'

      authorize :update_merge_request
    end
  end
end

そして、resolve メソッドでauthorize! を呼び出し、能力を検証したいリソースを渡します。

あるいは、find_object メソッドを追加して、変異時にオブジェクトをロードすることもできます。 この場合、authorized_find! ヘルパーメソッドを使うことができます。

ユーザーがアクションを実行できない場合、またはオブジェクトが見つからない場合は、Gitlab::Graphql::Errors::ResourceNotAvailable エラーを発生させなければなりません。 クライアントに正しくレンダリングされます。

突然変異のエラー

私たちは、エラーを突然変異のデータとして利用することを奨励します。これは、エラーを誰に関連するかによって区別し、誰がそれに対処できるかを定義するものです。

重要なポイント

  • すべての突然変異レスポンスにはerrors フィールドがあります。これは失敗時に入力されるべきもので、成功時に入力されることもあります。
  • エラーを見る必要があるのは誰なのかを考えてみましょう。
  • クライアントは、変異を実行する際に、常にerrors フィールドをリクエストする必要があります。
  • エラーは、$root.errors (トップレベルのエラー)または$root.data.mutationName.errors (変異エラー)のいずれかでユーザーに報告されることがあります。場所は、これがどのようなエラーであり、どのような情報を保持しているかによって異なります。

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

突然変異反応には3つの状態があります:

成功

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

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

失敗(ユーザーに関連)

ユーザーに影響を与えるエラーが発生しました。 これを_変異エラーと_呼びます。この場合、通常、thing

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

その例としては、以下のようなものがあります:

  • モデル検証エラー:ユーザーは入力を変更する必要があるかもしれません。
  • 権限エラー: ユーザーはこれを実行できないことを知る必要があり、権限を要求するかサインインする必要があります。
  • 例えば、マージが競合している、リソースがロックされているなどです。

理想的には、ユーザがここまで到達するのを防ぐべきですが、もし到達してしまったら、何が間違っているのかを教えて、失敗の理由と、たとえそれがリクエストを再試行するような単純なものであっても、自分の意図を達成するために何ができるかを理解させる必要があります。

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

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

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

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

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

これらは、次のようなプログラミングエラーを表しています:

  • GraphQL 構文エラー。Stringの代わりにInt が渡されたか、必須引数が存在しませんでした。
  • スキーマのエラー。例えば、null 値でないフィールドの値を提供できない場合などです。
  • システムエラー:例えば、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を参照することができます。

引数の検証

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

ミューテーションやリゾルバでいくつかのオプション引数を受け付けることがありますが、 その場合でもオプション引数の少なくともひとつが提供されていることを検証したいことがあります。 このような場合は、#ready?ミューテーションやリゾルバの#ready?内部で#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がマージされれば、InputUnions

GitLabのカスタムスカラー

Types::TimeType

Types::TimeTypeRubyTime およびDateTime オブジェクトを扱うすべてのフィールドと引数の型として使用する必要があります。

型はカスタムスカラーです:

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

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

使用例:

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

テスト

グラフクエリやミューテーションの_フルスタックテストは_、spec/requests/api/graphqlで実行できます。

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

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

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

クエリフローと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クエリーを使用すると、1つのリクエストで複数のクエリーを送信することができます。 これにより、サーバーに送信されるリクエスト数を減らすことができます(GraphQL Rubyが提供するカスタムMultiplexクエリーアナライザーとMultiplexインストゥルメンテーションがあります)。

クエリー制限

クエリと変異は、過剰な野心的クエリや悪意のあるクエリからサーバーリソースを保護するために、深さ、複雑さ、再帰によって制限されます。 これらの値はデフォルトとして設定することができ、必要に応じて特定のクエリでオーバーライドすることができます。 複雑さの値はオブジェクトごとに設定することもでき、最終的なクエリの複雑さは、返されるオブジェクトの数に基づいて評価されます。 これは、高価なオブジェクト(例えば、Gitalyコールを必要とする)に便利です。

例えば、リゾルバにおける条件付き複雑化法:

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

  complexity
end

複雑さの詳細:GraphQLRubyドキュメント

ドキュメントとスキーマ

私たちのスキーマはapp/graphql/gitlab_schema.rbにあります。 詳細はスキーマ・リファレンスを参照してください。

スキーマが変更された場合は、この生成されたGraphQLドキュメントを更新する必要があります。 GraphQLドキュメントとスキーマファイルの生成については、スキーマドキュメントの更新を参照してください。