5

I tried adding an upload argument to a GraphQL endpoint using graphql-upload's GraphQLUpload scalar:

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

@Mutation(() => Image, { nullable: true })
async addImage(@Args({name: 'image', type: () => GraphQLUpload}) image: FileUpload): Promise<Image | undefined> {
// do stuff...
}

And this worked initially. A few runs later however, and it started returning the following error:

"Variable \"$image\" got invalid value {}; Expected type Upload. Upload value invalid."

Tried testing with Insomnia client and curl:

curl localhost:8000/graphql \
  -F operations='{ "query": "mutation ($image: Upload!) { addImage(image: $image) { id } }", "variables": { "image": null } }'
  -F map='{ "0": ["variables.image"] }'
  -F 0=@/path/to/image
3
  • @xadm thanks for the reply. The spec here: github.com/jaydenseric/graphql-multipart-request-spec mentions these should be null, is this not the case? Commented Feb 4, 2020 at 15:17
  • it worked ... node.js errors/warnings? restart? Commented Feb 4, 2020 at 15:42
  • @xadm GraphQLError: Upload value invalid. at GraphQLScalarType.parseValue (/path/to/project/nest/node_modules/graphql-upload/lib/GraphQLUpload.js:66:11). It worked for me initially (about an hour), then the error came and I'm not sure what changed if anything. Commented Feb 4, 2020 at 15:50

6 Answers 6

24

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

not import GraphQLUpload from 'graphql-upload'

enter image description here

Sign up to request clarification or add additional context in comments.

3 Comments

Why this answer not at the top?!
I had issue just with the docker and this worked for me. you are from the future
apollo-server-express no longer includes GraphQLUpload.
11

Based on @willsquire's answer, I realized that for some reason the Scalar decorator is not working for me, therefore I ended up replacing graphql-upload with the next snippet

import * as FileType from 'file-type'
import { GraphQLError, GraphQLScalarType } from 'graphql'
import { Readable } from 'stream'

export interface FileUpload {
  filename: string
  mimetype: string
  encoding: string
  createReadStream: () => Readable
}

export const GraphQLUpload = new GraphQLScalarType({
  name: 'Upload',
  description: 'The `Upload` scalar type represents a file upload.',
  async parseValue(value: Promise<FileUpload>): Promise<FileUpload> {
    const upload = await value
    const stream = upload.createReadStream()
    const fileType = await FileType.fromStream(stream)

    if (fileType?.mime !== upload.mimetype)
      throw new GraphQLError('Mime type does not match file content.')

    return upload
  },
  parseLiteral(ast): void {
    throw new GraphQLError('Upload literal unsupported.', ast)
  },
  serialize(): void {
    throw new GraphQLError('Upload serialization unsupported.')
  },
})

2 Comments

Sorry for the late reply @Mutation(() => File) async uploadFile( @Args({ name: 'input', type: () => GraphQLUpload }) fileInput: FileUpload, ): Promise<File> { const url = await this.filesService.create(fileInput) return { url, success: true } } Where GraphQLUpload and FileUpload are imported from my previous code and File type is just the schema that you are going to return
This errors for me when I upload a file: RangeError: Maximum call stack size exceeded at ReadStream.open"
3

For me, solutions proposed here by @Yhozen and @Willsquire are a kind a workaround but not the true answer to the problem.

In my case, the real problem was coming from graphql-upload I had it in my dependencies and it was creating the bug described in this stack.

By removing the dependence it solved the problem. As @willsquire commented, graphql-upload is already in apollo-server package, no need to import it in package.

2 Comments

It might be exposed now, but at the time the type wasn't exposed (i.e. FileUpload ). Hence the dependency and import :)
Sure @willsquire. You’re comment from 12th Feb guide me. Thank you for that. After few hours trying to solve the issue, you gave me the rights tips!
1

After a little digging it appears that apollo-server-core automatically parses file uploads in the middleware with graphql-upload based on the request being a multi part form, rather than determining by scalar type name. So graphql-upload isn't necessarily needed as it's already integrated, but it's useful for getting the parsed type:

