How to upload file using nestjs-graphql-fastify server and how to test such feature?

Viewed 1138

I struggle to upload .csv file to nestjs-graphql-fastify server. Tried following code:

  @Mutation(() => Boolean)
  async createUsers(
    @Args({ name: 'file', type: () => GraphQLUpload })
    { createReadStream, filename }: FileUpload,
  ): Promise<boolean> {
    try {
      // backend logic . . .
    } catch {
      return false;
    }
    return true;
  }

but all I get when testing with postman is this response:

{
    "statusCode": 415,
    "code": "FST_ERR_CTP_INVALID_MEDIA_TYPE",
    "error": "Unsupported Media Type",
    "message": "Unsupported Media Type: multipart/form-data; boundary=--------------------------511769018912715357993837"
}

Developing with code-first approach.

Update: Tried to use fastify-multipart but issue still remains. What has changed is response in postman:

POST body missing, invalid Content-Type, or JSON object has no keys.
2 Answers

Found some answer's on Nestjs discord channel.

You had to do following changes:

main.ts

async function bootstrap() {
  const adapter = new FastifyAdapter();
  const fastify = adapter.getInstance();

  fastify.addContentTypeParser('multipart', (request, done) => {
    request.isMultipart = true;
    done();
  });
  fastify.addHook('preValidation', async function (request: any, reply) {
    if (!request.raw.isMultipart) {
      return;
    }

    request.body = await processRequest(request.raw, reply.raw);
  });

  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    adapter,
  );


  await app.listen(apiServerPort, apiServerHost);
}

bootstrap();

upload.scalar.ts

import { Scalar } from '@nestjs/graphql';
import { GraphQLUpload } from 'graphql-upload';

@Scalar('Upload')
export class UploadGraphQLScalar {
  protected parseValue(value) {
    return GraphQLUpload.parseValue(value);
  }

  protected serialize(value) {
    return GraphQLUpload.serialize(value);
  }

  protected parseLiteral(ast) {
    return GraphQLUpload.parseLiteral(ast, ast.value);
  }
}

users.resolver.ts

  @Mutation(() => CreateUsersOutput, {name: 'createUsers'})
  async createUsers(
    @Args('input', new ValidationPipe()) input: CreateUsersInput,
    @ReqUser() reqUser: RequestUser,
  ): Promise<CreateUsersOutput> {
    return this.usersService.createUsers(input, reqUser);
  }

create-shared.input.ts

@InputType()
export class DataObject {
  @Field(() => UploadGraphQLScalar)
  @Exclude()
  public readonly csv?: Promise<FileUpload>;
}

@InputType()
@ArgsType()
export class CreateUsersInput {
  @Field(() => DataObject)
  public readonly data: DataObject;
}

Also, I want to mention you should not use global validation pipes (in my case they made files unreadable)

  // app.useGlobalPipes(new ValidationPipe({ transform: true }));

You could use graphql-python/gql to try to upload a file:


from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport

transport = AIOHTTPTransport(url='YOUR_URL')

client = Client(transport=transport)

query = gql('''
  mutation($file: Upload!) {
    createUsers(file: $file)
  }
''')

with open("YOUR_FILE_PATH", "rb") as f:

    params = {"file": f}

    result = client.execute(
        query, variable_values=params, upload_files=True
    )

    print(result)

If you activate logging, you can see some message exchanged between the client and the backend.

Related