3
votes

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
5
@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? - Will Squire
it worked ... node.js errors/warnings? restart? - xadm
@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. - Will Squire

5 Answers

22
votes

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

not import GraphQLUpload from 'graphql-upload'

enter image description here

11
votes

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.')
  },
})
3
votes

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.

1
votes

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
1
votes

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
  }