- GitLabがGraphQLを実装する方法
- ディープダイブ
- GraphiQL
- GraphQL による変更を含むマージリクエストのレビュー
- GraphQL ログの読み取り
- 認証
- 限界
- 破壊的な変更
- グローバルID
- 型
- フィーチャーフラグ
- 非推奨スキーマ項目
- スキーマ項目をアルファとしてマーク
- 列挙
- JSON
- 説明
- 作成者
- リゾルバ
- 突然変異
- サブスクリプション
- ページネーションの実装
- 引数の検証
- GitLab カスタムスカラー
- テスト
- クエリフローとGraphQLインフラに関する注意事項
- ドキュメントとスキーマ
- 変更履歴エントリを含める
- 怠惰
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
)。
iid
、title
、description
は_スカラー_GraphQL 型です。iid
はGraphQL::Types::ID
、一意の ID を意味する特殊な文字列型です。title
とdescription
は通常の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!]!
さらに読む
- GraphQLベストプラクティスガイド。
- オブジェクトタイプとフィールドに関するGraphQLドキュメント。
- GraphQLでnullabilityを使用
グローバル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
接続タイプ
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
を指定することでオーバーライドできます。
開発者は、データを返すためにサーバでより多くの_作業が_発生するフィールドに対して、より高い複雑度を指定する必要があります。例えば、ほとんどの場合、id
やtitle
のように、ほとんど_何もしなくても_データを返すことができるフィールドには、0
の複雑度を指定することができます。
calls_gitaly
解決時にGitalyコールを実行する可能性があるフィールドは、定義時にcalls_gitaly: true
をfield
に渡すことで、そのようにマークする_必要が_あります。
使用例:
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-ruby
のfield
メソッドと同じ動作をしますが、デフォルトの説明とタイプを設定し、それらを NULL 以外にすることができます。これらのオプションは、引数として追加することでオーバーライドすることができます。 -
ability_field
:ポリシーで定義された能力を公開します。これはpermission_field
と同じように動作し、同じ引数をオーバーライドできます。 -
abilities
:ポリシーで定義された複数の能力を一度に公開できます。これらのフィールドはすべて、デフォルトの説明を持つヌルではないブール値でなければなりません。
フィーチャーフラグ
GraphQLでは、機能フラグを実装して切り替えることができます:
- フィールドの返り値。
- 引数や突然変異の動作。
これは、好みや状況に応じて、リゾルバや型、あるいはモデルメソッドで行うことができます。
機能フラグ項目の説明
機能フラグを使ってスキーマ項目の値や動作を切り替える場合、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でスキーマ項目を非推奨にするには:
- 項目の非推奨イシューを作成します。
- スキーマで非推奨として項目をマークします。
こちらも参照してください。
非推奨イシューの作成
すべての 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
はメンテナーとすべきであり、非推奨に言及するために更新されるべきでは_ありません_。その代わりに、reason
をdescription
に追加します。
非推奨の理由スタイルガイド
非推奨の理由がフィールド、引数、列挙値が置き換えられることによるものである場合、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
に非推奨を追加します。
シナリオの例
このシナリオ例は、このマージリクエストに基づいています。
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::Deprecations
のDEPRECATIONS
定数を編集し、新しいDeprecation
を配列に追加します:
DEPRECATIONS = [
Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(old_name: 'PrometheusService', new_name: 'Integrations::Prometheus', milestone: '14.0')
].freeze
その後、通常の非推奨プロセスに従ってください。後で以前の引数スタイルのサポートを削除するには、Deprecation
を削除してください:
DEPRECATIONS = [].freeze
非推奨期間中、APIは引数の値としてこれらの形式のいずれかを受け入れます:
"gid://gitlab/PrometheusService/1"
"gid://gitlab/Integrations::Prometheus/1"
API は、引数のクエリ・シグネチャでもこれらの型を受け入れます:
PrometheusServiceID
IntegrationsPrometheusID
PrometheusServiceID
) を使用したクエリは API では有効で実行可能ですが、 バリデータツールでは無効とみなされます。無効とみなされるのは、@deprecated
ディレクティブの外部で特注のメソッドを使用して非推奨としているため、 バリデータがそのサポートに気づいていないからです。ドキュメントでは、古いグローバル ID スタイルは非推奨であると述べています。
スキーマ項目をアルファとしてマーク
GraphQLスキーマ項目(フィールド、引数、列挙値、および変異)をアルファとしてマークすることができます。
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 given
やthe specified
の代わりにthis
を使うと簡潔です。例えば
Sort issues by this criteria.
一貫性と簡潔性のため、The
やA
で説明を始めないでください。
すべての説明の終わりはピリオド (.
) で終わらせてください。
ブーリアン
真偽値フィールド(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
を書いている場合は、オブジェクトを直接検索してください。 -
Resolver
やBaseObject
のメソッドを使用する場合は、バッチ処理を許可する必要があります。
エラーハンドリング
リゾルバはエラーを発生させる可能性があり、そのエラーは必要に応じてトップレベルのエラーに変換されます。予測されるエラーはすべてキャッチされ、適切な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
クラスで親オブジェクトを見つけるには、以下の手順に従います:
-
リゾルバの
resolve
メソッドから GraphQLcontext
に親オブジェクトを追加します:def resolve(**args) context[:parent_object] = parent end
-
リゾルバまたはフィールドが
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.'
-
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
実際の使用例として、 scopedPath
とscopedUrl
を追加した 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
必須引数の値が null
required: :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_path
やgroup_path
とiid
を組み合わせて使うことをお勧めします。
グローバルIDの廃止も参照してください。
フィールド
最も一般的な状況では、突然変異は2つのフィールドを返します:
- 変更されるリソース
- アクションを実行できなかった理由を説明するエラーのリスト。変異が成功した場合、このリストは空になります。
Mutations::BaseMutation
から新しい変異を継承することで、errors
フィールドが自動的に追加されます。また、clientMutationId
フィールドも追加されます。これは、1つのリクエストで複数の変異が実行されたときに、クライアントが1つの変異の結果を識別するために使うことができます。
resolve
メソッド
リゾルバを書くのと同様に、突然変異のresolve
メソッドは、サービスの周りの薄い宣言的なラッパーであることを目指すべきです。
resolve
メソッドは変異の引数をキーワード引数として受け取ります。ここから、リソースを変更するサービスを呼び出すことができます。
resolve
メソッドは、errors
配列を含む、変異で定義されたものと同じフィールド名を持つハッシュを返す必要があります。例えば、Mutations::MergeRequests::SetDraft
は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_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 クエリを実行し、結果をクライアントにプッシュします。
サブスクリプションの構築
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
は、RubyTime
やDateTime
オブジェクトを扱うすべてのフィールドや引数の型として使わなければなりません。
この型は
- Rubyの
Time
、DateTime
オブジェクトを標準化された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#resolve
やGraphqlHelpers#resolve_field
よりもGraphqlHelpers#execute_query
やGraphqlHelpers#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 })
ユニットテストの書き方(非推奨)
ユニットテストを作成する前に、以下の例をレビューしてください:
インテグレーションテストの書き方
インテグレーションテストは、GraphQL クエリまたは変異のスタック全体をチェックし、spec/requests/api/graphql
に保存されます。
スピードを上げるには、GitlabSchema.execute
を直接呼び出すか、テスト対象の型のみを含む小さなテストスキーマを利用することを検討してください。
しかし、データが返されるかどうかをチェックするフルリクエストのインテグレーションテストでは、以下の追加項目を検証します:
- 変異がスキーマで実際にクエリ可能であること(
MutationType
にマウントされていること)。 - リゾルバや変異が返すデータはフィールドの戻り値の型と正しく一致し、エラーなしで解決されます。
- 引数は入力時に正しく強制され、フィールドは出力時に正しくシリアライズされます。
インテグレーションテストでは、フルスタックを呼び出すため、以下の項目も検証できます:
- 引数またはスカラーの
prepare
が正しく適用されること。 - リゾルバまたは突然変異の
#ready?
メソッド の論理が正しく適用されます。 -
引数の
default_value
は正しく適用されます。 - オブジェクトは正常に解決され、N+1の問題はありません。
クエリを追加する際には、a working graphql query that returns data
とa 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_at
やGraphqlHelpers#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_graphql
のcurrent_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.rb
のTypes::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つの方法で処理できます。- リゾルバ仕様の95%はRubyオブジェクトである引数を使用しますが、GraphQL APIを使用する場合は文字列と整数しか使用しません。これはほとんどの場合問題なく動作します。
-
時間枠引数を受け付けるリゾルバ (
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
を使ってください。