mercurius-js / mercurius-upload

graphql-upload implementation plugin for Fastify & mercurius
https://mercurius.dev/
MIT License
29 stars 12 forks source link

Upload value invalid and type Fastify, TypeGraphQL #15

Closed CrispenGari closed 1 year ago

CrispenGari commented 1 year ago

I'm trying to do file uploads using a fastify server using mercurius, type-graphql and mercurius-upload plugin. Here is my server code:

import "reflect-metadata";
import "dotenv/config";
import _ from "node-env-types";
import Fastify from "fastify";
import mercurius from "mercurius";
import { buildSchema } from "type-graphql";
import { CtxType } from "./types";
import { resolvers } from "./resolvers";
import { PrismaClient } from "@prisma/client";
import cors from "@fastify/cors";
import cookie from "@fastify/cookie";
import { tokenRoute } from "./routes/token";
import fastifyStatic from "@fastify/static";
import path from "path";
import MercuriusGQLUpload from "mercurius-upload";
_();

const PORT: any = process.env.PORT || 3001;
const HOST =
  process.env.NODE_ENV === "production"
    ? "0.0.0.0"
    : "localhost" || "127.0.0.1";

(async () => {
  const fastify = Fastify({
    logger: false,
    ignoreTrailingSlash: true,
  });

  const prisma = new PrismaClient();
  const schema = await buildSchema({
    resolvers,
    validate: false,
  });

  fastify.register(cors, {
    credentials: true,
    origin: ["http://localhost:3000"],
  });

  fastify.register(cookie, {
    secret: "my-secret",
    parseOptions: {
      sameSite: "lax",
      httpOnly: false,
      secure: false,
      maxAge: 1000 * 60 * 60 * 24 * 7,
    },
  });
  fastify.register(fastifyStatic, {
    root: path.join(__dirname.replace("dist", ""), "storage"),
    prefixAvoidTrailingSlash: true,
    prefix: "/petmall/api/storage",
  });
  fastify.register(MercuriusGQLUpload, {});
  fastify.register(tokenRoute);
  fastify.register(mercurius, {
    context: (request, reply): CtxType => {
      return {
        request,
        reply,
        prisma,
      };
    },
    graphiql: true,
    schema: schema as any,
    errorHandler(error, request, reply) {
      console.log(error);
      console.error(error.message);
    },
  });

  fastify.listen({ port: PORT, host: HOST }, (error, address) => {
    if (error) {
      console.error(error);
      process.exit(1);
    }
    console.log(` Server is now listening on ${address}`);
  });
})();

Here is my PetResolver

import { Arg, Mutation, Resolver } from "type-graphql";
import { NewPetInputType } from "./inputs/inputTypes";

@Resolver()
export class PetResolver {
  @Mutation(() => Boolean)
  async add(@Arg("input", () => NewPetInputType) { image }: NewPetInputType) {
    console.log(image);
    return true;
  }
}

Here in my NewPetInputType

import { InputType, Field, Float, Int, registerEnumType } from "type-graphql";
import { Category, Gender } from "../../../types";
import GraphQLUpload from "graphql-upload/GraphQLUpload.js";
import { FileUpload } from "graphql-upload/Upload";
registerEnumType(Gender, {
  name: "Gender", // this one is mandatory
});

registerEnumType(Category, {
  name: "Category", // this one is mandatory
});

@InputType()
export class LocationInput {
  @Field(() => String, { nullable: true })
  district?: string;
  @Field(() => String, { nullable: true })
  city?: string;
  @Field(() => String, { nullable: true })
  street?: string;
  @Field(() => String, { nullable: true })
  region?: string;
  @Field(() => String, { nullable: true })
  country?: string;
  @Field(() => String, { nullable: true })
  postalCode?: string;
  @Field(() => String, { nullable: true })
  subregion?: string;
  @Field(() => String, { nullable: true })
  timezone?: string;
  @Field(() => String, { nullable: true })
  streetNumber?: string;
  @Field(() => String, { nullable: true })
  name?: string;
  @Field(() => String, { nullable: true })
  isoCountryCode?: string;
}

@InputType()
export class NewPetInputType {
  @Field(() => GraphQLUpload)
  image: FileUpload;

  @Field(() => LocationInput, { nullable: true })
  location?: LocationInput;

  @Field(() => String, { nullable: false })
  description: string;
  @Field(() => Category, { nullable: false })
  category: Category;
  @Field(() => Gender, { nullable: false })
  gender: Gender;
  @Field(() => Float, { nullable: false })
  price: number;
  @Field(() => Int, { nullable: false })
  age: number;
  @Field(() => String, { nullable: false })
  name: string;
}

On the client I'm using React-Native and URQL but when I submit to the file to the server I'm getting the following error.

errors: [
    GraphQLError: Variable "$input" got invalid value { uri: "file:///var/mobile/Containers/Data/Application/6833698F-0EB9-4FCE-AD0F-8DBE529CC55C/Library/Caches/ExponentExperienceData/%2540crispen_dev%252Fmobile/ImagePicker/2DD6F719-DD56-444B-A486-E272FC67793A.jpg", name: "IMG_6129.jpg", type: "image/png" } at "input.image"; Upload value invalid.
....
Graphql validation error

What maybe possibly the problem here?

CrispenGari commented 1 year ago

I just figure out that the problem was with my client as i tested the API using postman and worked

annibuliful commented 1 year ago

@CrispenGari would you mind to share your solution(sample code) because I get the same issue

CrispenGari commented 1 year ago

@annibuliful Is your your API working when you test it with clients like Postman?

annibuliful commented 1 year ago

Not at all so i need your suggestion to test it without API testing like Postman, Insomnia because they don't work

CrispenGari commented 1 year ago

I need to know which graphql client are you using, because the last time i faced this error, i realised that the problem wasn’t with my fastify graphql-sever but with my urql client on react native.

annibuliful commented 1 year ago

Is it possible to use just only fetch API? I'm following this article https://www.floriangaechter.com/posts/graphql-file-uploading/

CrispenGari commented 1 year ago

Yes it is possible I tested it on my Graphql server and it seems to work without problems. You can follow along this is my graphql-sever

So I tested the file upload locally by creating a simple html and javascript file and in my html i had the following

<body>
  <input type="file" id="picture" />
  <button id="button">Upload File</button>
</body>

In my javascript file i had the following code in it

let file = null;

document.getElementById("picture").addEventListener("change", (e) => {
  file = e.target.files[0];
});

document.getElementById("button").addEventListener("click", async () => {
  const formData = new FormData();
  const map = `{"0":["variables.picture"]}`;
  const operations = `{"query":"mutation UploadFile($picture: Upload!){  uploadFile(picture: $picture)}"}`;
  formData.append("operations", operations);
  formData.append("map", map);
  formData.append("0", file);
  console.log({ file });

  const res = await fetch("http://localhost:3001/graphql", {
    body: formData,
    method: "post",
  });
  const data = res.json();
  console.log({ data });
});

The url endpoint for my server was running locally at http://localhost:3001/graphql