Uploading an image using absinthe (no controller pure api project)

Hello. I’ve been trying to upload an image from the frontend without vail. I was hoping to get some advice in where within my code I might be making a mistake.

For my frontend, I’m mainly setting the image through the use of FormData and sending it back to the backend as

	const handleProductCreation = () => createProduct({ variables: buildForm() });
	const handeSetImage = ({ target }) => {
		const data = new FormData();
		data.append('fileData', target.files[0].name);

		setImage(target.files[0]);
	};

	const onSubmit = (e) => {
		e.preventDefault();
		handleProductCreation();
	};

	const buildForm = () => {
		let storeID = 2;
		return {
			productName,
			productDescription,
			productPrice,
			productType,
			isReturnable,
			storeID,
			fileData
		};
	};

This all goes through a graphql mutation that I created. The request looks like:

export const CREATE_PRODUCT_MUTATION = gql`
	mutation CreateProduct(
		$storeID: Int!
		$productName: String!
		$productDescription: String!
		$productPrice: Decimal!
		$productType: Int!
		$isReturnable: Boolean!
		$fileData: Upload!
	) {
		createProduct(
			product: {
				productName: $productName
				productDescription: $productDescription
				productPrice: $productPrice
				productType: $productType
				isReturnable: $isReturnable
			}
			storeId: $storeID
			fileData: $fileData
		) {
			id
			productName
			productDescription
			productPrice
		}
	}
`;

I’m not sure why I’m not getting any information within my api request. It continues to say first that fileData => {} and an error in console as Uncaught (in promise) Error: GraphQL error: Argument "fileData" has invalid value $fileData. I’m not sure why this might be happening. Within the backend, the schema has the code as

    @desc "List a new product"
    field :create_product, :product do
      arg(:product, :new_product)
      arg(:store_id, :integer)
      arg(:file_data, :upload)
      resolve(&Resolvers.Stores.create_product/3)
    end

All help in what I’m doing wrong or pointing in the right direction is appreciated. Thank you in advance for the help, and also for your time in the help.

Have you seen this

What logs do you get on the backend?

Hello Ben,

Thank you for looking at this one and sorry for my late reply.

On the backend I’m seeing

[debug] Processing with Absinthe.Plug
  Parameters: %{"operationName" => "CreateProduct", "query" => "mutation CreateProduct($storeID: Int!, $productName: String!, $productDescription: String!, $productPrice: Decimal!, $productType: Int!, $isReturnable: Boolean!, $fileData: Upload!) {\n  createProduct(product: {productName: $productName, productDescription: $productDescription, productPrice: $productPrice, productType: $productType, isReturnable: $isReturnable}, storeId: $storeID, fileData: $fileData) {\n    id\n    productName\n    productDescription\n    productPrice\n    __typename\n  }\n}\n", "variables" => %{"fileData" => %{}, "isReturnable" => false, "productDescription" => "", "productName" => "", "productPrice" => 1, "productType" => 1, "storeID" => 2}}

I notice that the fileData continues to be an empty object. I was wondering if there are additional steps needed to be taken in order to have a file successfully be send to the backend?

Thank you for the help and advice in regards to this one.

I’ve seen this article but didn’t notice until a second read that additional packages are required in order to successfully get it to work. Was hoping to possibly avoid adding additional packages if I can (although will continue reading and possibly give it a try if an alternative isn’t possible).

Really appreciate the reply and help on this one :smiley:

1 Like

You are welcome

Hello Ben,

While looking through the internet I did come across a file you’ve created (possibly the entire package) known as absinthe_plug. Could this be the reason why files are not being loaded since my GraphQL basically isn’t accepting a multipart/form-data content type? To be honest when I first saw that meaning within File Upload for absinthe, it didn’t make any sense to me. But I’m starting to think it’s possible the absinthe out of the box does not actually accept file uploads due to the send request type resulting in files not being able to be read fileData: {} result.

If this is wrong, please let me know what you might recommend. Thank you again for the help and also contributing to the creation/expansion of absinthe :smiley:

1 Like

Absinthe out of the box doesn’t even support HTTP. Absinthe doesn’t know anything about transport mechanisms. Absinthe.Plug is what lets you wire in Absinthe to plug to answer HTTP requests. If you want ot upload a file alongside a GraphQL query your GraphQL client will need to be able to make a multipart/form-data request.

