要实现使用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。

广告
将在 10 秒后关闭
bannerAds