15
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NestJS + GraphQL + Apollo Client でファイルのアップロードを実装するには

Last updated at Posted at 2022-02-13

前提

  • Apollo Clientなどを使ってGraphQLのコードを書く場合にリクエストの方法を意識することはあまりないが、実態はcontent-type: application/jsonのPOSTメソッドでbodyにGraphQLのクエリを記述してリクエストしているだけである
  • ファイルのアップロードを行う場合は通常content-type: multipart/form-dataである必要があるため、それに対応する一工夫が必要となる
  • NestJS + GraphQL + Apollo Client の構成での実装例が見つからなかったので備忘録として残しておく

実装方法

NestJS側でやること

npm/yarnでgraphql-uploadを追加

yarn add graphql-upload
yarn add -D @types/graphql-upload

AppModuleのmiddlewareとしてgraphqlUploadExpressを追加

  • これによりmultipart/form-dataのリクエストを処理できるようになります
app.module.ts
- import { Module } from "@nestjs/common";
+ import { MiddlewareConsumer, Module } from "@nestjs/common";
+ import { graphqlUploadExpress } from "graphql-upload";

@Module({
  imports: [
    GraphQLModule.forRoot(),
  ],
})
export class AppModule {
+  configure(consumer: MiddlewareConsumer) {
+    consumer.apply(graphqlUploadExpress()).forRoutes("graphql");
+  }
}

Resolverでfileを受け取る

  • 例えば次のようにfileを受け取ります
    • GraphQLのtypeはGraphQLUploadを指定します
    • 変数の型はFileUploadを使用します
profileImage.resolver.ts
import { Args, Mutation, Resolver } from "@nestjs/graphql";
import { FileUpload, GraphQLUpload } from "graphql-upload";

@Resolver()
export class ProfileImageResolver {
  @Mutation((returns) => ProfileImage)
  async uploadProfileImage(@Args({ name: "file", type: () => GraphQLUpload }) file: FileUpload) {
    console.log(file);
  }
}

  • GraphQLのCode Firstで実装している場合、次のようにGraphQLのschemaが生成されるはずです
schema.graphql
type Mutation {
  uploadProfileImage(file: Upload!): PrifileImage!
}

"""The `Upload` scalar type represents a file upload."""
scalar Upload

Apollo Client側でやること

npm/yarnでapollo-upload-clientを追加

yarn add apollo-upload-client
yarn add -D @types/apollo-upload-client

createHttpLinkcreateUploadLinkに置き換える

  • Apollo Clientでmultipart/form-dataを処理できるようにcreateHttpLinkの代わりにcreateUploadLinkを使用します
  • 置き換えた後、ファイルアップロードを伴わない通常のリクエストも正常に動作し続けます
import { ApolloClient, InMemoryCache, from } from "@apollo/client";
import { createUploadLink } from "apollo-upload-client";

- const httpLink = createHttpLink({
+ const uploadLink = createUploadLink({
  uri: `${APOLLO_URI}/graphql`,
});

export const client = new ApolloClient({
-   link: from([httpLink]),
+   link: from([uploadLink]),
});

GraphQL Code Generatorを使う場合はscalar Uploadに対応する

  • 前段のschema.graphgalscalar Uploadとして定義したものをFile型で扱うように定義しておきます
codegen.yaml
overwrite: true
schema: "../backend/schema.graphql"
documents:
  - ./graphql/queries/*.graphql
  - ./graphql/mutations/*.graphql
generates:
  graphql/generated.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
+     config:
+       scalars:
+        Upload: File
  • 例えば次のようにクエリを書きます
graphql/mutations/uploadProfileImage.graphql
mutation uploadProfileImage($file: Upload!) {
  uploadProfileImage(file: $file) {
    id
  }
}
  • 上記でcodegenを行うとReact Hooksでは次のように使用することができます
import { useUploadProfileImageMutation } from "../graphql/generated";

const [uploadProfileImage] = useUploadProfileImageMutation();

const handleUploadProfileImage = useCallback(
  async (file: File) => {
    await uploadProfileImage({
      variables: { file },
    });
  },
  [uploadProfileImage],
);

おまけ: Cloud Storageへのアップロード方法

  • Resolverで受け取ったfileをGCPのCloudStorageににファイルをアップロードする処理もついでに残しておきます
import { Storage } from "@google-cloud/storage";
import { FileUpload } from "graphql-upload";

const uploadFileToCloudStorage = async (file: FileUpload) => {
  const storage = new Storage({
    projectId: process.env.GCP_PROJECT_ID,
    credentials: {
      client_email: process.env.GCP_CLIENT_EMAIL,
      private_key: process.env.GCP_PRIVATE_KEY.replace(/\\n/g, "\n"),
    },
  });

  const bucket = await storage.bucket(process.env.CLOUD_STORAGE_BUCKET_NAME);
  const targetFile = bucket.file(file.fileName);
  const result = await new Promise<boolean>((resolve, reject) =>
    file
      .createReadStream()
      .pipe(targetFile.createWriteStream())
      .on("finish", () => resolve(true))
      .on("error", () => reject(false)),
  );
}

以上!

もっとGraphQL使いこなしていきたい :muscle:

15
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?