zf

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

plug.ex (19386B)


      1 defmodule Absinthe.Plug do
      2   @moduledoc """
      3   A plug for using [Absinthe](https://hex.pm/packages/absinthe) (GraphQL).
      4 
      5   ## Usage
      6 
      7   In your router:
      8 
      9       plug Plug.Parsers,
     10         parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
     11         pass: ["*/*"],
     12         json_decoder: Jason
     13 
     14       plug Absinthe.Plug,
     15         schema: MyAppWeb.Schema
     16 
     17   If you want only `Absinthe.Plug` to serve a particular route, configure your
     18   router like:
     19 
     20       plug Plug.Parsers,
     21         parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
     22         pass: ["*/*"],
     23         json_decoder: Jason
     24 
     25       forward "/api",
     26         to: Absinthe.Plug,
     27         init_opts: [schema: MyAppWeb.Schema]
     28 
     29   See the documentation on `Absinthe.Plug.init/1` and the `Absinthe.Plug.opts`
     30   type for information on the available options.
     31 
     32   To add support for a GraphiQL interface, add a configuration for
     33   `Absinthe.Plug.GraphiQL`:
     34 
     35       forward "/graphiql",
     36         to: Absinthe.Plug.GraphiQL,
     37         init_opts: [schema: MyAppWeb.Schema]
     38 
     39   For more information, see the API documentation for `Absinthe.Plug`.
     40 
     41   ### Phoenix.Router
     42 
     43   If you are using [Phoenix.Router](https://hexdocs.pm/phoenix/Phoenix.Router.html), `forward` expects different arguments:
     44 
     45   #### Plug.Router
     46 
     47       forward "/graphiql",
     48         to: Absinthe.Plug.GraphiQL,
     49         init_opts: [
     50           schema: MyAppWeb.Schema,
     51           interface: :simple
     52         ]
     53 
     54   #### Phoenix.Router
     55 
     56       forward "/graphiql",
     57         Absinthe.Plug.GraphiQL,
     58          schema: MyAppWeb.Schema,
     59          interface: :simple
     60 
     61   For more information see [Phoenix.Router.forward/4](https://hexdocs.pm/phoenix/Phoenix.Router.html#forward/4).
     62 
     63   ## Before Send
     64 
     65   If you need to set a value (like a cookie) on the connection after resolution
     66   but before values are sent to the client, use the `:before_send` option:
     67 
     68   ```
     69   plug Absinthe.Plug,
     70     schema: MyApp.Schema,
     71     before_send: {__MODULE__, :absinthe_before_send}
     72 
     73   def absinthe_before_send(conn, %Absinthe.Blueprint{} = blueprint) do
     74     if auth_token = blueprint.execution.context[:auth_token] do
     75       put_session(conn, :auth_token, auth_token)
     76     else
     77       conn
     78     end
     79   end
     80   def absinthe_before_send(conn, _) do
     81     conn
     82   end
     83   ```
     84 
     85   The `auth_token` can be placed in the context by using middleware after your
     86   mutation resolve:
     87 
     88   ```
     89   # mutation resolver
     90   resolve fn args, _ ->
     91     case authenticate(args) do
     92       {:ok, token} -> {:ok, %{token: token}}
     93       error -> error
     94     end
     95   end
     96   # middleware afterward
     97   middleware fn resolution, _ ->
     98     with %{value: %{token: token}} <- resolution do
     99       Map.update!(resolution, :context, fn ctx ->
    100         Map.put(ctx, :auth_token, token)
    101       end)
    102     end
    103   end
    104   ```
    105 
    106   ## Included GraphQL Types
    107 
    108   This package includes additional types for use in Absinthe GraphQL schema and
    109   type modules.
    110 
    111   See the documentation on `Absinthe.Plug.Types` for more information.
    112 
    113   ## More Information
    114 
    115   For more on configuring `Absinthe.Plug` and how GraphQL requests are made,
    116   see [the guide](https://hexdocs.pm/absinthe/plug-phoenix.html) at
    117   <http://absinthe-graphql.org>.
    118 
    119   """
    120 
    121   @behaviour Plug
    122   import Plug.Conn
    123   require Logger
    124 
    125   alias __MODULE__.Request
    126 
    127   @init_options [
    128     :adapter,
    129     :context,
    130     :no_query_message,
    131     :json_codec,
    132     :pipeline,
    133     :document_providers,
    134     :schema,
    135     :serializer,
    136     :content_type,
    137     :before_send,
    138     :log_level,
    139     :pubsub,
    140     :analyze_complexity,
    141     :max_complexity,
    142     :transport_batch_payload_key
    143   ]
    144   @raw_options [
    145     :analyze_complexity,
    146     :max_complexity
    147   ]
    148 
    149   @type function_name :: atom
    150 
    151   @typedoc """
    152   - `:adapter` -- (Optional) Absinthe adapter to use (default: `Absinthe.Adapter.LanguageConventions`).
    153   - `:context` -- (Optional) Initial value for the Absinthe context, available to resolvers. (default: `%{}`).
    154   - `:no_query_message` -- (Optional) Message to return to the client if no query is provided (default: "No query document supplied").
    155   - `:json_codec` -- (Optional) A `module` or `{module, Keyword.t}` dictating which JSON codec should be used (default: `Jason`). The codec module should implement `encode!/2` (e.g., `module.encode!(body, opts)`).
    156   - `:pipeline` -- (Optional) `{module, atom}` reference to a 2-arity function that will be called to generate the processing pipeline. (default: `{Absinthe.Plug, :default_pipeline}`).
    157   - `:document_providers` -- (Optional) A `{module, atom}` reference to a 1-arity function that will be called to determine the document providers that will be used to process the request. (default: `{Absinthe.Plug, :default_document_providers}`, which configures `Absinthe.Plug.DocumentProvider.Default` as the lone document provider). A simple list of document providers can also be given. See `Absinthe.Plug.DocumentProvider` for more information about document providers, their role in procesing requests, and how you can define and configure your own.
    158   - `:schema` -- (Required, if not handled by Mix.Config) The Absinthe schema to use. If a module name is not provided, `Application.get_env(:absinthe, :schema)` will be attempt to find one.
    159   - `:serializer` -- (Optional) Similar to `:json_codec` but allows the use of serialization formats other than JSON, like MessagePack or Erlang Term Format. Defaults to whatever is set in `:json_codec`.
    160   - `:content_type` -- (Optional) The content type of the response. Should probably be set if `:serializer` option is used. Defaults to `"application/json"`.
    161   - `:before_send` -- (Optional) Set a value(s) on the connection after resolution but before values are sent to the client.
    162   - `:log_level` -- (Optional) Set the logger level for Absinthe Logger. Defaults to `:debug`.
    163   - `:pubsub` -- (Optional) Pub Sub module for Subscriptions.
    164   - `:analyze_complexity` -- (Optional) Set whether to calculate the complexity of incoming GraphQL queries.
    165   - `:max_complexity` -- (Optional) Set the maximum allowed complexity of the GraphQL query. If a document’s calculated complexity exceeds the maximum, resolution will be skipped and an error will be returned in the result detailing the calculated and maximum complexities.
    166   - `:transport_batch_payload_key` -- (Optional) Set whether or not to nest Transport Batch request results in a `payload` key. Older clients expected this key to be present, but newer clients have dropped this pattern. (default: `true`)
    167 
    168   """
    169   @type opts :: [
    170           schema: module,
    171           adapter: module,
    172           context: map,
    173           json_codec: module | {module, Keyword.t()},
    174           pipeline: {module, atom},
    175           no_query_message: String.t(),
    176           document_providers:
    177             [Absinthe.Plug.DocumentProvider.t(), ...]
    178             | Absinthe.Plug.DocumentProvider.t()
    179             | {module, atom},
    180           analyze_complexity: boolean,
    181           max_complexity: non_neg_integer | :infinity,
    182           serializer: module | {module, Keyword.t()},
    183           content_type: String.t(),
    184           before_send: {module, atom},
    185           log_level: Logger.level(),
    186           pubsub: module | nil,
    187           transport_batch_payload_key: boolean
    188         ]
    189 
    190   @doc """
    191   Serve an Absinthe GraphQL schema with the specified options.
    192 
    193   ## Options
    194 
    195   See the documentation for the `Absinthe.Plug.opts` type for details on the available options.
    196   """
    197   @spec init(opts :: opts) :: Plug.opts()
    198   def init(opts) do
    199     adapter = Keyword.get(opts, :adapter, Absinthe.Adapter.LanguageConventions)
    200     context = Keyword.get(opts, :context, %{})
    201 
    202     no_query_message = Keyword.get(opts, :no_query_message, "No query document supplied")
    203 
    204     pipeline = Keyword.get(opts, :pipeline, {__MODULE__, :default_pipeline})
    205 
    206     document_providers =
    207       Keyword.get(opts, :document_providers, {__MODULE__, :default_document_providers})
    208 
    209     json_codec =
    210       case Keyword.get(opts, :json_codec, Jason) do
    211         module when is_atom(module) -> %{module: module, opts: []}
    212         other -> other
    213       end
    214 
    215     serializer =
    216       case Keyword.get(opts, :serializer, json_codec) do
    217         module when is_atom(module) -> %{module: module, opts: []}
    218         {mod, opts} -> %{module: mod, opts: opts}
    219         other -> other
    220       end
    221 
    222     content_type = Keyword.get(opts, :content_type, "application/json")
    223 
    224     schema_mod = opts |> get_schema
    225 
    226     raw_options = Keyword.take(opts, @raw_options)
    227     log_level = Keyword.get(opts, :log_level, :debug)
    228 
    229     pubsub = Keyword.get(opts, :pubsub, nil)
    230 
    231     before_send = Keyword.get(opts, :before_send)
    232 
    233     transport_batch_payload_key = Keyword.get(opts, :transport_batch_payload_key, true)
    234 
    235     %{
    236       adapter: adapter,
    237       context: context,
    238       document_providers: document_providers,
    239       json_codec: json_codec,
    240       no_query_message: no_query_message,
    241       pipeline: pipeline,
    242       raw_options: raw_options,
    243       schema_mod: schema_mod,
    244       serializer: serializer,
    245       content_type: content_type,
    246       log_level: log_level,
    247       pubsub: pubsub,
    248       before_send: before_send,
    249       transport_batch_payload_key: transport_batch_payload_key
    250     }
    251   end
    252 
    253   defp get_schema(opts) do
    254     default = Application.get_env(:absinthe, :schema)
    255     schema = Keyword.get(opts, :schema, default)
    256 
    257     valid_schema_module?(schema) ||
    258       raise ArgumentError, "#{inspect(schema)} is not a valid `Absinthe.Schema`"
    259 
    260     schema
    261   end
    262 
    263   defp valid_schema_module?(module) do
    264     with true <- is_atom(module),
    265          {:module, _} <- Code.ensure_compiled(module),
    266          true <- Absinthe.Schema in Keyword.get(module.__info__(:attributes), :behaviour, []) do
    267       true
    268     else
    269       _ -> false
    270     end
    271   end
    272 
    273   @doc false
    274   def apply_before_send(conn, bps, %{before_send: {mod, fun}}) do
    275     Enum.reduce(bps, conn, fn bp, conn ->
    276       apply(mod, fun, [conn, bp])
    277     end)
    278   end
    279 
    280   def apply_before_send(conn, _, _) do
    281     conn
    282   end
    283 
    284   @doc """
    285   Parses, validates, resolves, and executes the given Graphql Document
    286   """
    287   @spec call(Plug.Conn.t(), map) :: Plug.Conn.t() | no_return
    288   def call(conn, config) do
    289     config = update_config(conn, config)
    290     {conn, result} = conn |> execute(config)
    291 
    292     case result do
    293       {:input_error, msg} ->
    294         conn
    295         |> encode(400, error_result(msg), config)
    296 
    297       {:ok, %{"subscribed" => topic}} ->
    298         conn
    299         |> subscribe(topic, config)
    300 
    301       {:ok, %{data: _} = result} ->
    302         conn
    303         |> encode(200, result, config)
    304 
    305       {:ok, %{errors: _} = result} ->
    306         conn
    307         |> encode(200, result, config)
    308 
    309       {:ok, result} when is_list(result) ->
    310         conn
    311         |> encode(200, result, config)
    312 
    313       {:error, {:http_method, text}, _} ->
    314         conn
    315         |> encode(405, error_result(text), config)
    316 
    317       {:error, error, _} when is_binary(error) ->
    318         conn
    319         |> encode(500, error_result(error), config)
    320     end
    321   end
    322 
    323   @doc false
    324   def update_config(conn, config) do
    325     config
    326     |> update_config(:raw_options, conn)
    327     |> update_config(:init_options, conn)
    328     |> update_config(:pubsub, conn)
    329     |> update_config(:context, conn)
    330   end
    331 
    332   defp update_config(config, :pubsub, conn) do
    333     pubsub = config[:pubsub] || config.context[:pubsub] || conn.private[:phoenix_endpoint]
    334 
    335     if pubsub do
    336       put_in(config, [:context, :pubsub], pubsub)
    337     else
    338       config
    339     end
    340   end
    341 
    342   defp update_config(config, :raw_options, %{private: %{absinthe: absinthe}}) do
    343     raw_options = Map.take(absinthe, @raw_options) |> Map.to_list()
    344     update_in(config.raw_options, &Keyword.merge(&1, raw_options))
    345   end
    346 
    347   defp update_config(config, :init_options, %{private: %{absinthe: absinthe}}) do
    348     Map.merge(config, Map.take(absinthe, @init_options -- [:context | @raw_options]))
    349   end
    350 
    351   defp update_config(config, :context, %{private: %{absinthe: %{context: context}}}) do
    352     update_in(config.context, &Map.merge(&1, context))
    353   end
    354 
    355   defp update_config(config, _, _conn) do
    356     config
    357   end
    358 
    359   def subscribe(conn, topic, %{context: %{pubsub: pubsub}} = config) do
    360     pubsub.subscribe(topic)
    361 
    362     conn
    363     |> put_resp_header("content-type", "text/event-stream")
    364     |> send_chunked(200)
    365     |> subscribe_loop(topic, config)
    366   end
    367 
    368   def subscribe_loop(conn, topic, config) do
    369     receive do
    370       %{event: "subscription:data", payload: %{result: result}} ->
    371         case chunk(conn, "#{encode_json!(result, config)}\n\n") do
    372           {:ok, conn} ->
    373             subscribe_loop(conn, topic, config)
    374 
    375           {:error, :closed} ->
    376             Absinthe.Subscription.unsubscribe(config.context.pubsub, topic)
    377             conn
    378         end
    379 
    380       :close ->
    381         Absinthe.Subscription.unsubscribe(config.context.pubsub, topic)
    382         conn
    383     after
    384       30_000 ->
    385         case chunk(conn, ":ping\n\n") do
    386           {:ok, conn} ->
    387             subscribe_loop(conn, topic, config)
    388 
    389           {:error, :closed} ->
    390             Absinthe.Subscription.unsubscribe(config.context.pubsub, topic)
    391             conn
    392         end
    393     end
    394   end
    395 
    396   @doc """
    397   Sets the options for a given GraphQL document execution.
    398 
    399   ## Examples
    400 
    401       iex> Absinthe.Plug.put_options(conn, context: %{current_user: user})
    402       %Plug.Conn{}
    403   """
    404   @spec put_options(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()
    405   def put_options(%Plug.Conn{private: %{absinthe: absinthe}} = conn, opts) do
    406     opts = Map.merge(absinthe, Enum.into(opts, %{}))
    407     Plug.Conn.put_private(conn, :absinthe, opts)
    408   end
    409 
    410   def put_options(conn, opts) do
    411     Plug.Conn.put_private(conn, :absinthe, Enum.into(opts, %{}))
    412   end
    413 
    414   @doc """
    415   Adds key-value pairs into Absinthe context.
    416 
    417   ## Examples
    418 
    419       iex> Absinthe.Plug.assign_context(conn, current_user: user)
    420       %Plug.Conn{}
    421   """
    422   @spec assign_context(Plug.Conn.t(), Keyword.t() | map) :: Plug.Conn.t()
    423   def assign_context(%Plug.Conn{private: %{absinthe: absinthe}} = conn, assigns) do
    424     context =
    425       absinthe
    426       |> Map.get(:context, %{})
    427       |> Map.merge(Map.new(assigns))
    428 
    429     put_options(conn, context: context)
    430   end
    431 
    432   def assign_context(conn, assigns) do
    433     put_options(conn, context: Map.new(assigns))
    434   end
    435 
    436   @doc """
    437   Same as `assign_context/2` except one key-value pair is assigned.
    438   """
    439   @spec assign_context(Plug.Conn.t(), atom, any) :: Plug.Conn.t()
    440   def assign_context(conn, key, value) do
    441     assign_context(conn, [{key, value}])
    442   end
    443 
    444   @doc false
    445   @spec execute(Plug.Conn.t(), map) :: {Plug.Conn.t(), any}
    446   def execute(conn, config) do
    447     conn_info = %{
    448       conn_private: (conn.private[:absinthe] || %{}) |> Map.put(:http_method, conn.method)
    449     }
    450 
    451     with {:ok, conn, request} <- Request.parse(conn, config),
    452          {:ok, request} <- ensure_processable(request, config) do
    453       run_request(request, conn, conn_info, config)
    454     else
    455       result ->
    456         {conn, result}
    457     end
    458   end
    459 
    460   @doc false
    461   @spec ensure_processable(Request.t(), map) :: {:ok, Request.t()} | {:input_error, String.t()}
    462   def ensure_processable(request, config) do
    463     with {:ok, request} <- ensure_documents(request, config) do
    464       ensure_document_provider(request)
    465     end
    466   end
    467 
    468   @spec ensure_documents(Request.t(), map) :: {:ok, Request.t()} | {:input_error, String.t()}
    469   defp ensure_documents(%{queries: []}, config) do
    470     {:input_error, config.no_query_message}
    471   end
    472 
    473   defp ensure_documents(%{queries: queries} = request, config) do
    474     Enum.reduce_while(queries, {:ok, request}, fn query, _acc ->
    475       query_status =
    476         case query do
    477           {:input_error, error_msg} -> {:input_error, error_msg}
    478           query -> ensure_document(query, config)
    479         end
    480 
    481       case query_status do
    482         {:ok, _query} -> {:cont, {:ok, request}}
    483         {:input_error, error_msg} -> {:halt, {:input_error, error_msg}}
    484       end
    485     end)
    486   end
    487 
    488   @spec ensure_document(Request.Query.t(), map) ::
    489           {:ok, Request.Query.t()} | {:input_error, String.t()}
    490   defp ensure_document(%{document: nil}, config) do
    491     {:input_error, config.no_query_message}
    492   end
    493 
    494   defp ensure_document(%{document: _} = query, _) do
    495     {:ok, query}
    496   end
    497 
    498   @spec ensure_document_provider(Request.t()) :: {:ok, Request.t()} | {:input_error, String.t()}
    499   defp ensure_document_provider(%{queries: queries} = request) do
    500     if Enum.all?(queries, &Map.has_key?(&1, :document_provider)) do
    501       {:ok, request}
    502     else
    503       {:input_error, "No document provider found to handle this request"}
    504     end
    505   end
    506 
    507   @doc false
    508   def run_request(%{batch: true, queries: queries} = request, conn, conn_info, config) do
    509     Request.log(request, config.log_level)
    510     {conn, results} = Absinthe.Plug.Batch.Runner.run(queries, conn, conn_info, config)
    511 
    512     results =
    513       results
    514       |> Enum.zip(request.extra_keys)
    515       |> Enum.map(fn {result, extra_keys} ->
    516         result =
    517           if config.transport_batch_payload_key,
    518             do: %{payload: result},
    519             else: result
    520 
    521         Map.merge(extra_keys, result)
    522       end)
    523 
    524     {conn, {:ok, results}}
    525   end
    526 
    527   def run_request(%{batch: false, queries: [query]} = request, conn, conn_info, config) do
    528     Request.log(request, config.log_level)
    529     run_query(query, conn, conn_info, config)
    530   end
    531 
    532   defp run_query(query, conn, conn_info, config) do
    533     %{document: document, pipeline: pipeline} =
    534       Request.Query.add_pipeline(query, conn_info, config)
    535 
    536     case Absinthe.Pipeline.run(document, pipeline) do
    537       {:ok, %{result: result} = bp, _} ->
    538         conn = apply_before_send(conn, [bp], config)
    539         {conn, {:ok, result}}
    540 
    541       val ->
    542         {conn, val}
    543     end
    544   end
    545 
    546   #
    547   # PIPELINE
    548   #
    549 
    550   @doc """
    551   The default pipeline used to process GraphQL documents.
    552 
    553   This consists of Absinthe's default pipeline (as returned by `Absinthe.Pipeline.for_document/1`),
    554   with the `Absinthe.Plug.Validation.HTTPMethod` phase inserted to ensure that the correct
    555   HTTP verb is being used for the GraphQL operation type.
    556   """
    557   @spec default_pipeline(map, Keyword.t()) :: Absinthe.Pipeline.t()
    558   def default_pipeline(config, pipeline_opts) do
    559     config.schema_mod
    560     |> Absinthe.Pipeline.for_document(pipeline_opts)
    561     |> Absinthe.Pipeline.insert_after(
    562       Absinthe.Phase.Document.CurrentOperation,
    563       [
    564         {Absinthe.Plug.Validation.HTTPMethod, method: config.conn_private.http_method}
    565       ]
    566     )
    567   end
    568 
    569   #
    570   # DOCUMENT PROVIDERS
    571   #
    572 
    573   @doc """
    574   The default list of document providers that are enabled.
    575 
    576   This consists of a single document provider, `Absinthe.Plug.DocumentProvider.Default`, which
    577   supports ad hoc GraphQL documents provided directly within the request.
    578 
    579   For more information about document providers, see `Absinthe.Plug.DocumentProvider`.
    580   """
    581   @spec default_document_providers(map) :: [Absinthe.Plug.DocumentProvider.t()]
    582   def default_document_providers(_) do
    583     [Absinthe.Plug.DocumentProvider.Default]
    584   end
    585 
    586   #
    587   # SERIALIZATION
    588   #
    589 
    590   @doc false
    591   @spec encode(Plug.Conn.t(), 200 | 400 | 405 | 500, map | list, map) :: Plug.Conn.t() | no_return
    592   def encode(conn, status, body, %{
    593         serializer: %{module: mod, opts: opts},
    594         content_type: content_type
    595       }) do
    596     conn
    597     |> put_resp_content_type(content_type)
    598     |> send_resp(status, mod.encode!(body, opts))
    599   end
    600 
    601   @doc false
    602   def encode_json!(value, %{json_codec: json_codec}) do
    603     json_codec.module.encode!(value, json_codec.opts)
    604   end
    605 
    606   @doc false
    607   def error_result(message), do: %{"errors" => [%{"message" => message}]}
    608 end