request.ex (5284B)
1 defmodule Absinthe.Plug.Request do 2 @moduledoc false 3 4 # This struct is the default return type of Request.parse. 5 # It contains parsed Request structs -- typically just one, 6 # but when `batched` is set to true, it can be multiple. 7 # 8 # extra_keys: e.g. %{"id": ...} sent by react-relay-network-layer, 9 # which need to be merged back into the list of final results 10 # before sending it to the client 11 12 import Plug.Conn 13 alias Absinthe.Plug.Request.Query 14 15 defstruct queries: [], 16 batch: false, 17 extra_keys: [] 18 19 @type t :: %__MODULE__{ 20 queries: list(Absinthe.Plug.Request.Query.t()), 21 batch: boolean(), 22 extra_keys: list(map()) 23 } 24 25 @spec parse(Plug.Conn.t(), map) :: {:ok, Plug.Conn.t(), t} | {:input_error, String.t()} 26 def parse(conn, config) do 27 root_value = 28 config 29 |> Map.get(:root_value, %{}) 30 |> Map.merge(extract_root_value(conn)) 31 32 context = 33 config 34 |> Map.get(:context, %{}) 35 |> Map.merge(extract_context(conn, config)) 36 37 config = Map.merge(config, %{root_value: root_value, context: context}) 38 39 config = 40 (conn.private[:absinthe] || %{}) 41 |> Enum.reduce(config, fn 42 # keys we already handled 43 {k, _}, config when k in [:context, :root_value] -> 44 config 45 46 {k, v}, config -> 47 Map.put(config, k, v) 48 end) 49 50 with {:ok, conn, body, params} <- extract_body_and_params(conn, config), 51 true <- valid_request?(params) do 52 {:ok, conn, build_request(body, params, config, batch?: is_batch?(params))} 53 end 54 end 55 56 # Plug puts parsed params under the "_json" key when the 57 # structure is not a map; otherwise it's just the keys themselves, 58 # and they may sit in the body or in the params 59 60 defp is_batch?(params) do 61 Map.has_key?(params, "_json") && is_list(params["_json"]) 62 end 63 64 defp valid_request?(%{"_json" => json}) when is_list(json) do 65 Enum.all?(json, &is_map(&1)) || 66 {:input_error, "Invalid request structure. Expecting a list of objects."} 67 end 68 69 defp valid_request?(%{"_json" => json}) when is_binary(json) do 70 {:input_error, "Invalid request structure. Expecting an object or list of objects."} 71 end 72 73 defp valid_request?(_params), do: true 74 75 defp build_request(_body, params, config, batch?: true) do 76 queries = 77 Enum.map(params["_json"], fn query -> 78 Query.parse("", query, config) 79 end) 80 81 extra_keys = 82 Enum.map(params["_json"], fn query -> 83 Map.drop(query, ["query", "variables"]) 84 end) 85 86 %__MODULE__{ 87 queries: queries, 88 batch: true, 89 extra_keys: extra_keys 90 } 91 end 92 93 defp build_request(body, params, config, batch?: false) do 94 queries = 95 body 96 |> Query.parse(params, config) 97 |> List.wrap() 98 99 %__MODULE__{ 100 queries: queries, 101 batch: false 102 } 103 end 104 105 # 106 # BODY / PARAMS 107 # 108 109 @spec extract_body_and_params(Plug.Conn.t(), map()) :: {:ok, Plug.Conn.t(), String.t(), map()} 110 defp extract_body_and_params(%{body_params: %{"query" => _}} = conn, _config) do 111 conn = fetch_query_params(conn) 112 {:ok, conn, "", conn.params} 113 end 114 115 defp extract_body_and_params(%{body_params: %{"_json" => _}} = conn, config) do 116 extract_body_and_params_batched(conn, "", config) 117 end 118 119 defp extract_body_and_params(conn, config) do 120 with {:ok, body, conn} <- read_body(conn) do 121 extract_body_and_params_batched(conn, body, config) 122 end 123 end 124 125 defp convert_operations_param(conn = %{params: %{"operations" => operations}}) 126 when is_binary(operations) do 127 put_in(conn.params["_json"], conn.params["operations"]) 128 |> Map.delete("operations") 129 end 130 131 defp convert_operations_param(conn), do: conn 132 133 defp extract_body_and_params_batched(conn, body, config) do 134 conn = 135 conn 136 |> fetch_query_params() 137 |> convert_operations_param() 138 139 with %{"_json" => string} = params when is_binary(string) <- conn.params, 140 {:ok, decoded} <- config.json_codec.module.decode(string) do 141 {:ok, conn, body, %{params | "_json" => decoded}} 142 else 143 {:error, {:invalid, token, pos}} -> 144 {:input_error, "Could not parse JSON. Invalid token `#{token}` at position #{pos}"} 145 146 {:error, %{__exception__: true} = exception} -> 147 {:input_error, "Could not parse JSON. #{Exception.message(exception)}"} 148 149 %{} -> 150 {:ok, conn, body, conn.params} 151 end 152 end 153 154 # 155 # CONTEXT 156 # 157 158 @spec extract_context(Plug.Conn.t(), map) :: map 159 defp extract_context(conn, config) do 160 config.context 161 |> Map.merge(conn.private[:absinthe][:context] || %{}) 162 |> Map.merge(uploaded_files(conn)) 163 end 164 165 # 166 # UPLOADED FILES 167 # 168 169 @spec uploaded_files(Plug.Conn.t()) :: map 170 defp uploaded_files(conn) do 171 files = 172 conn.params 173 |> Enum.filter(&match?({_, %Plug.Upload{}}, &1)) 174 |> Map.new() 175 176 %{ 177 __absinthe_plug__: %{ 178 uploads: files 179 } 180 } 181 end 182 183 # 184 # ROOT VALUE 185 # 186 187 @spec extract_root_value(Plug.Conn.t()) :: any 188 defp extract_root_value(conn) do 189 conn.private[:absinthe][:root_value] || %{} 190 end 191 192 @spec log(t, atom) :: :ok 193 def log(request, level) do 194 Enum.each(request.queries, &Query.log(&1, level)) 195 :ok 196 end 197 end