1

Is anyone has an example of how to upload a file in NestJs using GraphQl?

I can upload using given example via controller

https://github.com/nestjs/nest/issues/262#issuecomment-366098589,

but I couldn't find any comprehensive documentation how to upload using GrahpQL in NestJS

Sofyan Hadi A.
  • 45
  • 1
  • 1
  • 9

7 Answers7

5

Apollo Server 2.0 should be able to do this now (packaged in nest), although I needed to install graphql-upload and import GraphQLUpload as I couldn't find the Upload type:

@Mutation(() => Image, { nullable: true })
async addImage(@Args({ name: 'image', type: () => GraphQLUpload }) image) {
  // Do stuff with image...
}
Will Squire
  • 6,127
  • 7
  • 45
  • 57
2

At the time of this answer FileInterceptor is using multer and by converting ExecutionContext to http it uses getRequest and getResponse methods to provide req and res to multer.single which they are (req and res) undefined in GraphQL.

I have tried to get request from context using:

const ctx = GqlExecutionContext.create(context);

and there is req property in ctx but I can't find a way to use multer (yet).

Anyway, I made some changes to FileFieldsInterceptor to use it inside my project, but I may make pull request when I had time to clean it up:

import { Observable } from 'rxjs';
import {
  NestInterceptor,
  Optional,
  ExecutionContext,
  mixin,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { storeFile } from './storeFile';

interface IField {
  name: string;
  options?: any;
}

export function GraphqlFileFieldsInterceptor(
  uploadFields: IField[],
  localOptions?: any,
) {
  class MixinInterceptor implements NestInterceptor {
    options: any = {};
    constructor(@Optional() options: any = {}) {
      this.options = { ...options, ...localOptions };
    }

    async intercept(
      context: ExecutionContext,
      call$: Observable<any>,
    ): Promise<Observable<any>> {
      const ctx = GqlExecutionContext.create(context);
      const args = ctx.getArgs();

      let storeFilesResult = await Promise.all(
        uploadFields.map(uploadField => {
          const file = args[uploadField.name];
          return storeFile(file, {
            ...uploadField.options,
            ...this.options,
          }).then(address => {
            args[uploadField.name] = address;
            return address;
          });
        }),
      );

      return call$;
    }
  }
  const Interceptor = mixin(MixinInterceptor);
  return Interceptor;
}

and store file is something like this (may not be used like this):

import uuid from 'uuid/v4';
import fs from 'fs';
import path from 'path';

const dir = './files';
if (!fs.existsSync(dir)) {
  fs.mkdirSync(dir);
}

export const storeFile = async (file, options): Promise<any> => {
  // options is not doing anything right now
  const { stream } = await file;
  const filename = uuid();

  const fileAddress = path.join(dir, filename + '.jpg');
  return new Promise((resolve, reject) =>
    stream
      .on('error', error => {
        if (stream.truncated)
          // Delete the truncated file
          fs.unlinkSync(fileAddress);
        reject(error);
      })
      .pipe(fs.createWriteStream(fileAddress))
      .on('error', error => reject(error))
      .on('finish', () => resolve(fileAddress)),
  );
};

In my Cats.resolvers.ts:

...
  @Mutation()
  @UseInterceptors(
    GraphqlFileFieldsInterceptor([
      { name: 'catImage1' },
      { name: 'catImage2' },
      { name: 'catImage3' },
    ]),
  )
  async cats(
    @Args('catImage1') catImage1: string,
    @Args('catImage2') catImage2: string,
    @Args('catImage3') catImage3: string,
  ){
    console.log(catImage1) // will print catImage1 address
    ...
Developia
  • 3,928
  • 1
  • 28
  • 43
1

This implementation works perfectly with Node >= v14

  1. package.json

Remove the fs-capacitor and graphql-upload entries from the resolutions section if you added them, and install the latest version of graphql-upload (v11.0.0 at this time) package as a dependency.

  1. src/app.module.ts

Disable Apollo Server's built-in upload handling and add the graphqlUploadExpress middleware to your application.

import { graphqlUploadExpress } from "graphql-upload"
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"

@Module({
  imports: [
    GraphQLModule.forRoot({
      uploads: false, // disable built-in upload handling
    }),
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(graphqlUploadExpress()).forRoutes("graphql")
  }
}
  1. src/blog/post.resolver.ts (example resolver)

Remove the GraphQLUpload import from apollo-server-core and import from graphql-upload instead

import { FileUpload, GraphQLUpload } from "graphql-upload"

@Mutation(() => Post)
async postCreate(
  @Args("title") title: string,
  @Args("body") body: string,
  @Args("attachment", { type: () => GraphQLUpload }) attachment: Promise<FileUpload>,
) {
  const { filename, mimetype, encoding, createReadStream } = await attachment
  console.log("attachment:", filename, mimetype, encoding)

  const stream = createReadStream()
  stream.on("data", (chunk: Buffer) => /* do stuff with data here */)
}

Source: https://github.com/nestjs/graphql/issues/901#issuecomment-780007582

Some other links that I found helpful:

Piyush Kakkar
  • 59
  • 1
  • 1
  • 5
0

EDIT: As per Developia comment below, apollo-server now implements file upload. Should be preferred way.

Below, original answer, for reference.

One normally does not use GraphQL for upload. GraphQL is fancy "specification of API", meaning that in the end of the day, low level HTTP request and responses are translated to/from JSON objects (if you don't have custom transport).

One solution could be to define special endpoint in GraphQL schema like:

mutation Mutation {
  uploadFile(base64: String): Int
}

Then client would convert binary data to base64 string, which would be handled accordingly on resolver side. This way, file will become part of JSON object exchanged between GraphQL client and server.

While this is might be suitable for small files, small number of operations, it is definitely not a solution for upload service.

muradm
  • 1,973
  • 19
  • 30
  • 1
    Using Base64 to upload files is not scalable, if you have multiple files to upload or with huge file sizes. It's possible now to use apollo-upload-server which currently merged into apollo-server@2, so it can handle multipart form data. Nestjs is providing apollo-graphql@2 as gql server, OP asked for docs because although it (logically) possible there is no sample in nestjs docs. – Developia Oct 16 '18 at 14:53
0

try this

import { Resolver, Mutation, Args } from '@nestjs/graphql';
import { createWriteStream } from 'fs';

import {GraphQLUpload} from "apollo-server-express"

@Resolver('Download')
export class DownloadResolver {
    @Mutation(() => Boolean)
    async uploadFile(@Args({name: 'file', type: () => GraphQLUpload})
    {
        createReadStream,
        filename
    }): Promise<boolean> {
        return new Promise(async (resolve, reject) => 
            createReadStream()
                .pipe(createWriteStream(`./uploads/${filename}`))
                .on('finish', () => resolve(true))
                .on('error', () => reject(false))
        );
    }
    
}
-2

You could use the apollo-upload-server lib. Seems like the easiest thing to do, in my opinion. Cheers

Clayton Ray
  • 265
  • 2
  • 13
-4

You need to define an upload controller and add it in your app.module, this is an example of what a controller should be (back-end):

@Controller()
export class Uploader {
  @Post('sampleName')
  @UseInterceptors(FileInterceptor('file'))
  uploadFile(@UploadedFile() file) {
  // file name selection 
    const path = `desired path`;
    const writeStream = fs.createWriteStream(path);  
    writeStream.write(file.buffer);
    writeStream.end();
    return {
      result: [res],
    };
  }
}

And call your controller by fetch in the front-end:

    fetch('controller address', {
          method: 'POST',
          body: data,
        })
          .then((response) => response.json())
          .then((success) => {
            // What to do when succeed 
});
          })
          .catch((error) => console.log('Error in uploading file: ', error));
Ali Tourani
  • 1,170
  • 3
  • 21
  • 31