jaydenseric / graphql-upload

Middleware and an Upload scalar 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 131 forks source link

0 bytes on multiple files when use nestjs inputType #276

Closed SergioSuarezDev closed 2 years ago

SergioSuarezDev commented 2 years ago

When I load images with a resolver in GraphQL using a inputType class in NestJS, I get 0-byte files when i upload to s3. However, if I add everything as args, the files arrive correctly, I don't see too many differences, what could be the problem?

**mutation:**

@Mutation(() => Course)
  async addResources(
    @Args({ name: 'input', type: () => CreateResources }) input: CreateResources,
    @CurrentUser() user: User
  ) {
    const type = EXTRARESOURCES;
    const result = await this.courseService.addExtraResources(
      user,
      input,
      type
    );
    if (!result) errorInvalidInput(type);
    return result;
  }

inputType

@InputType()
export class CreateResources {
  @IsOptional()
  @Field(() => [CreateResourceLinkInput], { nullable: true })
  links: CreateResourceLinkInput[];

  @IsOptional()
  @Field(() => [CreateResourceVideoInput], { nullable: true })
  videos: CreateResourceVideoInput[];

  @IsOptional()
  @Field(() => [GraphQLUpload], { nullable: true })
  documents: Promise<[FileUpload]>;

  @IsOptional()
  @Field(() => [GraphQLUpload], { nullable: true })
  images: Promise<[FileUpload]>;
}

All as args

@Mutation(() => Course)
  async AddResources(
    @Args({ name: 'links', type: () => [CreateResourceLinkInput], nullable: true }) links: [CreateResourceLinkInput],
    @Args({ name: 'videos', type: () => [CreateResourceVideoInput], nullable: true }) videos: [CreateResourceVideoInput],
    @Args({ name: 'documents', type: () => [GraphQLUpload], nullable: true }) documents: [FileUpload],
    @Args({ name: 'images', type: () => [GraphQLUpload], nullable: true }) images: [FileUpload],
    @CurrentUser() user: User
  ) {
    const type =EXTRARESOURCES;
    const input = {
      links,
      videos,
      documents,
      images
    };
    const result = await this.courseService.addExtraResources(
      user,
      input,
      type
    );
    if (!result) errorInvalidInput(type);
    return result;
  }
jaydenseric commented 2 years ago

I'm not familiar with the way that code is written, can you please rephrase your question using standard GraphQL SDL?

SergioSuarezDev commented 2 years ago

Yes, here part of my schema.gql

THIS FAILS

AddResources(input: CreateResources! ): Course!
input CreateResources {
  documents: [Upload!]
  images: [Upload!]
  links: [CreateResourceLinkInput!]
  videos: [CreateResourceVideoInput!]
  order: Int
}

THIS WORKS

 AddResources(documents: [Upload!], images: [Upload!]): Course!
joaokorcz commented 2 years ago

Hey, Sergio, how do you request to this resolver? I need to upload many images in one single mutation too, but i dont know how to send de "body" of the request

joaokorcz commented 2 years ago

In your CreateResources InputType, images shouldnt it be images: Promise<FileUpload[]> instead of Promise<[FileUpload]>?

SergioSuarezDev commented 2 years ago

Hey, Sergio, how do you request to this resolver? I need to upload many images in one single mutation too, but i dont know how to send de "body" of the request

hope this helps you


mutation addResources($images: [Upload!]! $documents: [Upload!]!) {
  addResources (
      courseId: 12345678
      images: $images
      documents: $documents      
  ) {
      ...courseFragment
    }
} 
SergioSuarezDev commented 2 years ago

And i'm using altair graphql client It's amazing to send 1 or more files and test the api https://altair.sirmuel.design/docs/features/file-upload.html https://altair.sirmuel.design/

Captura de pantalla 2021-11-04 a las 14 54 07

jaydenseric commented 2 years ago

@SergioSuarezDev the GraphQL multipart request spec allows Upload scalars to be used both ways you're describing; nested in an input type or not nested. graphql-upload should process either approaches ok if the request is correct. There must be something specific to your backend implementation or how the requests are made that is the problem.

joaokorcz commented 2 years ago

@SergioSuarezDev thanks very much, for real. I was using postman and apollo playground to test and send files with postman to graphql is stressful, i'll check this altair now

SergioSuarezDev commented 2 years ago

@SergioSuarezDev the GraphQL multipart request spec allows Upload scalars to be used both ways you're describing; nested in an input type or not nested. graphql-upload should process either approaches ok if the request is correct. There must be something specific to your backend implementation or how the requests are made that is the problem.

