zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

graphiql.ex (12747B)


      1 defmodule Absinthe.Plug.GraphiQL do
      2   @moduledoc """
      3   Provides a GraphiQL interface.
      4 
      5 
      6   ## Examples
      7 
      8   The examples here are shown in
      9 
     10   Serve the GraphiQL "advanced" interface at `/graphiql`, but only in
     11   development:
     12 
     13       if Mix.env == :dev do
     14         forward "/graphiql",
     15           to: Absinthe.Plug.GraphiQL,
     16           init_opts: [schema: MyAppWeb.Schema]
     17       end
     18 
     19   Use the "simple" interface (original GraphiQL) instead:
     20 
     21       forward "/graphiql",
     22         to: Absinthe.Plug.GraphiQL,
     23         init_opts: [
     24           schema: MyAppWeb.Schema,
     25           interface: :simple
     26         ]
     27 
     28   Finally there is also support for GraphiQL Playground
     29   https://github.com/graphcool/graphql-playground
     30 
     31       forward "/graphiql",
     32         to: Absinthe.Plug.GraphiQL,
     33         init_opts: [
     34           schema: MyAppWeb.Schema,
     35           interface: :playground
     36         ]
     37 
     38 
     39   ## Interface Selection
     40 
     41   The GraphiQL interface can be switched using the `:interface` option.
     42 
     43   - `:advanced` (default) will serve the [GraphiQL Workspace](https://github.com/OlegIlyenko/graphiql-workspace) interface from Oleg Ilyenko.
     44   - `:simple` will serve the original [GraphiQL](https://github.com/graphql/graphiql) interface from Facebook.
     45   - `:playground` will serve the [GraphQL Playground](https://github.com/graphcool/graphql-playground) interface from Graphcool.
     46 
     47   See `Absinthe.Plug` for the other  options.
     48 
     49   ## Default Headers
     50 
     51   You can optionally provide default headers if the advanced interface (GraphiQL Workspace) is selected.
     52   Note that you may have to clean up your existing workspace by clicking the trashcan icon in order to see the newly set default headers.
     53 
     54       forward "/graphiql",
     55         to: Absinthe.Plug.GraphiQL,
     56         init_opts: [
     57           schema: MyAppWeb.Schema,
     58           default_headers: {__MODULE__, :graphiql_headers}
     59         ]
     60 
     61       def graphiql_headers do
     62         %{
     63           "X-CSRF-Token" => Plug.CSRFProtection.get_csrf_token(),
     64           "X-Foo" => "Bar"
     65         }
     66       end
     67 
     68   You can also provide a function that takes a conn argument if you need to access connection data
     69   (e.g. if you need to set an Authorization header based on the currently logged-in user).
     70 
     71       def graphiql_headers(conn) do
     72         %{
     73           "Authorization" => "Bearer " <> conn.assigns[:token]
     74         }
     75       end
     76 
     77   ## Default URL
     78 
     79   You can also optionally set the default URL to be used for sending the queries to.
     80   This only applies to the advanced interface (GraphiQL Workspace) and the GraphQL Playground.
     81 
     82       forward "/graphiql",
     83         to: Absinthe.Plug.GraphiQL,
     84         init_opts: [
     85           schema: MyAppWeb.Schema,
     86           default_url: "https://api.mydomain.com/graphql"
     87         ]
     88 
     89   This option also accepts a function:
     90 
     91       forward "/graphiql",
     92         to: Absinthe.Plug.GraphiQL,
     93         init_opts: [
     94           schema: MyAppWeb.Schema,
     95           default_url: {__MODULE__, :graphiql_default_url}
     96         ]
     97 
     98       def graphiql_default_url(conn) do
     99         conn.assigns[:graphql_url]
    100       end
    101 
    102   ## Socket URL
    103 
    104   You can also optionally set the default websocket URL to be used for subscriptions.
    105   This only applies to the advanced interface (GraphiQL Workspace) and the GraphQL Playground.
    106 
    107       forward "/graphiql",
    108         to: Absinthe.Plug.GraphiQL,
    109         init_opts: [
    110           schema: MyAppWeb.Schema,
    111           socket_url: "wss://api.mydomain.com/socket"
    112         ]
    113 
    114   This option also accepts a function:
    115 
    116       forward "/graphiql",
    117         to: Absinthe.Plug.GraphiQL,
    118         init_opts: [
    119           schema: MyAppWeb.Schema,
    120           socket_url: {__MODULE__, :graphiql_socket_url}
    121         ]
    122 
    123       def graphiql_socket_url(conn) do
    124         conn.assigns[:graphql_socket_url]
    125       end
    126   """
    127 
    128   require EEx
    129 
    130   @graphiql_template_path Path.join(__DIR__, "graphiql")
    131 
    132   EEx.function_from_file(
    133     :defp,
    134     :graphiql_html,
    135     Path.join(@graphiql_template_path, "graphiql.html.eex"),
    136     [:query_string, :variables_string, :result_string, :socket_url, :assets]
    137   )
    138 
    139   EEx.function_from_file(
    140     :defp,
    141     :graphiql_workspace_html,
    142     Path.join(@graphiql_template_path, "graphiql_workspace.html.eex"),
    143     [:query_string, :variables_string, :default_headers, :default_url, :socket_url, :assets]
    144   )
    145 
    146   EEx.function_from_file(
    147     :defp,
    148     :graphiql_playground_html,
    149     Path.join(@graphiql_template_path, "graphiql_playground.html.eex"),
    150     [:default_url, :socket_url, :assets]
    151   )
    152 
    153   @behaviour Plug
    154 
    155   import Plug.Conn
    156 
    157   @type opts :: [
    158           schema: atom,
    159           adapter: atom,
    160           path: binary,
    161           context: map,
    162           json_codec: atom | {atom, Keyword.t()},
    163           interface: :playground | :advanced | :simple,
    164           default_headers: {module, atom},
    165           default_url: binary,
    166           assets: Keyword.t(),
    167           socket: module,
    168           socket_url: binary
    169         ]
    170 
    171   @doc false
    172   @spec init(opts :: opts) :: map
    173   def init(opts) do
    174     assets = Absinthe.Plug.GraphiQL.Assets.get_assets()
    175 
    176     opts
    177     |> Absinthe.Plug.init()
    178     |> Map.put(:interface, Keyword.get(opts, :interface) || :advanced)
    179     |> Map.put(:default_headers, Keyword.get(opts, :default_headers))
    180     |> Map.put(:default_url, Keyword.get(opts, :default_url))
    181     |> Map.put(:assets, assets)
    182     |> Map.put(:socket, Keyword.get(opts, :socket))
    183     |> Map.put(:socket_url, Keyword.get(opts, :socket_url))
    184     |> Map.put(:default_query, Keyword.get(opts, :default_query, ""))
    185     |> set_pipeline
    186   end
    187 
    188   @doc false
    189   def call(conn, config) do
    190     case html?(conn) do
    191       true -> do_call(conn, config)
    192       _ -> Absinthe.Plug.call(conn, config)
    193     end
    194   end
    195 
    196   defp html?(conn) do
    197     Plug.Conn.get_req_header(conn, "accept")
    198     |> List.first()
    199     |> case do
    200       string when is_binary(string) ->
    201         String.contains?(string, "text/html")
    202 
    203       _ ->
    204         false
    205     end
    206   end
    207 
    208   defp do_call(conn, %{interface: interface} = config) do
    209     config =
    210       config
    211       |> handle_default_headers(conn)
    212       |> put_config_value(:default_url, conn)
    213       |> handle_socket_url(conn)
    214 
    215     with {:ok, conn, request} <- Absinthe.Plug.Request.parse(conn, config),
    216          {:process, request} <- select_mode(request),
    217          {:ok, request} <- Absinthe.Plug.ensure_processable(request, config),
    218          :ok <- Absinthe.Plug.Request.log(request, config.log_level) do
    219       conn_info = %{
    220         conn_private: (conn.private[:absinthe] || %{}) |> Map.put(:http_method, conn.method)
    221       }
    222 
    223       {conn, result} = Absinthe.Plug.run_request(request, conn, conn_info, config)
    224 
    225       case result do
    226         {:ok, result} ->
    227           # GraphiQL doesn't batch requests, so the first query is the only one
    228           query = hd(request.queries)
    229           {:ok, conn, result, query.variables, query.document || ""}
    230 
    231         {:error, {:http_method, _}, _} ->
    232           query = hd(request.queries)
    233           {:http_method_error, query.variables, query.document || ""}
    234 
    235         other ->
    236           other
    237       end
    238     end
    239     |> case do
    240       {:ok, conn, result, variables, query} ->
    241         query = query |> js_escape
    242 
    243         var_string =
    244           variables
    245           |> config.json_codec.module.encode!(pretty: true)
    246           |> js_escape
    247 
    248         result =
    249           result
    250           |> config.json_codec.module.encode!(pretty: true)
    251           |> js_escape
    252 
    253         config =
    254           %{
    255             query: query,
    256             var_string: var_string,
    257             result: result
    258           }
    259           |> Map.merge(config)
    260 
    261         conn
    262         |> render_interface(interface, config)
    263 
    264       {:input_error, msg} ->
    265         conn
    266         |> send_resp(400, msg)
    267 
    268       :start_interface ->
    269         conn
    270         |> render_interface(interface, config)
    271 
    272       {:http_method_error, variables, query} ->
    273         query = query |> js_escape
    274 
    275         var_string =
    276           variables
    277           |> config.json_codec.module.encode!(pretty: true)
    278           |> js_escape
    279 
    280         config =
    281           %{
    282             query: query,
    283             var_string: var_string
    284           }
    285           |> Map.merge(config)
    286 
    287         conn
    288         |> render_interface(interface, config)
    289 
    290       {:error, error, _} when is_binary(error) ->
    291         conn
    292         |> send_resp(500, error)
    293     end
    294   end
    295 
    296   defp set_pipeline(config) do
    297     config
    298     |> Map.put(:additional_pipeline, config.pipeline)
    299     |> Map.put(:pipeline, {__MODULE__, :pipeline})
    300   end
    301 
    302   @doc false
    303   def pipeline(config, opts) do
    304     {module, fun} = config.additional_pipeline
    305 
    306     apply(module, fun, [config, opts])
    307     |> Absinthe.Pipeline.insert_after(
    308       Absinthe.Phase.Document.CurrentOperation,
    309       [
    310         Absinthe.GraphiQL.Validation.NoSubscriptionOnHTTP
    311       ]
    312     )
    313   end
    314 
    315   @spec select_mode(request :: Absinthe.Plug.Request.t()) ::
    316           :start_interface | {:process, Absinthe.Plug.Request.t()}
    317   defp select_mode(%{queries: [%Absinthe.Plug.Request.Query{document: nil}]}),
    318     do: :start_interface
    319 
    320   defp select_mode(request), do: {:process, request}
    321 
    322   defp find_socket_path(conn, socket) do
    323     if endpoint = conn.private[:phoenix_endpoint] do
    324       Enum.find_value(endpoint.__sockets__, :error, fn
    325         # Phoenix 1.4
    326         {path, ^socket, _opts} -> {:ok, path}
    327         # Phoenix <= 1.3
    328         {path, ^socket} -> {:ok, path}
    329         _ -> false
    330       end)
    331     else
    332       :error
    333     end
    334   end
    335 
    336   @render_defaults %{var_string: "", results: ""}
    337 
    338   @spec render_interface(Plug.Conn.t(), :advanced | :simple | :playground, map()) ::
    339           Plug.Conn.t()
    340   defp render_interface(conn, interface, opts)
    341 
    342   defp render_interface(conn, :simple, opts) do
    343     opts = opts_with_default(opts)
    344 
    345     graphiql_html(
    346       opts[:query],
    347       opts[:var_string],
    348       opts[:result],
    349       opts[:socket_url],
    350       opts[:assets]
    351     )
    352     |> rendered(conn)
    353   end
    354 
    355   defp render_interface(conn, :advanced, opts) do
    356     opts = opts_with_default(opts)
    357 
    358     graphiql_workspace_html(
    359       opts[:query],
    360       opts[:var_string],
    361       opts[:default_headers],
    362       default_url(opts[:default_url]),
    363       opts[:socket_url],
    364       opts[:assets]
    365     )
    366     |> rendered(conn)
    367   end
    368 
    369   defp render_interface(conn, :playground, opts) do
    370     opts = opts_with_default(opts)
    371 
    372     graphiql_playground_html(
    373       default_url(opts[:default_url]),
    374       opts[:socket_url],
    375       opts[:assets]
    376     )
    377     |> rendered(conn)
    378   end
    379 
    380   defp opts_with_default(opts) do
    381     defaults = Map.put(@render_defaults, :query, opts[:default_query])
    382 
    383     Map.merge(defaults, opts)
    384   end
    385 
    386   defp default_url(nil), do: "window.location.origin + window.location.pathname"
    387   defp default_url(url), do: "'#{url}'"
    388 
    389   @spec rendered(String.t(), Plug.Conn.t()) :: Plug.Conn.t()
    390   defp rendered(html, conn) do
    391     conn
    392     |> put_resp_content_type("text/html")
    393     |> send_resp(200, html)
    394   end
    395 
    396   defp js_escape(string) do
    397     string
    398     |> String.replace(~r/\n/, "\\n")
    399     |> String.replace(~r/'/, "\\'")
    400   end
    401 
    402   defp handle_default_headers(config, conn) do
    403     case get_config_val(config, :default_headers, conn) do
    404       nil ->
    405         Map.put(config, :default_headers, "[]")
    406 
    407       val when is_map(val) ->
    408         header_string =
    409           val
    410           |> Enum.map(fn {k, v} -> %{"name" => k, "value" => v} end)
    411           |> config.json_codec.module.encode!(pretty: true)
    412 
    413         Map.put(config, :default_headers, header_string)
    414 
    415       val ->
    416         raise "invalid default headers: #{inspect(val)}"
    417     end
    418   end
    419 
    420   defp function_arity(module, fun) do
    421     Enum.find([1, 0], nil, &function_exported?(module, fun, &1))
    422   end
    423 
    424   defp put_config_value(config, key, conn) do
    425     case get_config_val(config, key, conn) do
    426       nil ->
    427         config
    428 
    429       val when is_binary(val) ->
    430         Map.put(config, key, val)
    431 
    432       val ->
    433         raise "invalid #{key}: #{inspect(val)}"
    434     end
    435   end
    436 
    437   defp get_config_val(config, key, conn) do
    438     case Map.get(config, key) do
    439       {module, fun} when is_atom(fun) ->
    440         case function_arity(module, fun) do
    441           1 ->
    442             apply(module, fun, [conn])
    443 
    444           0 ->
    445             apply(module, fun, [])
    446 
    447           _ ->
    448             raise "function for #{key}: {#{module}, #{fun}} is not exported with arity 1 or 0"
    449         end
    450 
    451       val ->
    452         val
    453     end
    454   end
    455 
    456   defp handle_socket_url(config, conn) do
    457     config
    458     |> put_config_value(:socket_url, conn)
    459     |> normalize_socket_url(conn)
    460   end
    461 
    462   defp normalize_socket_url(%{socket_url: nil, socket: socket} = config, conn) do
    463     url =
    464       with {:ok, socket_path} <- find_socket_path(conn, socket) do
    465         "`${protocol}//${window.location.host}#{socket_path}`"
    466       else
    467         _ -> "''"
    468       end
    469 
    470     %{config | socket_url: url}
    471   end
    472 
    473   defp normalize_socket_url(%{socket_url: url} = config, _) do
    474     %{config | socket_url: "'#{url}'"}
    475   end
    476 end