Nestjs 中的 Graphql Apollo 上传 returns 无效值 {}

Graphql Apollo upload in Nestjs returns invalid value {}

我尝试使用 graphql-uploadGraphQLUpload 标量向 GraphQL 端点添加上传参数:

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

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

这最初有效。然而,稍后运行了几次,它开始返回以下错误:

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

尝试使用 Insomnia 客户端和 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

经过一些挖掘后,apollo-server-core 会自动解析中间件中的文件上传 graphql-upload 基于多部分形式的请求,而不是通过标量类型名称来确定。所以 graphql-upload 不一定需要,因为它已经集成,但它对于获取解析类型很有用:

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
  }
}

更新 01/02/2021

今天仍在为此苦苦挣扎。这里有一些很好的答案,但它们不再对我有用。问题是如果我在 parseValue 中抛出错误 'hangs'。下面的解决方案最适合我,它解决了 'hanging' 问题并仍然推送实际文件供使用(用例是 .csv 文件):

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)
  }
}

这个在ArgsType:

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

解析器中的这个:

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

if (isError(fileRes)) throw fileRes

根据@willsquire 的回答,我意识到由于某种原因 Scalar 装饰器不适合我,因此我最终用下一个片段替换了 graphql-upload

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

对我来说,@Yhozen 和@Willsquire 在这里提出的解决方案是一种解决方法,但不是问题的真正答案。

就我而言,真正的问题来自 graphql-upload 我在我的依赖项中有它,它正在创建此堆栈中描述的错误。

通过删除依赖它解决了问题。正如@willsquire 评论的那样,graphql-upload 已经在 apollo-server 包中,不需要在包中导入它。

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

从'graphql-upload'

导入GraphQLUpload

如果你想使用graphql-upload包,那么你必须应用他们的Express中间件,并禁用apollo服务器的内部上传模块。

看到这个答案:

之后像这样的突变对我有用:

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
  }

最佳答案在这里: https://dilushadasanayaka.medium.com/nestjs-file-upload-with-grapql-18289b9e32a2

注意:要测试文件上传,您必须发送正确格式的文件。 Graphql 不支持文件路径。如:

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

并且必须向您发送完整的文件。