I'm going to try creating an UpdateScalar in Nestjs to see if that may be the problem with inputTypes Something like this: https://vortac.io/2020/05/09/uploading-files-with-nestjs-and-graphql/

SergioSuarezDev commented 2 years ago

didn't work :(

jaydenseric commented 2 years ago

Closing because this issue doesn't appear to be actionable on the graphql-upload side, but feel free to keep the conversation going. I'm happy to answer any questions you may have about graphql-upload, but I can't be of much help with NestJS stuff since I've never used it in a project before. Maybe you'll figure it out, or someone here can offer more advice, but the NestJS community might be a better place to ask about these problems.

SergioSuarezDev commented 2 years ago

Hi guys.

After reading and reading and testing and testing. The problem comes from the "class-validator" module that is used in Nestjs because the validator internally tries to transform the object (since the file instance is an object) and fails. By adding the @Exclude decorator to the GraphQLUpload field, the files are received correctly.

Like this:

@InputType('CreateResources')
export class CreateResources {
  @Exclude()
  @IsOptional()
  @Field(() => [GraphQLUpload], { nullable: true })
  documents?: Promise<FileUpload>[];

  @Exclude()
  @IsOptional()
  @Field(() => [GraphQLUpload], { nullable: true })
  images?: Promise<FileUpload>[];

  @IsOptional()
  @IsNumber()
  @Field(() => Int, { nullable: true })
  order: number;
}
SergioSuarezDev commented 2 years ago
Captura de pantalla 2021-11-05 a las 10 02 52
Jawell commented 2 years ago

Hi guys.

After reading and reading and testing and testing. The problem comes from the "class-validator" module that is used in Nestjs because the validator internally tries to transform the object (since the file instance is an object) and fails. By adding the @Exclude decorator to the GraphQLUpload field, the files are received correctly.

Like this:

@InputType('CreateResources')
export class CreateResources {
  @Exclude()
  @IsOptional()
  @Field(() => [GraphQLUpload], { nullable: true })
  documents?: Promise<FileUpload>[];

  @Exclude()
  @IsOptional()
  @Field(() => [GraphQLUpload], { nullable: true })
  images?: Promise<FileUpload>[];

  @IsOptional()
  @IsNumber()
  @Field(() => Int, { nullable: true })
  order: number;
}

Hi @SergioSuarezDev How do you use @Args({ name: 'input', type: () => CreateResources }) input: CreateResources, after @Exclude() data?

Migushthe2nd commented 2 years ago

@Exclude works, however, unfortunately it requires that all parent objects also be excluded:

ParentOne (@Exclude)
|---- ChildOne (@Exclude)
        |---- file: Promise<FileUpload> (@Exclude)
|---- ChildTwo

Actually, only ParentOne has to be exluded, since it would apply to all children.

I'd like my ParentOne to be an array and use @ArrayMinSize, which is not possible if it also has the @Exclude decorator.

SergioSuarezDev commented 2 years ago

@Exclude works, however, unfortunately it requires that all parent objects also be excluded:

ParentOne (@Exclude)
|---- ChildOne (@Exclude)
        |---- file: Promise<FileUpload> (@Exclude)
|---- ChildTwo

Actually, only ParentOne has to be exluded, since it would apply to all children.

I'd like my ParentOne to be an array and use @ArrayMinSize, which is not possible if it also has the @Exclude decorator.

sure, but you can do a manual check of the arrays within the service and send an exception error using nestjs

Migushthe2nd commented 2 years ago

sure, but you can do a manual check of the arrays within the service and send an exception error using nestjs

Yeah I had intended to do so, but then I noticed that all other fields on ChildOne weren't being validated anymore either. Even if I use @Expose for a property in ChildOne/ChildTwo, the parent @Exclude is preventing all fields in the children from being validated. I even tried this https://github.com/typestack/routing-controllers/issues/200#issuecomment-327370091 and adding @ValidateNested()

Edit: @ValidateNested() wasn't working because I was missing the @Type (class-transformer) decorator:

    @ValidateNested()
    @Type(() => ChildOne)
    child: ChildOne[]

Edit 2: I got it working properly. Now I don't have to @Exclude all parents, all nested properties are validated as expected, and file is simply skipped:

// parent
    @ValidateNested()
    @Type(() => ChildOne)
    child: ChildOne[];
// child
    @Exclude()
    file: Promise<FileUpload>;