NestJS + TypeScript + GraphQL でファイルをアップロードする

ライブラリをインストール

shell
npm i graphql-upload-ts

Resolver を作成

あくまでサンプルなので、ディレクトリを作成する処理などを入れています。実業務では AWS S3 などに保存することになるでしょう。

uploader.resolver.ts
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { createWriteStream } from 'fs';
import { FileUpload, GraphQLUpload } from 'graphql-upload-ts';
import { existsSync, mkdirSync } from 'fs';

@Resolver()
export class UploaderResolver {
  @Mutation(() => Boolean)
  async singleUpload(
    @Args({ name: 'file', type: () => GraphQLUpload })
    { createReadStream, filename }: FileUpload,
  ) {
    const writeDirName = './stores';
    if (!existsSync(writeDirName)) {
      mkdirSync(writeDirName);
    }

    return new Promise((resolve, reject) => {
      createReadStream()
        .pipe(createWriteStream(`${writeDirName}/${filename}`))
        .on('finish', () => {
          console.log('完了しました。');
          resolve(true);
        })
        .on('error', (error) => {
          console.error('File write error:', error);
          reject(false);
        });
    });
  }
}

上の UploaderResolverapp.module.tsproviders に登録してください。

providers: [AppService, UploaderResolver],

main.ts に app.use(graphqlUploadExpress()); を設定する

app.use(graphqlUploadExpress()); を main.ts で設定しなければ、ファイルのアップロードはうまくいきませんでした。

GraphQL: POST body missing, invalid Content-Type, or JSON object has no keys というエラーが出ました。

GraphQL: POST body missing, invalid Content-Type, or JSON object has no keys

main.ts は以下のようになります。

main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { ValidationPipe } from '@nestjs/common';
import { graphqlUploadExpress } from 'graphql-upload-ts';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  const appPort = configService.get<number>('app.port');
  app.useGlobalPipes(new ValidationPipe());

  app.use(graphqlUploadExpress());
  await app.listen(appPort);

  console.log(`App is running on: ${await app.getUrl()}`);
}
bootstrap();

リクエストヘッダーに apollo-require-preflight: true を設定する

リクエストヘッダのキーに apollo-require-preflight , value に true を設定します。

ヘッダーに `apollo-require-preflight:trueを設定しなければ、リクエストがうまく通りませんでした。

apollo-server(v3系)は非推奨となったので、@apollo/server(v4系)に移行しましょう

クエリを投げる

QUERY
mutation singleUpload($file: Upload!) {
  singleUpload(file: $file)
}
VARIABLES
{"file": Upload}

ファイルを設定する部分が一番迷うと思うのですが、 Altair GraphQL Client を使えば、下の方にある Add files というボタンをクリックするだけでファイルを追加して、勝手にクエリを修正してくれました。

Postman を使う場合は以下の Stackoverflow の真似をします。

Is there any way to upload files via postman into a GraphQL API?

クエリを投げると、NestJS のプロジェクトのルートに stores/example.jpg が保存されました。

Altair GraphQL Client を使う

Stackoverflow の何者かのスクリーンショットを手がかりに見つけた GraphQL クライアントです。

https://altairgraphql.dev/

ファイルの設定が Postman より使いやすかったので、気分で乗り換えてもいいかもしれません。

https://www.xkoji.dev/blog/working-with-file-uploads-using-altair-graphql/

基本的には singed URL を使う

Apollo recommends handling the file upload itself out-of-band of your GraphQL server for the sake of simplicity and security. This “signed URL” approach allows your client to retrieve a URL from your GraphQL server (via S3 or other storage service) and upload the file to the URL rather than to your GraphQL server. 

Apolloでは、シンプルさとセキュリティの観点から、ファイルのアップロード自体をGraphQLサーバーの帯域外で処理することを推奨しています。この「署名付きURL」アプローチにより、クライアントはGraphQLサーバーからURLを取得し(S3またはその他のストレージサービスを介して)、GraphQLサーバーではなくURLにファイルをアップロードすることができます。

https://www.apollographql.com/docs/apollo-server/v3/data/file-uploads/