Last active
May 22, 2024 23:23
-
-
Save sescobb27/8e42d39a78bddb0a728d260a0f2c29f6 to your computer and use it in GitHub Desktop.
absinthe integration with apollo's file upload
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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