GraphQL

はじめに

リソース

一般的なリソース

GitLabでのGraphQL

ライブラリ

フロントエンド開発にGraphQLを使用する場合、Apollo(特にApollo Client)とVue Apolloを使用します。

VueアプリケーションでGraphQLを使用する場合、VueApolloのインテグレーション方法を学ぶには、Vueでの使用法のセクションが役立ちます。

その他の使用例については、「Vue以外での使用」セクションをご覧ください。

イミュータブルなキャッシュの更新にはImmerを使用します。詳細については、イミュータビリティとキャッシュの更新を参照してください。

ツール

Apollo GraphQL VSコード拡張

VS Codeを使用している場合、Apollo GraphQL拡張機能は、.graphql ファイル内のオートコンプリートをサポートします。GraphQL拡張機能をセットアップするには、以下の手順に従ってください:

  1. スキーマを生成します:bundle exec rake gitlab:graphql:schema:dump
  2. gitlab ローカル・ディレクトリのルートにapollo.config.js ファイルを追加します。
  3. そのファイルに以下の内容を入力してください:

    module.exports = {
      client: {
        includes: ['./app/assets/javascripts/**/*.graphql', './ee/app/assets/javascripts/**/*.graphql'],
        service: {
          name: 'GitLab',
          localSchemaFile: './tmp/tests/graphql/gitlab_schema.graphql',
        },
      },
    };
    
  4. VSコードを再起動します。

GraphQL API の探索

私たちのGraphQL APIは、インスタンスの/-/graphql-explorer 、またはGitLab.comでGraphiQLを介して探索することができます。必要に応じてGitLab GraphQL API Reference ドキュメントを参照してください。

すべての既存のクエリと変異を確認するには、GraphiQLの右側でDocumentation explorerを選択します。作成したクエリや変異の実行を確認するには、左上で[Execute query]を選択します。

GraphiQL interface

Apolloクライアント

異なるアプリでクライアントが重複して作成されるのを防ぐため、デフォルトのクライアントを使用するようにしています。これは、正しいURLでApolloクライアントを設定し、CSRFヘッダも設定します。

デフォルトクライアントは2つのパラメータを受け付けます:resolversconfig

  • 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タイプに対して idglobalをクエリする必要があります:

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エンドポイントからidsha を取得します。そして、authorcreatedAt のバージョンプロパティに、ハードコードされた値を割り当てます。このデータにより、フロントエンド開発者はバックエンドにブロックされることなく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つの方法があります:

  1. skip
export default {
  apollo: {
    user: {
      query: QUERY_IMPORT,
      skip() {
        // only make the query when dropdown is open
        return !this.isOpen;
      },
    }
  },
};
  1. テストの拡張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は接続の良い概要と入門書です。

すべての接続タイプ(たとえば、DesignConnectionDiscussionConnection )には、ページネーションに必要な情報を含むフィールドpageInfo があります:

pageInfo {
  endCursor
  hasNextPage
  hasPreviousPage
  startCursor
}

ここに

  • startCursor は最初の項目のカーソルを表示し、endCursor は最後の項目のカーソルを表示します。
  • hasPreviousPagehasNextPage 、現在のページの前後に利用可能なページがあるかどうかをチェックすることができます。

接続タイプでデータをフェッチするとき、カーソルを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正しい定義が難しいことがあります。この場合、falsekeyArgs 設定 keyArgsします。 これにより、Apollo Clientは自動マージは行わず、merge 関数に入れたロジックに完全に依存するようになります。

例えば、次のようなクエリがあるとします:

query searchGroupsWhereUserCanTransfer {
  currentUser {
    id
    groups(after: 'somecursor') {
      nodes {
        id
        fullName
      }
      pageInfo {
        ...PageInfo
      }
    }
  }
}

ここでは、groups フィールドには、keyArgs の良い候補がありません :after 引数は、後続のページをリクエストする際に変更されるため、考慮したくありません。keyArgsfalse に設定することで、更新は意図したとおりに動作します:

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 キャッシュキーでデータを保存します。

そのデータをキャッシュから取り出すには、afterbefore のようなページネーション固有の変数を省略し、$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 の内部で自動的に実行されます。

note
RubyMine IDEを使用していて、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@3vue-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 || "/"})