Nestjs 中的 Graphql Apollo 上传 returns 无效值 {}
Graphql Apollo upload in Nestjs returns invalid value {}
我尝试使用 graphql-upload
的 GraphQLUpload
标量向 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'
}
并且必须向您发送完整的文件。
我尝试使用 graphql-upload
的 GraphQLUpload
标量向 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'
}
并且必须向您发送完整的文件。