import { Scalar } from '@nestjs/graphql'
import FileType from 'file-type'
import { GraphQLError } from 'graphql'
import { FileUpload } from 'graphql-upload'
import { isUndefined } from 'lodash'

@Scalar('Upload')
export class Upload {
  description = 'File upload scalar type'

  async parseValue(value: Promise<FileUpload>) {
    const upload = await value
    const stream = upload.createReadStream()
    const fileType = await FileType.fromStream(stream)

    if (isUndefined(fileType)) throw new GraphQLError('Mime type is unknown.')

    if (fileType?.mime !== upload.mimetype)
      throw new GraphQLError('Mime type does not match file content.')

    return upload
  }
}

Update 01/02/2021

Still struggle with this today. Some great answers here, but they no longer work for me. Issue is if I throw an error in parseValue it 'hangs'. The below solution works the best for me by resolving the 'hanging' issue and still pushing the actual file through for usage (use case is .csv file):

import { UnsupportedMediaTypeException } from '@nestjs/common'
import { Scalar } from '@nestjs/graphql'
import { ValueNode } from 'graphql'
import { FileUpload, GraphQLUpload } from 'graphql-upload'

export type CSVParseProps = {
  file: FileUpload
  promise: Promise<FileUpload>
}

export type CSVUpload = Promise<FileUpload | Error>
export type CSVFile = FileUpload

@Scalar('CSV', () => CSV)
export class CSV {
  description = 'CSV upload type.'
  supportedFormats = ['text/csv']

  parseLiteral(arg: ValueNode) {
    const file = GraphQLUpload.parseLiteral(arg, (arg as any).value)

    if (
      file.kind === 'ObjectValue' &&
      typeof file.filename === 'string' &&
      typeof file.mimetype === 'string' &&
      typeof file.encoding === 'string' &&
      typeof file.createReadStream === 'function'
    )
      return Promise.resolve(file)

    return null
  }

  // If this is `async` then any error thrown
  // hangs and doesn't return to the user. However,
  // if a non-promise is returned it fails reading the
  // stream later. We can't evaluate the `sync`
  // version of the file either as there's a data race (it's not
  // always there). So we return the `Promise` version
  // for usage that gets parsed after return...
  parseValue(value: CSVParseProps) {
    return value.promise.then((file) => {
      if (!this.supportedFormats.includes(file.mimetype))
        return new UnsupportedMediaTypeException(
          `Unsupported file format. Supports: ${this.supportedFormats.join(
            ' '
          )}.`
        )

      return file
    })
  }

  serialize(value: unknown) {
    return GraphQLUpload.serialize(value)
  }
}

This on the ArgsType:

@Field(() => CSV)
file!: CSVUpload

This in the resolver:

// returns either the file or error to throw
const fileRes = await file

if (isError(fileRes)) throw fileRes

2 Comments

Can you explain a little further how did you get your mutation working? I'm running into the same error
Is your apollo-server-core package at latest? If so, it includes graphql-upload in it's dependencies and has the middleware handling file uploads already.
1

If you want to use the graphql-upload package, then you have to apply their Express middleware, and disable apollo server's internal upload module.

see this answer: https://stackoverflow.com/a/64659256/10404270

After that a mutation like this works for me:

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

  @Mutation(() => Boolean)
  async docUpload(
    @Arg('userID') userid: number,
    @Arg('file', () => GraphQLUpload)
    file: FileUpload
  ) {
    const { filename, createReadStream } = file;
    console.log(userid, file, filename, createReadStream);
    return true
  }

1 Comment

Apollo Server no longer supports uploads out of the box.
0

The best answer is here: https://dilushadasanayaka.medium.com/nestjs-file-upload-with-grapql-18289b9e32a2

Note: To test a file upload you must send correct format of the file. Graphql does not support file paths. such as:

{
    file: '/user/mim/desktop/t.txt'
}

and you must be sent complete file.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.