Skip to content

Instantly share code, notes, and snippets.

@sescobb27
Last active May 22, 2024 23:23
Show Gist options
  • Save sescobb27/8e42d39a78bddb0a728d260a0f2c29f6 to your computer and use it in GitHub Desktop.
Save sescobb27/8e42d39a78bddb0a728d260a0f2c29f6 to your computer and use it in GitHub Desktop.
absinthe integration with apollo's file upload
defmodule MyWeb.UploadFileTest do
use MyWeb.ConnCase
@email_suppressions_mutation """
mutation UploadFile($file: Upload!) {
uploadFile (file: $file) {
success
message
}
}
"""
setup do
path = Path.join(["test", "fixtures", "suppressions_unsubscribes.csv"])
csv = %Plug.Upload{
path: path,
filename: "suppressions_unsubscribes.csv",
content_type: "text/csv"
}
{:ok, csv: csv}
end
describe "apollo uploads" do
test "suppress emails from file", %{conn: conn, account: account, user: user, csv: csv} do
variables = %{
"file" => nil
}
file_mapper = Jason.encode!(%{"file" => ["variables.file"]})
operations = Jason.encode!(%{query: @email_suppressions_mutation, variables: variables})
assert %{"data" => %{"uploadEmailSuppressions" => %{"message" => nil, "success" => true}}} ==
conn
|> graphql_upload(account, user,
operations: operations,
map: file_mapper,
upload: %{name: :file, file: csv}
)
|> json_response(200)
end
end
defp graphql_upload(conn \\ build_conn(), account, user, opts) do
query = Keyword.get(opts, :query)
operations = Keyword.get(opts, :operations)
variables = Keyword.get(opts, :variables)
map = Keyword.get(opts, :map)
%{name: name, file: file} = Keyword.fetch!(opts, :upload)
body =
Map.reject(
%{
name => file,
query: query,
variables: variables,
operations: operations,
map: map
},
fn {_k, v} -> is_nil(v) end
)
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/v1/graphql", body)
end
end
pipeline :apollo_uploads do
plug MyWeb.Plugs.UrlqlUpload
end
scope "/v1" do
pipe_through [:auth, :accept_json, :apollo_uploads]
forward "/graphql", Absinthe.Plug, schema: MyWeb.Graphql.Schema
end
defmodule CustomerApiWeb.Plugs.UrlqlUpload do
@moduledoc """
Absinthe plug to support Apollo upload format.
Implementation of https://github.com/jaydenseric/graphql-multipart-request-spec which
is what urlql uses.
based on: https://github.com/shavit/absinthe-upload
"""
@behaviour Plug
import Plug.Conn, only: [get_req_header: 2]
@impl true
def init(conn), do: conn
# body_params must be already fetched and parsed
# NOTE: this doesn't support batch operations
@impl true
def call(%{body_params: body_params} = conn, _)
when is_map_key(body_params, "operations") and is_map_key(body_params, "map") do
# 'operations' contains the JSON payload
# 'map' contains the JSON mapping between body params keys and variables
with ["multipart/form-data" <> _] <- get_req_header(conn, "content-type"),
{:ok, file_mapper} = Jason.decode(body_params["map"]),
{:ok, %{"query" => query, "variables" => variables}} <-
Jason.decode(body_params["operations"]) do
uploads = get_uploads(body_params, file_mapper)
mapped_variables = add_uploads_to_variables(uploads, variables)
Map.update!(conn, :params, fn params ->
params
|> Map.drop(["operations", "map" | Map.keys(file_mapper)])
|> Map.merge(%{"query" => query, "variables" => mapped_variables})
|> Map.merge(uploads)
end)
else
# if request is not a multipart or body doesn't have an operation along
# side a map of vars mapping to uploads continue (see `graphql-multipart-request-spec`)
_ -> conn
end
end
def call(conn, _), do: conn
defp get_uploads(body_params, file_mapper) do
Map.new(file_mapper, fn {file_key, [path]} ->
key = path |> String.split(".") |> List.last()
value = body_params[file_key]
{key, value}
end)
end
defp add_uploads_to_variables(uploads, variables) do
Map.new(uploads, fn {key, _} ->
{key, key}
end)
|> Enum.into(variables)
end
end
defmodule MyWeb.Plugs.UrlqlUploadTest do
use MyWeb.ConnCase
alias MyWeb.Plugs.UrlqlUpload
setup do
path = Path.join(["test", "fixtures", "suppressions_unsubscribes.csv"])
file = %Plug.Upload{
path: path,
filename: "suppressions_unsubscribes.csv",
content_type: "text/csv"
}
{:ok, file: file}
end
test "conn/2 transforms request body into absinthe format - indexes", %{file: file} do
params_ = %{
"0" => file,
"map" => "{\"0\":[\"variables.attachment.0\"]}",
"operations" =>
"{\"query\":\"mutation DemoUpload($attachment: Upload) {\\n demoUpload(attachment: $attachment)\\n}\",\"variables\":{\"attachment\":[null]},\"operationName\":\"DemoUpload\"}"
}
opts = []
conn =
build_conn(:post, "/v1/graphql", params_)
|> put_req_header("content-type", "multipart/form-data")
|> UrlqlUpload.call(opts)
assert %{
"0" => file,
"query" =>
"mutation DemoUpload($attachment: Upload) { demoUpload(attachment: $attachment)}",
"variables" => %{"0" => "0", "attachment" => [nil]}
} == conn.params
end
test "conn/2 transforms request body into absinthe format - named", %{file: file} do
params_ = %{
"0" => file,
"map" => "{\"0\":[\"variables.attachment\"]}",
"operations" =>
"{\"query\":\"mutation DemoUpload($attachment: Upload) { demoUpload(attachment: $attachment)}\",\"variables\":{\"attachment\":null}}"
}
opts = []
conn =
build_conn(:post, "/v1/graphql", params_)
|> put_req_header("content-type", "multipart/form-data")
|> UrlqlUpload.call(opts)
assert %{
"attachment" => %Plug.Upload{
path: "/tmp/plug-1605/multipart-1605259564-858222931410900-3",
content_type: "text/rtf",
filename: "San Francisco.rtf"
},
"query" =>
"mutation DemoUpload($attachment: Upload) { demoUpload(attachment: $attachment)}",
"variables" => %{"attachment" => "attachment"}
} == conn.params
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment