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