- はじめに
- Apolloクライアント
- GraphQLクエリ
- グローバルID
- 不変性とキャッシュ更新
- Vueでの使用法
- エラーの処理
- Vue以外での使用
- GraphQLスタートアップコールによる初期クエリの作成
- トラブルシューティング
GraphQL
はじめに
リソース
一般的なリソース
GitLabでのGraphQL:
- GitLab Unfiltered GraphQL プレイリスト
-
GitLabでのGraphQL:ディープダイブ(ビデオ) by Nick Thomas
- GitLabにおけるGraphQLの歴史の概要(フロントエンドに特化したものではありません)
-
GraphQLとVue ApolloによるGitLab機能のウォークスルー(ビデオ) by Natalia Tepluhina
- GraphQLを使ってGitLabのフロントエンド機能を実装する実例。
- GitLabにおけるクライアントサイドGraphQLの歴史(ビデオ) Illya Klymov and Natalia Tepluhina
-
VuexからApolloへ(ビデオ) by Natalia Tepluhina
- VuexよりもApolloの方が良い選択となる場合の概要と、どのように移行を進めるかについて説明します。
-
😎 Vuex -> Apolloマイグレーション: 概念実証プロジェクト
- Vue+GraphQL+(VuexまたはApollo)アプリで可能な状態管理のアプローチを示すサンプル集
ライブラリ
フロントエンド開発にGraphQLを使用する場合、Apollo(特にApollo Client)とVue Apolloを使用します。
VueアプリケーションでGraphQLを使用する場合、VueApolloのインテグレーション方法を学ぶには、Vueでの使用法のセクションが役立ちます。
その他の使用例については、「Vue以外での使用」セクションをご覧ください。
イミュータブルなキャッシュの更新にはImmerを使用します。詳細については、イミュータビリティとキャッシュの更新を参照してください。
ツール
Apollo GraphQL VSコード拡張
VS Codeを使用している場合、Apollo GraphQL拡張機能は、.graphql
ファイル内のオートコンプリートをサポートします。GraphQL拡張機能をセットアップするには、以下の手順に従ってください:
- スキーマを生成します:
bundle exec rake gitlab:graphql:schema:dump
-
gitlab
ローカル・ディレクトリのルートにapollo.config.js
ファイルを追加します。 -
そのファイルに以下の内容を入力してください:
module.exports = { client: { includes: ['./app/assets/javascripts/**/*.graphql', './ee/app/assets/javascripts/**/*.graphql'], service: { name: 'GitLab', localSchemaFile: './tmp/tests/graphql/gitlab_schema.graphql', }, }, };
- VSコードを再起動します。
GraphQL API の探索
私たちのGraphQL APIは、インスタンスの/-/graphql-explorer
、またはGitLab.comでGraphiQLを介して探索することができます。必要に応じてGitLab GraphQL API Reference ドキュメントを参照してください。
すべての既存のクエリと変異を確認するには、GraphiQLの右側でDocumentation explorerを選択します。作成したクエリや変異の実行を確認するには、左上で[Execute query]を選択します。
Apolloクライアント
異なるアプリでクライアントが重複して作成されるのを防ぐため、デフォルトのクライアントを使用するようにしています。これは、正しいURLでApolloクライアントを設定し、CSRFヘッダも設定します。
デフォルトクライアントは2つのパラメータを受け付けます:resolvers
とconfig
。
-
resolvers
パラメータは、ローカルの状態管理クエリおよび変異用のリゾルバのオブジェクトを受け取るために作成されます。 -
config
パラメータは設定オブジェクトを受け取ります:-
cacheConfig
フィールドには、アポロキャッシュをカスタマイズするためのオプションの設定オブジェクトを受け取ります。 -
baseUrl
では、メインのエンドポイントとは異なるGraphQLエンドポイントのURLを渡すことができます (たとえば、${gon.relative_url_root}/api/graphql
)。 -
fetchPolicy
コンポーネントをApolloキャッシュとどのように相互作用させたいかを決定します。デフォルトは “cache-first “です。
-
同じオブジェクトに対する複数のクライアントクエリ
同じApolloクライアントオブジェクトに対して複数のクエリを実行している場合、次のエラーが発生する可能性があります:Cache data may be lost when replacing the someProperty field of a Query object. To address this problem, either ensure all objects of SomeEntityhave an id or a custom merge function
.id
を持つid
すべてのGraphQLタイプについて id
すでにid
存在を id
チェックしているため、このようなケースは発生しないはずです(ユニット テストを実行しているときにこの警告が表示される場合を除きます。)
SomeEntity
型が GraphQL スキーマにid
プロパティを持たない場合、この警告を修正するにはカスタムマージ関数を定義する必要があります。
デフォルトのクライアントでtypePolicies
としてmerge: true
が定義されているクライアント全体の型がいくつかあります(これは、Apollo が後続のクエリの場合に既存の応答と受信応答をマージすることを意味します)。そこにSomeEntity
を追加するか、カスタムマージ関数を定義することを検討してください。
GraphQLクエリ
実行時のクエリコンパイルの手間を省くために、webpackは.graphql
ファイルを直接インポートすることができます。これにより、クライアントがクエリのコンパイルを行う代わりに、webpackがコンパイル時にクエリの前処理を行うことができます。
クエリと変異やフラグメントを区別するために、以下の命名規則を推奨します:
-
all_users.query.graphql
クエリ; -
add_user.mutation.graphql
突然変異 -
basic_user.fragment.graphql
フラグメント
CustomersDot GraphQL エンドポイント用のクエリを使用する場合は、ファイル名の最後に.customer.query.graphql
、.customer.mutation.graphql
、または.customer.fragment.graphql
を付けてください。
フラグメント
フラグメントは、複雑なGraphQLクエリをより読みやすく、再利用可能にする方法です。以下にGraphQLフラグメントの例を示します:
fragment DesignListItem on Design {
id
image
event
filename
notesCount
}
フラグメントは個別のファイルに保存することができ、インポートしてクエリ、変異、または他のフラグメントで使用することができます。
#import "./design_list.fragment.graphql"
#import "./diff_refs.fragment.graphql"
fragment DesignItem on Design {
...DesignListItem
fullPath
diffRefs {
...DesignDiffRefs
}
}
フラグメントの詳細GraphQLドキュメント
グローバルID
GitLab GraphQL APIは、id
PostgreSQLの主キーではなくグローバルIDとしてフィールドを id
表現します。グローバルIDは、クライアントサイドのライブラリでキャッシュやフェッチのために使用される規約です。
グローバル ID を主キーid
に変換するには、getIdFromGraphQLId
を使用します:
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
const primaryKeyId = getIdFromGraphQLId(data.id);
スキーマにid
anがあるすべてのGraphQLタイプに対して id
globalをクエリする必要があります:
query allReleases(...) {
project(...) {
id // Project has an ID in GraphQL schema so should fetch it
releases(...) {
nodes {
// Release has no ID property in GraphQL schema
name
tagName
tagPath
assets {
count
links {
nodes {
id // Link has an ID in GraphQL schema so should fetch it
name
}
}
}
}
pageInfo {
// PageInfo no ID property in GraphQL schema
startCursor
hasPreviousPage
hasNextPage
endCursor
}
}
}
}
不変性とキャッシュ更新
Apolloバージョン3.0.0から、すべてのキャッシュ更新は不変である必要があります。新しい更新されたオブジェクトに完全に置き換える必要があります。
キャッシュを更新し、新しいオブジェクトを返すプロセスを容易にするために、ライブラリImmer を使用します。以下の規約に従ってください:
- 更新されたキャッシュの名前は
data
です。 - 元のキャッシュデータの名前は
sourceData
です。
典型的な更新プロセスは次のようになります:
...
const sourceData = client.readQuery({ query });
const data = produce(sourceData, draftState => {
draftState.commits.push(newCommit);
});
client.writeQuery({
query,
data,
});
...
コード例で示したように、produce
を使用することで、draftState
を直接操作することができます。また、immer
は、draftState
の変更を含む新しい状態が生成されることを保証します。
Vueでの使用法
Vue Apolloを使用するには、デフォルトのクライアントと同様にVue Apolloプラグインをインポートします。これは、Vueアプリケーションがマウントされるのと同じ時点で作成する必要があります。
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
new Vue({
...,
apolloProvider,
...
});
Vue Apolloの詳細については、Vue Apolloのドキュメントを参照してください。
Apolloを使用したローカルステート
デフォルトのクライアントを作成する際に、Apolloでアプリケーションの状態を管理することが可能です。
クライアント側リゾルバの使用
デフォルトの状態は、デフォルトのクライアントを設定した後にキャッシュに書き込むことで設定できます。以下の例では、@client
Apolloディレクティブを使用したクエリを使用して、Apolloキャッシュに初期データを書き込み、Vueコンポーネントでこの状態を取得しています:
// user.query.graphql
query User {
user @client {
name
surname
age
}
}
// index.js
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import userQuery from '~/user/user.query.graphql'
Vue.use(VueApollo);
const defaultClient = createDefaultClient();
defaultClient.cache.writeQuery({
query: userQuery,
data: {
user: {
name: 'John',
surname: 'Doe',
age: 30
},
},
});
const apolloProvider = new VueApollo({
defaultClient,
});
// App.vue
import userQuery from '~/user/user.query.graphql'
export default {
apollo: {
user: {
query: userQuery
}
}
}
writeQuery
を使用する代わりに、キャッシュからuserQuery
を読み込もうとするたびにuser
を返すタイプポリシーを作成できます:
const defaultClient = createDefaultClient({}, {
cacheConfig: {
typePolicies: {
Query: {
fields: {
user: {
read(data) {
return data || {
user: {
name: 'John',
surname: 'Doe',
age: 30
},
}
}
}
}
}
}
}
});
ローカルデータを作成するだけでなく、既存のGraphQL型を@client
フィールドで拡張することもできます。これは、GraphQL APIにまだ追加されていないフィールドのAPIレスポンスをモックする必要がある場合に非常に便利です。
ローカルのApolloキャッシュを使用したAPIレスポンスのモッキング
ローカルApolloキャッシュの使用は、ローカルでGraphQL APIレスポンス、クエリ、または変異をモックする理由がある場合に便利です(実際のAPIにまだ追加されていない場合など)。
たとえば、クエリで使用するDesignVersion
のフラグメントがあります:
fragment VersionListItem on DesignVersion {
id
sha
}
また、バージョンのドロップダウンリストに表示するために、バージョンの作成者とcreated at
プロパティを取得する必要があります。しかし、これらの変更はまだAPIに実装されていません。既存のフラグメントを変更して、これらの新しいフィールドに対するモック応答を取得することができます:
fragment VersionListItem on DesignVersion {
id
sha
author @client {
avatarUrl
name
}
createdAt @client
}
これでApolloは、@client
ディレクティブでマークされたすべてのフィールドの_リゾルバを_見つけようとします。DesignVersion
型の DesignVersion
リゾルバを作成しましょうDesignVersion
(なぜかと DesignVersion
いうと、私たちのフラグメントがこの型で作成されているからです)。
// resolvers.js
const resolvers = {
DesignVersion: {
author: () => ({
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
name: 'Administrator',
__typename: 'User',
}),
createdAt: () => '2019-11-13T16:08:11Z',
},
};
export default resolvers;
既存のApolloクライアントにリゾルバオブジェクトを渡す必要があります:
// graphql.js
import createDefaultClient from '~/lib/graphql';
import resolvers from './graphql/resolvers';
const defaultClient = createDefaultClient(resolvers);
バージョンを取得しようとするたびに、クライアントはリモートAPIエンドポイントからid
とsha
を取得します。そして、author
とcreatedAt
のバージョンプロパティに、ハードコードされた値を割り当てます。このデータにより、フロントエンド開発者はバックエンドにブロックされることなくUIに取り組むことができます。レスポンスがAPIに追加されると、カスタムローカルリゾルバは削除できます。クエリ/フラグメントの変更は、@client
ディレクティブを削除することだけです。
Apolloによるローカル状態の管理については、Vue Apolloのドキュメントを参照してください。
Vuexでの使用
VuexとApollo Clientを組み合わせて新しいアプリケーションを作成することはお勧めしません。理由はいくつかあります:
- VueXとApolloはどちらもグローバルストアであり、責任を共有し、2つの真実のソースを持つことを意味します。
- VueXとApolloの同期を維持することは、高いメンテナンスが必要です。
- ApolloとVueX間の通信に起因するバグは、微妙でデバッグが困難です。
フロントエンドとバックエンドが同期していないときに、GraphQLベースの機能に取り組むこと
GraphQL クエリ/変異を作成または更新する必要がある機能は、慎重に計画する必要があります。フロントエンドとバックエンドの担当者は、クライアントサイドとサーバーサイドの両方の要件を満たすスキーマに合意する必要があります。これにより、両部門が互いにブロックすることなくそれぞれの部分の実装を開始することができます。
理想的には、バックエンドの実装をフロントエンドよりも先に行い、クライアントが部門間の行き来を最小限に抑えてAPIへのクエリをすぐに開始できるようにすることです。しかし、私たちは優先順位が必ずしも一致しないことを認識しています。イテレーションやコミットした仕事を提供するためには、フロントエンドをバックエンドより先に実装する必要があるかもしれません。
フロントエンドのクエリと変異をバックエンドより先に実装する場合
このような場合、フロントエンドはバックエンドのリゾルバにまだ対応していないGraphQLスキーマやフィールドを定義します。これは、実装が適切に機能フラグが付けられている限り問題なく、製品の公開エラーにつながることはありません。ただし、graphql-verify
CIジョブでバックエンドのGraphQLスキーマに対するクライアントサイドのクエリ/変異を検証します。バックエンドが実際にサポートする前にマージする場合は、変更がバリデーションに合格していることを確認する必要があります。以下はそのためのいくつかの提案です。
@client
ディレクティブの使用
バックエンドでまだサポートされていない新しいクエリ、変異、フィールドには@client
ディレクティブを使用するのが望ましい方法です。このディレクティブを持つエンティティはgraphql-verify
バリデーションジョブによってスキップされます。
さらに、Apolloはクライアント側で解決を試みます。これは、ローカルのApolloキャッシュを使用したAPI応答の模擬と組み合わせて使用することができます。これは、クライアント側で定義された偽のデータで機能をテストする便利な方法を提供します。あなたの変更に対してマージリクエストを出す場合、レビュアーが自分のGDKで適用できるパッチとしてローカルリゾルバを提供し、あなたの作業を簡単にスモークテストできるようにするのは良い考えです。
ディレクティブの削除をフォローアップイシューで追跡するか、バックエンドの実装計画の一部として追跡するようにしてください。
既知の失敗のリストへの例外の追加
GraphQLクエリ/変異の検証は、.eslintignore
ファイルを使用して一部のファイルに対してESLintを無効にするのと同じように、config/known_invalid_graphql_queries.yml
ファイルにパスを追加することで、特定のファイルに対して完全にオフにすることができます。ここにリストされたファイルはバリデーションされません。既存のクエリにフィールドを追加するだけであれば、@client
ディレクティブを使用してください。
繰り返しますが、適切なイシューでオーバーライドの削除を追跡することで、オーバーライドが可能な限り短命であることを確認してください。
機能フラグ付きクエリ
バックエンドが完成していて、フロントエンドが機能フラグの後ろに実装されている場合、GraphQLクエリで機能フラグを活用するためにいくつかのオプションが利用可能です。
@include
ディレクティブ
@include
(またはその反対の@skip
) は、エンティティをクエリに含めるかどうかを制御するために使用できます。@include
ディレクティブがfalse
と評価された場合、エンティティのリゾルバはヒットせず、そのエンティティはレスポンスから除外されます。例えば
query getAuthorData($authorNameEnabled: Boolean = false) {
username
name @include(if: $authorNameEnabled)
}
次に、クエリのVue(またはJavaScript)呼び出しで、機能フラグを渡すことができます。この機能フラグは、すでに正しく設定されている必要があります。正しい方法については、機能フラグのドキュメントを参照してください。
export default {
apollo: {
user: {
query: QUERY_IMPORT,
variables() {
return {
authorNameEnabled: gon?.features?.authorNameEnabled,
};
},
}
},
};
ディレクティブがfalse
と評価された場合でも、ガードされたエンティティはバックエンドに送信され、GraphQL スキーマと照合されることに注意してください。そのため、このアプローチでは、機能フラグが無効になっている場合でも、機能フラグが設定されたエンティティがスキーマに存在する必要があります。機能フラグがオフになっている場合、リゾルバは少なくともフロントエンドと同じ機能フラグを使用してnull
を返すことが推奨されます。API GraphQLガイドを参照してください。
異なるバージョンのクエリ
標準クエリを複製するアプローチもありますが、これは避けるべきです。コピーには新しいエンティティが含まれ、元のエンティティは変更されません。機能フラグの状態に基づいて適切なクエリをトリガするかどうかは、プロダクション・コード次第です。例えば
export default {
apollo: {
user: {
query() {
return this.glFeatures.authorNameEnabled ? NEW_QUERY : ORIGINAL_QUERY,
}
}
},
};
複数バージョンのクエリの回避
複数バージョンのアプローチは、マージリクエストが大きくなり、機能フラグが存在する限り2つの類似したクエリをメンテナーする必要があるため、推奨されません。複数のバージョンを使用できるのは、新しい GraphQL エンティティがまだスキーマの一部でない場合や、スキーマレベルで機能フラグが設定されている場合です (new_entity: :feature_flag
)。
手動によるクエリのトリガー
コンポーネントのapollo
プロパティに対するクエリは、コンポーネントの作成時に自動的に実行されます。コンポーネントによっては、オンデマンドでネットワーク要求が行われるものもあります。
これには2つの方法があります:
-
skip
。
export default {
apollo: {
user: {
query: QUERY_IMPORT,
skip() {
// only make the query when dropdown is open
return !this.isOpen;
},
}
},
};
- テストの拡張
addSmartQuery
スマートクエリを手動で作成することができます。
handleClick() {
this.$apollo.addSmartQuery('user', {
// this takes the same values as you'd have in the `apollo` section
query: QUERY_IMPORT,
}),
};
ページネーションの操作
GitLab GraphQL API は、接続タイプに対してRelay スタイルのカーソルによるページ分割を使用します。つまり、”カーソル” を使ってデータセットのどこから次の項目を取得すべきかを追跡します。GraphQL Ruby Connection Conceptsは接続の良い概要と入門書です。
すべての接続タイプ(たとえば、DesignConnection
やDiscussionConnection
)には、ページネーションに必要な情報を含むフィールドpageInfo
があります:
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
ここに
-
startCursor
は最初の項目のカーソルを表示し、endCursor
は最後の項目のカーソルを表示します。 -
hasPreviousPage
とhasNextPage
、現在のページの前後に利用可能なページがあるかどうかをチェックすることができます。
接続タイプでデータをフェッチするとき、カーソルをafter
またはbefore
パラメータとして渡すことができます。これらのパラメータに続けて、first
またはlast
パラメータを渡すことで、指定したエンドポイントの後または前にフェッチするアイテムの_数を_指定することができます。
例えば、ここではカーソル(これをprojectQuery
)の後に10個のデザインをフェッチしています:
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query {
project(fullPath: "root/my-project") {
id
issue(iid: "42") {
designCollection {
designs(atVersion: null, after: "Ihwffmde0i", first: 10) {
edges {
node {
id
}
}
pageInfo {
...PageInfo
}
}
}
}
}
}
pageInfo
の情報を入力するためにpage_info.fragment.graphql
を使用していることに注意してください。
コンポーネントでfetchMore
メソッドを使用します。
このアプローチは、ユーザーハンドルのページネーションで使用するのが理にかなっています。たとえば、より多くのデータを取得するためにスクロールする場合や、明示的に「次のページ」ボタンをクリックする場合などです。最初にすべてのデータを取得する必要がある場合は、代わりに (スマートではない) クエリを使用することをお勧めします。
最初にデータを取得する場合、通常は最初からページ分割を開始します。この場合、次のいずれかを実行します:
- カーソルを渡すのをスキップします。
-
null
を明示的にafter
に渡します。
データが取得された後、update
-hookをきっかけとして、Vueコンポーネントのプロパティに設定されているデータをカスタマイズすることができます。これにより、他のデータの中からpageInfo
オブジェクトを取得することができます。
result
-hook では、pageInfo
オブジェクトを検査して、次のページを取得する必要があるかどうかを確認できます。アプリケーションが無限に次のページを要求し続けないように、requestCount
も保持していることに注意してください:
data() {
return {
pageInfo: null,
requestCount: 0,
}
},
apollo: {
designs: {
query: projectQuery,
variables() {
return {
// ... The rest of the design variables
first: 10,
};
},
update(data) {
const { id = null, issue = {} } = data.project || {};
const { edges = [], pageInfo } = issue.designCollection?.designs || {};
return {
id,
edges,
pageInfo,
};
},
result() {
const { pageInfo } = this.designs;
// Increment the request count with each new result
this.requestCount += 1;
// Only fetch next page if we have more requests and there is a next page to fetch
if (this.requestCount < MAX_REQUEST_COUNT && pageInfo?.hasNextPage) {
this.fetchNextPage(pageInfo.endCursor);
}
},
},
},
次のページに移動したい場合は、ApollofetchMore
メソッドを使用し、新しいカーソル(オプションで新しい変数)を渡します。
fetchNextPage(endCursor) {
this.$apollo.queries.designs.fetchMore({
variables: {
// ... The rest of the design variables
first: 10,
after: endCursor,
},
});
}
フィールドマージポリシーの定義
既存の結果と入力結果をマージする方法を指定するフィールドポリシーを定義する必要もあります。たとえば、Previous/Next
ボタンがある場合、既存の結果を入力結果と置き換えることは理にかなっています:
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
typePolicies: {
DesignCollection: {
fields: {
designs: {
merge(existing, incoming) {
if (!incoming) return existing;
if (!existing) return incoming;
// We want to save only incoming nodes and replace existing ones
return incoming
}
}
}
}
}
},
},
),
});
無限スクロールの場合、designs
ノードを既存のものに追加します。この場合、マージ関数は少し異なります:
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
typePolicies: {
DesignCollection: {
fields: {
designs: {
merge(existing, incoming) {
if (!incoming) return existing;
if (!existing) return incoming;
const { nodes, ...rest } = incoming;
// We only need to merge the nodes array.
// The rest of the fields (pagination) should always be overwritten by incoming
let result = rest;
result.nodes = [...existing.nodes, ...nodes];
return result;
}
}
}
}
}
},
},
),
});
apollo-client
はページ分割されたクエリで使用されるいくつかのフィールドポリシーを提供します。concatPagination
ポリシーで無限スクロールのページ分割を実現する別の方法を紹介します:
import { concatPagination } from '@apollo/client/utilities';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
typePolicies: {
Project: {
fields: {
dastSiteProfiles: {
keyArgs: ['fullPath'], // You might need to set the keyArgs option to enforce the cache's integrity
},
},
},
DastSiteProfileConnection: {
fields: {
nodes: concatPagination(),
},
},
},
},
},
),
});
新しいページの結果が前のページに追加されるので、これは上記のDesignCollection
の例と似ています。
場合によっては、keyArgs
全てのフィールドが更新されるため、フィールドの keyArgs
正しい定義が難しいことがあります。この場合、false
にkeyArgs
設定 keyArgs
します。 これにより、Apollo Clientは自動マージは行わず、merge
関数に入れたロジックに完全に依存するようになります。
例えば、次のようなクエリがあるとします:
query searchGroupsWhereUserCanTransfer {
currentUser {
id
groups(after: 'somecursor') {
nodes {
id
fullName
}
pageInfo {
...PageInfo
}
}
}
}
ここでは、groups
フィールドには、keyArgs
の良い候補がありません :after
引数は、後続のページをリクエストする際に変更されるため、考慮したくありません。keyArgs
をfalse
に設定することで、更新は意図したとおりに動作します:
typePolicies: {
UserCore: {
fields: {
groups: {
keyArgs: false,
},
},
},
GroupConnection: {
fields: {
nodes: concatPagination(),
},
},
}
コンポーネントでの再帰クエリの使用
ページ分割されたすべてのデータを最初に取得する必要がある場合、Apollo クエリがその役割を果たします。ユーザーインタラクションに基づいて次のページを取得する必要がある場合は、fetchMore
-hookとともにsmartQuery
を使用することをお勧めします。
クエリが解決したら、コンポーネントデータを更新し、pageInfo
オブジェクトを検査することができます。これにより、再帰的にメソッドを呼び出して、次のページをフェッチする必要があるかどうかを確認できます。
アプリケーションが無限に次のページをリクエストし続けないように、requestCount
も保持していることに注意してください。
data() {
return {
requestCount: 0,
isLoading: false,
designs: {
edges: [],
pageInfo: null,
},
}
},
created() {
this.fetchDesigns();
},
methods: {
handleError(error) {
this.isLoading = false;
// Do something with `error`
},
fetchDesigns(endCursor) {
this.isLoading = true;
return this.$apollo
.query({
query: projectQuery,
variables() {
return {
// ... The rest of the design variables
first: 10,
endCursor,
};
},
})
.then(({ data }) => {
const { id = null, issue = {} } = data.project || {};
const { edges = [], pageInfo } = issue.designCollection?.designs || {};
// Update data
this.designs = {
id,
edges: [...this.designs.edges, ...edges];
pageInfo: pageInfo;
};
// Increment the request count with each new result
this.requestCount += 1;
// Only fetch next page if we have more requests and there is a next page to fetch
if (this.requestCount < MAX_REQUEST_COUNT && pageInfo?.hasNextPage) {
this.fetchDesigns(pageInfo.endCursor);
} else {
this.isLoading = false;
}
})
.catch(this.handleError);
},
},
ページネーションと楽観的更新
Apolloは、ページ分割されたデータをクライアントサイドでキャッシュする際、pageInfo
キャッシュキーに変数を pageInfo
含めます。pageInfo
そのデータを楽観的に更新したい pageInfo
場合は、.readQuery()
や.writeQuery()
を使ってキャッシュとやりとりする際に変数をpageInfo
指定する必要が pageInfo
あります。 これは面倒で、直感的ではありません。
キャッシュされたページ分割クエリを扱いやすくするために、Apolloは@connection
ディレクティブを提供しています。このディレクティブは、データをキャッシュする際に静的キーとして使用されるkey
パラメータを受け付けます。これにより、ページネーション固有の変数を与えることなくデータを取得できるようになります。
以下に@connection
ディレクティブを使用したクエリの例を示します:
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query DastSiteProfiles($fullPath: ID!, $after: String, $before: String, $first: Int, $last: Int) {
project(fullPath: $fullPath) {
siteProfiles: dastSiteProfiles(after: $after, before: $before, first: $first, last: $last)
@connection(key: "dastSiteProfiles") {
pageInfo {
...PageInfo
}
edges {
cursor
node {
id
# ...
}
}
}
}
}
この例では、Apollo は安定したdastSiteProfiles
キャッシュキーでデータを保存します。
そのデータをキャッシュから取り出すには、after
やbefore
のようなページネーション固有の変数を省略し、$fullPath
変数だけを指定すればよいことになります:
const data = store.readQuery({
query: dastSiteProfilesQuery,
variables: {
fullPath: 'namespace/project',
},
});
@connection
ディレクティブについてはApollo のドキュメントを参照してください。
パフォーマンスの管理
Apolloクライアントはデフォルトでクエリをバッチします。3つの遅延クエリが与えられると、Apolloはそれらを1つのリクエストにグループ化し、1つのリクエストをサーバーに送信し、3つのクエリすべてが完了した後に応答します。
クエリを個別のリクエストとして送信する必要がある場合は、追加のコンテキストを提供することで、Apolloにそのように指示することができます。
export default {
apollo: {
user: {
query: QUERY_IMPORT,
context: {
isSingleRequest: true,
}
}
},
};
ポーリングとパフォーマンス
Apolloクライアントは単純なポーリングをサポートしていますが、パフォーマンス上の理由から、毎回データベースをヒットするよりも、ETagベースのキャッシュを優先しています。
ETagリソースがバックエンドからキャッシュされるように設定された後、フロントエンドで行ういくつかの変更があります。
まず、バックエンドからETagリソースを取得します。ETagリソースはURLパスの形をしているはずです。パイプライングラフの例では、これはgraphql_resource_etag
と呼ばれ、Apollo コンテキストに追加する新しいヘッダーを作成するために使用されます:
/* pipelines/components/graph/utils.js */
/* eslint-disable @gitlab/require-i18n-strings */
const getQueryHeaders = (etagResource) => {
return {
fetchOptions: {
method: 'GET',
},
headers: {
/* This will depend on your feature */
'X-GITLAB-GRAPHQL-FEATURE-CORRELATION': 'verify/ci/pipeline-graph',
'X-GITLAB-GRAPHQL-RESOURCE-ETAG': etagResource,
'X-REQUESTED-WITH': 'XMLHttpRequest',
},
};
};
/* eslint-enable @gitlab/require-i18n-strings */
/* component.vue */
apollo: {
pipeline: {
context() {
return getQueryHeaders(this.graphqlResourceEtag);
},
query: getPipelineDetails,
pollInterval: 10000,
..
},
},
ここで、apolloクエリはgraphqlResourceEtag
の変更を監視しています。 ETagリソースが動的に変更される場合は、クエリヘッダーで送信するリソースも更新されるようにする必要があります。そのためには、ETagリソースをローカルキャッシュに動的に保存して更新します。
パイプラインエディタのパイプラインステータスでこの例を見ることができます。パイプラインエディタは最新のパイプラインの変更を監視します。ユーザーが新しいコミットを作成すると、パイプラインクエリを更新して新しいパイプラインの変更をポーリングします。
# pipeline_etag.query.graphql
query getPipelineEtag {
pipelineEtag @client
}
/* pipeline_editor/components/header/pipeline_status.vue */
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
apollo: {
pipelineEtag: {
query: getPipelineEtag,
},
pipeline: {
context() {
return getQueryHeaders(this.pipelineEtag);
},
query: getPipelineQuery,
pollInterval: POLL_INTERVAL,
},
}
/* pipeline_editor/components/commit/commit_section.vue */
await this.$apollo.mutate({
mutation: commitCIFile,
update(store, { data }) {
const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
if (pipelineEtag) {
store.writeQuery({ query: getPipelineEtag, data: { pipelineEtag } });
}
},
});
ETags は、GraphQL の通常のPOST
ではなくGET
であるリクエストに依存します。デフォルトのリンクライブラリはGET
リクエストをサポートしていないため、デフォルトのApolloクライアントに別のライブラリを使用するように知らせる必要があります。これは、アプリがクエリをバッチ処理できないことを意味します。
/* componentMountIndex.js */
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
useGet: true,
},
),
});
最後に、ブラウザのタブがアクティビティでない場合に、コンポーネントがポーリングを一時停止するように、可視性チェックを追加します。これにより、ページへのリクエスト負荷が軽減されるはずです。
/* component.vue */
import { toggleQueryPollingByVisibility } from '~/pipelines/components/graph/utils';
export default {
mounted() {
toggleQueryPollingByVisibility(this.$apollo.queries.pipeline, POLL_INTERVAL);
},
};
フロントエンドでETagキャッシュを完全に実装する方法については、このMRを参考にしてください。
サブスクリプションが成熟すれば、このプロセスはサブスクリプションを使用することで置き換えることができ、別のリンクライブラリを削除してクエリのバッチ処理に戻すことができます。
ETagキャッシュのテスト方法
ネットワークタブでリクエストをチェックすることで、実装が動作することをテストできます。ETag リソースに変更がなければ、ポーリングされたリクエストはすべて変更されるはずです:
-
POST
リクエストの代わりにGET
リクエストにしてください。 -
200
の代わりに304
の HTTP ステータスを持ちます。
テストの際には、開発者ツールでキャッシュが無効になっていないことを確認してください。
Chromeを使用していて、200
HTTPステータスコードが表示され続ける場合、このバグの可能性があります:開発者ツールは 304 ではなく 200 を表示します。この場合、レスポンスヘッダのソースを調べ、リクエストが実際にキャッシュされ、304
ステータスコードが返されたことを確認してください。
サブスクリプション
Webソケット経由でGraphQL APIからリアルタイムの更新を受信するためにサブスクリプションを使用します。現在、既存のサブスクリプションの数には限りがあり、GraphqiQLエクスプローラーで利用可能なサブスクリプションのリストを確認できます。
注:GraphiQLを使用してサブスクリプションをテストすることはできません。なぜなら、サブスクリプションにはActionCableクライアントが必要であり、GraphiQLは現在サポートしていないからです。
サブスクリプションの包括的な紹介については、リアルタイム ウィジェット開発者ガイドを参照してください。
ベストプラクティス
突然変異でupdate
フックを使う(使わない)場合
Apollo Clientの.mutate()
メソッドは、変異のライフサイクル中に2回呼び出されるupdate
フックを公開しています:
- 最初に1回。つまり、変異が完了する前です。
- 突然変異が完了した後に1回。
このフックは、ストア(つまりApolloCache)からアイテムを追加または削除する場合にのみ使用してください。既存のアイテムを_更新_する場合は、通常、グローバルなid
.
この場合、変異クエリ定義にこのid
があると、ストアは自動的に更新されます。以下は、id
が存在する典型的な変異クエリの例です:
mutation issueSetWeight($input: IssueSetWeightInput!) {
issuableSetWeight: issueSetWeight(input: $input) {
issuable: issue {
id
weight
}
errors
}
}
テスト
GraphQLスキーマの生成
いくつかのテストはスキーマのJSONファイルを読み込みます。これらのファイルを生成するには、以下を実行します:
bundle exec rake gitlab:graphql:schema:dump
このタスクは、アップストリームからプルした後か、ブランチをリベースするときに実行します。このタスクはgdk update
の内部で自動的に実行されます。
tmp
ディレクトリを “Excluded “としてマークしている場合は、gitlab/tmp/tests/graphql
に対して “Mark Directory As -> Not Excluded “を実行してください。これにより、JS GraphQLプラグインが自動的にスキーマを検索し、インデックスを作成します。Apolloクライアントのモッキング
Apolloオペレーションを持つコンポーネントをテストするには、ユニットテストでApolloクライアントをモックする必要があります。Apolloクライアントをモックするためにmock-apollo-client
ライブラリを使用し、その上に作成したcreateMockApollo
ヘルパー 。
Vue.use(VueApollo)
を呼び出して、VueApollo
をVueインスタンスに注入する必要があります。これにより、ファイル内のすべてのテストに対してVueApollo
がグローバルにインストールされます。インポートの直後にVue.use(VueApollo)
を呼び出すことをお勧めします。
import VueApollo from 'vue-apollo';
import Vue from 'vue';
Vue.use(VueApollo);
describe('Some component with Apollo mock', () => {
let wrapper;
function createComponent(options = {}) {
wrapper = shallowMount(...);
}
})
この後、モックされたApolloプロバイダを作成する必要があります:
import createMockApollo from 'helpers/mock_apollo_helper';
describe('Some component with Apollo mock', () => {
let wrapper;
let mockApollo;
function createComponent(options = {}) {
mockApollo = createMockApollo(...)
wrapper = shallowMount(SomeComponent, {
apolloProvider: mockApollo
});
}
afterEach(() => {
// we need to ensure we don't have provider persisted between tests
mockApollo = null
})
})
次に、クエリや変異ごとに_ハンドラの_配列を定義する必要があります。ハンドラは、正しいクエリのレスポンスかエラーを返すモック関数でなければなりません:
import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
describe('Some component with Apollo mock', () => {
let wrapper;
let mockApollo;
function createComponent(options = {
designListHandler: jest.fn().mockResolvedValue(designListQueryResponse)
}) {
mockApollo = createMockApollo([
[getDesignListQuery, options.designListHandler],
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
[moveDesignMutation, jest.fn().mockResolvedValue(moveDesignMutationResponse)],
])
wrapper = shallowMount(SomeComponent, {
apolloProvider: mockApollo
});
}
})
解決された値をモックする場合、レスポンスの構造が実際のAPIレスポンスと同じであることを確認してください。例えば、ルート・プロパティはdata
であるべきです:
const designListQueryResponse = {
data: {
project: {
id: '1',
issue: {
id: 'issue-1',
designCollection: {
copyState: 'READY',
designs: {
nodes: [
{
id: '3',
event: 'NONE',
filename: 'fox_3.jpg',
notesCount: 1,
image: 'image-3',
imageV432x230: 'image-3',
currentUserTodos: {
nodes: [],
},
},
],
},
versions: {
nodes: [],
},
},
},
},
},
};
クエリをテストする場合、クエリはプロミスなので、結果をレンダリングするために_解決する_必要があることに注意してください。解決せずに、クエリのloading
状態を確認することができます:
it('renders a loading state', () => {
const wrapper = createComponent();
expect(wrapper.findComponent(LoadingSpinner).exists()).toBe(true)
});
it('renders designs list', async () => {
const wrapper = createComponent();
await waitForPromises()
expect(findDesigns()).toHaveLength(3);
});
クエリのエラーをテストする必要がある場合は、拒否された値をリクエストハンドラとしてモックする必要があります:
it('renders error if query fails', async () => {
const wrapper = createComponent({
designListHandler: jest.fn.mockRejectedValue('Houston, we have a problem!')
});
await waitForPromises()
expect(wrapper.find('.test-error').exists()).toBe(true)
})
変異も同じようにテストできます:
const moveDesignHandlerSuccess = jest.fn().mockResolvedValue(moveDesignMutationResponse)
function createComponent(options = {
designListHandler: jest.fn().mockResolvedValue(designListQueryResponse),
moveDesignHandler: moveDesignHandlerSuccess
}) {
mockApollo = createMockApollo([
[getDesignListQuery, options.designListHandler],
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
[moveDesignMutation, moveDesignHandler],
])
wrapper = shallowMount(SomeComponent, {
apolloProvider: mockApollo
});
}
it('calls a mutation with correct parameters and reorders designs', async () => {
const wrapper = createComponent();
wrapper.find(VueDraggable).vm.$emit('change', {
moved: {
newIndex: 0,
element: designToMove,
},
});
expect(moveDesignHandlerSuccess).toHaveBeenCalled();
await waitForPromises();
expect(
findDesigns()
.at(0)
.props('id'),
).toBe('2');
});
複数のクエリレスポンス状態、成功と失敗をモックするために、Apollo Clientのネイティブの再試行動作は、Jestのモック関数と組み合わせて、一連のレスポンスを作成することができます。これらは手動で進める必要はありませんが、特定の方法で待機させる必要があります。
describe('when query times out', () => {
const advanceApolloTimers = async () => {
jest.runOnlyPendingTimers();
await waitForPromises()
};
beforeEach(async () => {
const failSucceedFail = jest
.fn()
.mockResolvedValueOnce({ errors: [{ message: 'timeout' }] })
.mockResolvedValueOnce(mockPipelineResponse)
.mockResolvedValueOnce({ errors: [{ message: 'timeout' }] });
createComponentWithApollo(failSucceedFail);
await waitForPromises();
});
it('shows correct errors and does not overwrite populated data when data is empty', async () => {
/* fails at first, shows error, no data yet */
expect(getAlert().exists()).toBe(true);
expect(getGraph().exists()).toBe(false);
/* succeeds, clears error, shows graph */
await advanceApolloTimers();
expect(getAlert().exists()).toBe(false);
expect(getGraph().exists()).toBe(true);
/* fails again, alert returns but data persists */
await advanceApolloTimers();
expect(getAlert().exists()).toBe(true);
expect(getGraph().exists()).toBe(true);
});
});
以前は、Apolloの機能をテストするためにmount
で{ mocks: { $apollo ...}}
を使っていました。このアプローチは推奨されません。適切な$apollo
モッキングは、多くの実装の詳細をテストに漏らします。モックされたApolloプロバイダに置き換えることを検討してください。
wrapper = mount(SomeComponent, {
mocks: {
// avoid! Mock real graphql queries and mutations instead
$apollo: {
mutate: jest.fn(),
queries: {
groups: {
loading,
},
},
},
},
});
サブスクリプションのテスト
サブスクリプションをテストする際には、vue-apollo@4
におけるサブスクリプションのデフォルトの挙動は、再サブスクライブし、エラー時に新しいリクエストを即座に発行することであることに注意しましょう (skip
の値で制限されている場合を除く)。
import waitForPromises from 'helpers/wait_for_promises';
// subscriptionMock is registered as handler function for subscription
// in our helper
const subcriptionMock = jest.fn().mockResolvedValue(okResponse);
// ...
it('testing error state', () => {
// Avoid: will stuck below!
subscriptionMock = jest.fn().mockRejectedValue({ errors: [] });
// component calls subscription mock as part of
createComponent();
// will be stuck forever:
// * rejected promise will trigger resubscription
// * re-subscription will call subscriptionMock again, resulting in rejected promise
// * rejected promise will trigger next re-subscription,
await waitForPromises();
// ...
})
vue@3
とvue-apollo@4
を使用する際に、このような内部ループを避けるために、ワンタイム拒否の使用を検討してください。
it('testing failure', () => {
// OK: subscription will fail once
subscriptionMock.mockRejectedValueOnce({ errors: [] });
// component calls subscription mock as part of
createComponent();
await waitForPromises();
// code below now will be executred
})
@client
クエリのテスト
モックリゾルバの使用
アプリケーションに@client
クエリが含まれている場合、ハンドラのみを渡すと次のような Apollo Client の警告が表示されます:
Unexpected call of console.warn() with:
Warning: mock-apollo-client - The query is entirely client-side (using @client directives) and resolvers have been configured. The request handler will not be called.
これを解決するには、モックhandlers
ではなく、モックresolvers
を定義する必要があります。例えば、次のような@client
クエリがあるとします:
query getBlobContent($path: String, $ref: String!) {
blobContent(path: $path, ref: $ref) @client {
rawData
}
}
そして、実際のクライアント側リゾルバは
import Api from '~/api';
export const resolvers = {
Query: {
blobContent(_, { path, ref }) {
return {
__typename: 'BlobContent',
rawData: Api.getRawFile(path, { ref }).then(({ data }) => {
return data;
}),
};
},
},
};
export default resolvers;
同じ形のデータを返すモック・リゾルバを使い、その結果をモック関数でモックすればよいのです:
let mockApollo;
let mockBlobContentData; // mock function, jest.fn();
const mockResolvers = {
Query: {
blobContent() {
return {
__typename: 'BlobContent',
rawData: mockBlobContentData(), // the mock function can resolve mock data
};
},
},
};
const createComponentWithApollo = ({ props = {} } = {}) => {
mockApollo = createMockApollo([], mockResolvers); // resolvers are the second parameter
wrapper = shallowMount(MyComponent, {
propsData: {},
apolloProvider: mockApollo,
// ...
})
};
その後、必要な値を解決したり拒否したりすることができます。
beforeEach(() => {
mockBlobContentData = jest.fn();
});
it('shows data', async() => {
mockBlobContentData.mockResolvedValue(data); // you may resolve or reject to mock the result
createComponentWithApollo();
await waitForPromises(); // wait on the resolver mock to execute
expect(findContent().text()).toBe(mockCiYml);
});
テストの拡張cache.writeQuery
ローカルクエリのresult
フックをテストしたいことがあります。これをトリガーさせるには、このクエリでフェッチされる正しいデータをキャッシュに投入する必要があります:
query fetchLocalUser {
fetchLocalUser @client {
name
}
}
import fetchLocalUserQuery from '~/design_management/graphql/queries/fetch_local_user.query.graphql';
describe('Some component with Apollo mock', () => {
let wrapper;
let mockApollo;
function createComponent(options = {
designListHandler: jest.fn().mockResolvedValue(designListQueryResponse)
}) {
mockApollo = createMockApollo([...])
mockApollo.clients.defaultClient.cache.writeQuery({
query: fetchLocalUserQuery,
data: {
fetchLocalUser: {
__typename: 'User',
name: 'Test',
},
},
});
wrapper = shallowMount(SomeComponent, {
apolloProvider: mockApollo
});
}
})
モックされたアポロクライアントのキャッシュ動作を設定する必要がある場合は、モックされたクライアントのインスタンスを作成するときに追加のキャッシュオプションを指定します:
const defaultCacheOptions = {
fragmentMatcher: { match: () => true },
addTypename: false,
};
mockApollo = createMockApollo(
requestHandlers,
{},
{
dataIdFromObject: (object) =>
// eslint-disable-next-line no-underscore-dangle
object.__typename === 'Requirement' ? object.iid : defaultDataIdFromObject(object),
},
);
エラーの処理
GitLab GraphQL変異には2つの異なるエラーモードがあります:トップレベルとerrors-as-dataです。
GraphQL変異を利用する際には、エラーが発生したときにユーザーが適切なフィードバックを受け取れるように、これら両方のエラーモードを処理することを検討してください。
トップレベルのエラー
これらのエラーは GraphQL レスポンスの「トップレベル」に位置します。これらは引数エラーや構文エラーを含む回復不可能なエラーであり、ユーザーに直接表示されるべきではありません。
トップレベルエラーの処理
Apolloはトップレベルのエラーを認識しているため、Apolloのさまざまなエラー処理メカニズムを活用してこれらのエラーを処理することができます。たとえば、mutate
メソッドを呼び出した後の Promise の拒否処理や、ApolloMutation
コンポーネントから発せられるerror
イベントの処理などです。
これらのエラーはユーザー向けではないため、トップレベルのエラーメッセージはクライアント側で定義する必要があります。
データとしてのエラー
これらのエラーは、GraphQL レスポンスのdata
オブジェクトの内部にネストされます。これらは回復可能なエラーであり、理想的にはユーザーに直接提示することができます。
データとしてのエラーの処理
まず、errors
を変異オブジェクトに追加しなければなりません:
mutation createNoteMutation($input: String!) {
createNoteMutation(input: $input) {
note {
id
+ errors
}
}
これで、この変異をコミットしてエラーが発生したときに、レスポンスにerrors
が含まれるようになります:
{
data: {
mutationName: {
errors: ["Sorry, we were not able to update the note."]
}
}
}
エラーをデータとして扱う場合、レスポンスに含まれるエラーメッセージをユーザーに表示するか、クライアントサイドで定義された別のメッセージをユーザーに表示するかは、最善の判断で決めてください。
Vue以外での使用
クエリ付きのデフォルトクライアントを直接インポートして使用することで、Vueの外部でGraphQLを使用することも可能です。
import createDefaultClient from '~/lib/graphql';
import query from './query.graphql';
const defaultClient = createDefaultClient();
defaultClient.query({ query })
.then(result => console.log(result));
Vuexを使用する場合は、キャッシュを無効にします:
- データが他の場所にキャッシュされている場合
- データが他の場所でキャッシュされている場合、あるいは指定されたユースケースでキャッシュの必要がない場合、ユースケースはキャッシュを必要としません。
import createDefaultClient from '~/lib/graphql';
import fetchPolicies from '~/graphql_shared/fetch_policy_constants';
const defaultClient = createDefaultClient(
{},
{
fetchPolicy: fetchPolicies.NO_CACHE,
},
);
GraphQLスタートアップコールによる初期クエリの作成
パフォーマンスを向上させるために、初期のGraphQLクエリを早期に作成したい場合があります。これを行うには、以下の手順でスタートアップ呼び出しに追加します:
- アプリケーションで最初に必要なクエリをすべて
app/graphql/queries
に移動します; -
ネストされたクエリ・レベルごとに
__typename
プロパティを追加します:query getPermissions($projectPath: ID!) { project(fullPath: $projectPath) { __typename userPermissions { __typename pushCode forkProject createMergeRequestIn } } }
-
クエリにフラグメントが含まれる場合、フラグメントをインポートするのではなく、クエリ・ファイルに直接移動する必要があります:
fragment PageInfo on PageInfo { __typename hasNextPage hasPreviousPage startCursor endCursor } query getFiles( $projectPath: ID! $path: String $ref: String! ) { project(fullPath: $projectPath) { __typename repository { __typename tree(path: $path, ref: $ref) { __typename pageInfo { ...PageInfo } } } } } }
-
フラグメントが一度しか使用されない場合は、フラグメントを完全に削除することもできます:
query getFiles( $projectPath: ID! $path: String $ref: String! ) { project(fullPath: $projectPath) { __typename repository { __typename tree(path: $path, ref: $ref) { __typename pageInfo { __typename hasNextPage hasPreviousPage startCursor endCursor } } } } } }
- アプリケーションのビューとして機能する HAML ファイルに、正しい変数で起動コールを追加します。GraphQLスタートアップコールを追加するには、
add_page_startup_graphql_call
ヘルパーを使用します。最初のパラメーターはクエリへのパスで、2番目のパラメーターはクエリ変数を含むオブジェクトです。クエリへのパスはapp/graphql/queries
フォルダからの相対パスです。例えば、app/graphql/queries/repository/files.query.graphql
クエリが必要な場合、パスはrepository/files
となります。
トラブルシューティング
モックされたクライアントがモック応答の代わりに空のオブジェクトを返します
モックデータの代わりに空のオブジェクトがレスポンスに含まれるためにユニットテストが失敗する場合は、__typename
フィールドをモックレスポンスに追加します。
あるいは、GraphQLクエリフィクスチャは生成時に自動的に__typename
。
キャッシュデータの損失に関する警告
コンソールに警告が表示されることがあります:Cache data may be lost when replacing the someProperty field of a Query object. To address this problem, either ensure all objects of SomeEntityhave an id or a custom merge function
。複数のクエリに関するセクションをチェックして、イシューを解決してください。
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- add_page_startup_graphql_call('repository/permissions', { projectPath: @project.full_path })
- add_page_startup_graphql_call('repository/files', { nextPageCursor: "", pageSize: 100, projectPath: @project.full_path, ref: current_ref, path: current_route_path || "/"})