要实现使用NestJS + GraphQL + Apollo Client上传文件,可以考虑以下方法:
前提 –
先决条件
-
- 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を使用するためインストールしておきます
https://github.com/jaydenseric/graphql-upload
yarn add graphql-upload
yarn add -D @types/graphql-upload
将graphqlUploadExpress添加为AppModule的中间件。
- これによりmultipart/form-dataのリクエストを処理できるようになります
- 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");
+ }
}
使用解析器接收文件
-
- 例えば次のようにfileを受け取ります
GraphQLのtypeはGraphQLUploadを指定します
変数の型はFileUploadを使用します
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が生成されるはずです
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を使用するためインストールしておきます
https://github.com/jaydenseric/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型で扱うように定義しておきます
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
- 例えば次のようにクエリを書きます
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],
);
- 参考: https://stackoverflow.com/questions/49507035
附:将文件上传至云存储的方法
- 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)),
);
}
以上!- Over!
我想要更熟练地使用GraphQL。