【Nuxt 3】apollo-upload-clientをNuxtで使用してみた

## 概要
Apollo Clientでファイルアップロードを実現する際、Apolloの[File uploads](https://www.apollographql.com/docs/react/data/file-uploads/)のドキュメントにある通り、[apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client)のように別途ライブラリを使用する方法が挙げられています。ドキュメントではReactでの使用が載せられていますが、今回はこのapollo-upload-clientをNuxtで使用してみたのでそのメモ書きです。

## 前提
* 使用したNuxtのバージョンは`3.13.0`です。
* 使用したNuxt Apolloのバージョンは`5.0.0-alpha.14`です。
* 使用したapollo-upload-clientのバージョンは`18.0.1`です。
* NuxtをTypeScriptで今回使用しましたが、apollo-upload-clientがこの記事を書いた時点（2024年9月）で型に対応してないので、[types/apollo-upload-client](https://www.npmjs.com/package/@types/apollo-upload-client)を別途入れます。バージョンは`18.0.0`を使用しました。
* `graphql-codegen/cli`の`5.0.2`を使用して、schemaからコードを生成します。

## 対応方針
基本的な実装方針は[GraphQL File Uploads Client Side with NuxtJS](https://javascript.plainenglish.io/graphql-file-uploads-client-side-with-nuxtjs-627e7b155f36)にある通りです。pluginsで別途Apollo Clientの設定を実装することができるので、ここでapollo-upload-clientを使用するようにします。

## 設定部分の実装サンプル

まずはcodegenの設定でschemaでUploadを使用できるよう、File型を設定するようにします。以下の通り設定しました。

```typescript:codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: "graphqlSchema/*.graphql",
  documents: ["query/*.ts"],
  ignoreNoDocuments: true, // for better experience with the watcher
  generates: {
    "./gql/": {
      preset: "client",
      presetConfig: {
        fragmentMasking: false,
      },
      config: {
        useTypeImports: true,
        scalars: {
          Upload: "File", // UploadにFile型を割り当てる
        },
      },
    },
  },
};

export default config;
```

次にpluginsの実装です。基本的には上記で紹介した記事の通りですが、TypeScriptの対応などで一部実装を変えてる箇所もあります。
```typescript:plugins/apolloConfig.ts
import { from, ApolloLink } from "@apollo/client/core";
import { onError } from "@apollo/client/link/error";
import { setContext } from "@apollo/client/link/context";
import { provideApolloClient } from "@vue/apollo-composable";
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";

export default defineNuxtPlugin((app) => {
  const envVars = useRuntimeConfig();
  // 型のエラーを出さないために一旦anyにする
  const { $apollo } = useNuxtApp();
  const apollo: any = $apollo;

  const errorLink = onError((err) => {
    app.callHook("vue:error", err, null, "apollo error"); // must be called bc `@nuxtjs/apollo` will not do it anymore
  });

  const authLink = setContext(async (_, { headers }) => {
    const { getToken } = useApollo(); //get you token from Nuxt/Apollo helper
    const token = await getToken();
    return {
      headers: {
        ...headers,
        "Apollo-Require-Preflight": "true",
        Authorization: `Bearer ${token}`,
      },
    };
  });

  // create an customLink as example for an custom manual link
  const customLink = new ApolloLink((operation, forward) => {
    return forward(operation).map((data) => {
      return data;
    });
  });

  // Default httpLink (main communication for apollo) use createUploadLink function from apollo-upload-client
  const httpLink = createUploadLink({
    uri: envVars.public.apiEndpoint, // 環境変数でGraphQLの接続先を設定する
    useGETForQueries: false, // GETは使用しないのでfalseにする
  });

  apollo.defaultClient.setLink(
    from([errorLink, authLink, customLink, httpLink])
  );

  provideApolloClient(apollo.defaultClient);
});
```