- GitLabがGraphQLを実装する方法
- ディープ・ダイブ
- グラフィカルキューエル
- 認証
- 種類
- フィーチャーフラグ
- 非推奨フィールド
- 列挙
- 内容
- 作成者
- リゾルバ
- 突然変異
- 引数の検証
- GitLabのカスタムスカラー
- テスト
- クエリフローとGraphQLインフラストラクチャに関する注意事項
- ドキュメントとスキーマ
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
)。
iid
、title
、description
は_スカラー_GraphQL 型です。iid
はGraphQL::ID_TYPE
、一意の ID を意味する特別な文字列型です。title
とdescription
は通常の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_path
をID_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-ruby
のfield
メソッドと同じ動作をしますが、デフォルトの説明と型を設定し、 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_project
とanother_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_permission
とsecond_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
。 - より適切であれば、
Set
、Add
、Toggle
のようなドメイン固有の動詞。
例:
EpicTreeReorder
IssueSetWeight
IssueUpdate
TodoMarkDone
データを削除する変異:
- 動詞は
Destroy
ではなくDelete
。 -
Remove
のようなドメイン固有の動詞の方が適切です。
例:
AwardEmojiRemove
NoteDelete
突然変異の命名についてアドバイスが必要な場合は、Slack#graphql
チャンネルでフィードバックを求めてください。
議論
ミューテーションに必要な引数は、フィールドに必要な引数として定義することができます。 これらは、ミューテーションの入力タイプにラップされます。たとえば、Mutations::MergeRequests::SetWip
with 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::SetWip
はmerge_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::TimeType
RubyTime
およびDateTime
オブジェクトを扱うすべてのフィールドと引数の型として使用する必要があります。
型はカスタムスカラーです:
- Rubyの
Time
およびDateTime
オブジェクトを、GraphQLフィールドの型として使用する場合に、標準化されたISO-8601形式の文字列に変換します。 - GraphQL引数の型として使用する場合、ISO-8601形式の時間文字列をRuby
Time
オブジェクトに変換します。
これにより、私たちの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
ドキュメントとスキーマ
私たちのスキーマはapp/graphql/gitlab_schema.rb
にあります。 詳細はスキーマ・リファレンスを参照してください。
スキーマが変更された場合は、この生成されたGraphQLドキュメントを更新する必要があります。 GraphQLドキュメントとスキーマファイルの生成については、スキーマドキュメントの更新を参照してください。