The way that works, is that you have a part that contains the actual file content, and you name that part something, let’s say theFile. Then, your GraphQL variable fileData should be the NAME of the http part, not the data itself. So in this case it should be the string "theFile". Then it will work. There’s a complete example with curl as the client here: https://hexdocs.pm/absinthe_plug/1.5.0-alpha.0/Absinthe.Plug.Types.html#content

3 Likes

Hello Ben,

Thank you for replying and helping me better explain what is happening. I am still a little confused so I was hoping to say what I understand and hopefully can get some clarity.

I noticed within my application I did have multipart accepted within a request since within my endpoint.ex I have

    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()```

   ROUTE
    scope "/" do
    pipe_through(:api)

    forward("/api", Absinthe.Plug, schema: HuntersWeb.Schema.Schema)

The one part that confuses me is how data is being send to the backend. I do understand the part of naming I believe. An example would be

	const handeSetImage = ({ target }) => {
		const data = new FormData();
		data.append('fileData', target.files[0]);

		setImage('fileData');
	};

As we can see. When a person uploads the file, we append that file to a name of fileData and send that to the backend. Was this how we should approach it? when I do I’m seeing to things that isn’t right. First the backend shows

[debug] QUERY OK source="stores" db=0.4ms
SELECT s0."id", s0."city", s0."country", s0."state", s0."store_name", s0."user_id", s0."inserted_at", s0."updated_at", s0."user_id" FROM "stores" AS s0 WHERE (s0."user_id" = $1) [60]
[debug] ABSINTHE schema=HuntersWeb.Schema.Schema variables=%{"fileData" => "fileData", "isReturnable" => false, "productDescription" => "", "productName" => "", "productPrice" => 1, "productType" => 1, "storeID" => 2}

and an error of Uncaught (in promise) Error: GraphQL error: Argument "fileData" has invalid value $fileData.

Should we be sending the files set to a different variable while the name we are spending to it, in this case the fileData will be set to fileData. For example, $fileData = 'thefile'. Within our graphQL it’ll have fileData: $fileData. What I’m a little confused about is how are we sending that FormData that has the file information.

My last question is you’ve mentioned curl. Is that what we should be expecting to see on the backend if everything is set up correctly? Thank you again for the help and clarity on this one and sorry for being a little slow in understanding this one.

While looking at the example online. I guess it does confuse me a little as well.

 mutation do
    field :upload_file, :string do
      arg :users, non_null(:upload)
      arg :metadata, :upload

      resolve fn args, _ ->
        args.users # this is a `%Plug.Upload{}` struct.

        {:ok, "success"}
      end
    end
  end

From this mutation. From what I can understand, the users will be a string with the name that we except the data to return while the metadata will be where we send all the files?

No, curl is an HTTP client you use from the command line, the documentation there is just an example for people who are familiar with curl. It has nothing to do with Elixir.

What the curl example DOES show is the overall structure of the HTTP request. Let me try to sketch that here, because I think your Javascript code isn’t sending the javascript request properly. What absinthe expects is a multipart HTTP request. The way that those work is that there are a bunch of sub parts to the request, and each part gets a name, and a value:

---------------------- Start of a part ----------------------
name: query
value: mutation CreateProduct($storeID: Int!, $productName: String!, $productDescription: String!, $productPrice: Decimal!, $productType: Int!, $isReturnable: Boolean!, $fileData: Upload!) { ... }
---------------------- End of a part -----------------------
---------------------- Start of a part ----------------------
name: variables
value: {"fileData":"theFile", ... other variables here}
---------------------- End of a part -----------------------
---------------------- Start of a part ----------------------
name: theFile
value: <file content goes here>
---------------------- End of a part -----------------------

So, in this case there are 3 parts. A part named query that is expected to have the graphql query, a part named variables that is expected to have the graphql variables, and a part containing the file data with whatever name you want to give it, in this case I named it theFile. Importantly, notice {"fileData":"theFile". Your upload variable tells Absinthe.Plug "hey, go look for the HTTP part named theFile for the upload.

Unfortunately, I cannot really guide you about how to write javascript to make this happen, I don’t write javascript. I highly recommend using the additional package already linked if you’re using Apollo already.