jaydenseric / graphql-upload

Middleware and a scalar Upload to add support for GraphQL multipart requests (file uploads via queries and mutations) to various Node.js GraphQL servers.
https://npm.im/graphql-upload
MIT License
1.43k stars 132 forks source link

Strange unawaited file wrapped structure #253

Closed ssukienn closed 3 years ago

ssukienn commented 3 years ago

Hi. I have some problems with the structure of the uploaded file(s).

I integrated this library inside my federated project, the gateway, and example service. On top of that I am using Nest.js framework which is instantiating both mentioned above.

In both services, I turned off the default upload (in ApolloGateway and ApolloServer) by setting uploads: false. Additionally, I set the graphqlUploadExpress middleware:

...
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer.apply(graphqlUploadExpress()).forRoutes('graphql');
    }
}

My service federated schema related to uploads looks like:

type Mutation {
    uploadTest(attachment: Upload!, file: UploadModel!): Survey
}

"""The `Upload` scalar type represents a file upload."""
scalar Upload

input UploadModel {
  uploadFile: Upload
}

and resolver:

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

@Mutation(() => SomeModel, {
    nullable: true,
})
async uploadTest(
    @Args('file') upload: UploadModel,
    @Args('attachment', { type: () => GraphQLUpload }) attachment: FileUpload,
) {
    const actualFile = await this.resolveFile(attachment);
    const stream = actualFile.createReadStream();
    stream?.on('data', (chunk: Buffer) => console.log(chunk.toString()));
}

async resolveFile(file: any) {
    const awaitedFile = await (file as { promise: Promise<{ file: FileUpload }> }).promise;
    return await (awaitedFile as any).promise;
}

Nest reference: https://github.com/nestjs/graphql/issues/901#issuecomment-780007582

UploadModel is some input object which takes a file but it doesn't matter if the file is nested inside the input or passed as a direct argument. The actual file containing createReadStream is obtained after some promise shenanigans happening in resolveFile helper.

When looking inside the debugger the actual structure of the attachment is something like: image where I have Upload object with file property without the stream and the promise which when awaited consists of { resolve, reject, promise} and after awaiting this second promise (which I thought won't be there and I will get the file thus typing in resolveFile) I actually get the real structure of FileUpload. The file object from the screen on the same level as resolve and reject is not actually there until awaiting the second promise (that's why double promise awaits in resolveFile).

Unfortunately, it will be hard for me to provide some reproducible repo due to the size of the project but maybe someone faced similar issues with strange wrapping and have an idea of what I am doing wrong here?

I am testing it with the Altair with request source like:

------WebKitFormBoundaryMIblSV1Wzt7MZCa7
Content-Disposition: form-data; name="operations"

{"query":"# query someQuery {\n#   me {\n#     tenant {\n#       uuid\n#       logo\n#     }\n#   }\n# }\n\nmutation someMutation($someFile: Upload!) {\n  uploadTest(attachment: $someFile, file: { uploadFile: $someFile }) {\n    uuid\n  }\n}","variables":{"someFile":null},"operationName":"someMutation"}
------WebKitFormBoundaryMIblSV1Wzt7MZCa7
Content-Disposition: form-data; name="map"

{"0":["variables.someFile"]}
------WebKitFormBoundaryMIblSV1Wzt7MZCa7
Content-Disposition: form-data; name="0"; filename="test.txt"
Content-Type: text/plain

------WebKitFormBoundaryMIblSV1Wzt7MZCa7--

Thanks for help.

versions:

graphql-upload: 12.0.0 @types/graphql-upload: 8.0.5

jaydenseric commented 3 years ago

It seems the complexity and source of your problems is Nest, Apollo Server and Gateway; it's better to ask for help from the Nest community or Apollo customer service if you're having difficulty with their products.

Note that for schema stitching some of the tooling that might be present in your project substitutes the GraphQLUpload scalar, because the original scalar is only intended as an input, but they "serialize" (even though a file upload stream can't be serialized to a string like the GraphQL serialization system was originally intended) the value back out to forward it to the next GraphQL API:

https://github.com/ardatan/graphql-tools/blob/eb233dae26f74c45ba74c02edf77ef1975bfbf69/packages/links/src/GraphQLUpload.ts#L4-L22

There is past discussion on this topic you can see in https://github.com/jaydenseric/graphql-upload/issues/194 .

Although everything about graphql-upload is documented quite thoroughly in the readme, I'm happy to answer here any questions you may have about just the graphql-upload project.