前提
- Apollo Clientなどを使ってGraphQLのコードを書く場合にリクエストの方法を意識することはあまりないが、実態は
content-type: application/json
のPOSTメソッドでbodyにGraphQLのクエリを記述してリクエストしているだけである - ファイルのアップロードを行う場合は通常
content-type: multipart/form-data
である必要があるため、それに対応する一工夫が必要となる - NestJS + GraphQL + Apollo Client の構成での実装例が見つからなかったので備忘録として残しておく
実装方法
NestJS側でやること
npm/yarnでgraphql-upload
を追加
-
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
を使用します
- GraphQLのtypeは
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
- NestJS側の準備は以上です
- 参考: https://github.com/nestjs/graphql/issues/901#issuecomment-780007582
Apollo Client側でやること
npm/yarnでapollo-upload-client
を追加
-
apollo-upload-client
を使用するためインストールしておきます
yarn add apollo-upload-client
yarn add -D @types/apollo-upload-client
createHttpLink
をcreateUploadLink
に置き換える
- 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.graphgal
でscalar 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使いこなしていきたい