zf

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

middleware.ex (10922B)


      1 defmodule Absinthe.Middleware do
      2   @moduledoc """
      3   Middleware enables custom resolution behaviour on a field.
      4 
      5   All resolution happens through middleware. Even `resolve` functions are
      6   middleware, as the `resolve` macro is just
      7 
      8   ```
      9   quote do
     10     middleware Absinthe.Resolution, unquote(function_ast)
     11   end
     12   ```
     13 
     14   Resolution happens by reducing a list of middleware spec onto an
     15   `%Absinthe.Resolution{}` struct.
     16 
     17   ## Example
     18 
     19   ```
     20   defmodule MyApp.Web.Authentication do
     21     @behaviour Absinthe.Middleware
     22 
     23     def call(resolution, _config) do
     24       case resolution.context do
     25         %{current_user: _} ->
     26           resolution
     27         _ ->
     28           resolution
     29           |> Absinthe.Resolution.put_result({:error, "unauthenticated"})
     30       end
     31     end
     32   end
     33   ```
     34 
     35   By specifying `@behaviour Absinthe.Middleware` the compiler will ensure that
     36   we provide a `def call` callback. This function takes an
     37   `%Absinthe.Resolution{}` struct and will also need to return one such struct.
     38 
     39   On that struct there is a `context` key which holds the absinthe context. This
     40   is generally where things like the current user are placed. For more
     41   information on how the current user ends up in the context please see our full
     42   authentication guide on the website.
     43 
     44   Our `call/2` function simply checks the context to see if there is a current
     45   user. If there is, we pass the resolution onward. If there is not, we update
     46   the resolution state to `:resolved` and place an error result.
     47 
     48   Middleware can be placed on a field in three different ways:
     49 
     50   1. Using the `Absinthe.Schema.Notation.middleware/2`
     51   macro used inside a field definition.
     52   2. Using the `middleware/3` callback in your schema.
     53   3. Returning a `{:middleware, middleware_spec, config}`
     54   tuple from a resolution function.
     55 
     56   ## The `middleware/2` macro
     57 
     58   For placing middleware on a particular field, it's handy to use
     59   the `middleware/2` macro.
     60 
     61   Middleware will be run in the order in which they are specified.
     62   The `middleware/3` callback has final say on what middleware get
     63   set.
     64 
     65   Examples
     66 
     67   `MyApp.Web.Authentication` would run before resolution, and `HandleError` would run after.
     68   ```
     69   field :hello, :string do
     70     middleware MyApp.Web.Authentication
     71     resolve &get_the_string/2
     72     middleware HandleError, :foo
     73   end
     74   ```
     75 
     76   Anonymous functions are a valid middleware spec. A nice use case
     77   is altering the context in a logout mutation. Mutations are the
     78   only time the context should be altered. This is not enforced.
     79   ```
     80   field :logout, :query do
     81     middleware fn res, _ ->
     82       %{res |
     83         context: Map.delete(res.context, :current_user),
     84         value: "logged out",
     85         state: :resolved
     86       }
     87     end
     88   end
     89   ```
     90 
     91   `middleware/2` even accepts local public function names. Note
     92   that `middleware/2` is the only thing that can take local function
     93   names without an associated module. If not using macros, use
     94   `{{__MODULE__, :function_name}, []}`
     95   ```
     96   def auth(res, _config) do
     97     # auth logic here
     98   end
     99 
    100   query do
    101     field :hello, :string do
    102       middleware :auth
    103       resolve &get_the_string/2
    104     end
    105   end
    106   ```
    107 
    108   ## The `middleware/3` callback
    109 
    110   `middleware/3` is a function callback on a schema. When you `use
    111   Absinthe.Schema` a default implementation of this function is placed in your
    112   schema. It is passed the existing middleware for a field, the field itself,
    113   and the object that the field is a part of.
    114 
    115   So for example if your schema contained:
    116 
    117   ```
    118   object :user do
    119     field :name, :string
    120     field :age, :integer
    121   end
    122 
    123   query do
    124     field :lookup_user, :user do
    125       resolve fn _, _ ->
    126         {:ok, %{name: "Bob"}}
    127       end
    128     end
    129   end
    130 
    131   def middleware(middleware, field, object) do
    132     middleware |> IO.inspect
    133     field |> IO.inspect
    134     object |> IO.inspect
    135 
    136     middleware
    137   end
    138   ```
    139 
    140   Given a document like:
    141   ```graphql
    142   { lookupUser { name }}
    143   ```
    144 
    145   `object` is each object that is accessed while executing the document. In our
    146   case that is the `:user` object and the `:query` object. `field` is every
    147   field on that object, and middleware is a list of whatever middleware
    148   spec have been configured by the schema on that field. Concretely
    149   then, the function will be called , with the following arguments:
    150 
    151   ```
    152   YourSchema.middleware([{Absinthe.Resolution, #Function<20.52032458/0>}], lookup_user_field_of_root_query_object, root_query_object)
    153   YourSchema.middleware([{Absinthe.Middleware.MapGet, :name}], name_field_of_user, user_object)
    154   YourSchema.middleware([{Absinthe.Middleware.MapGet, :age}], age_field_of_user, user_object)
    155   ```
    156 
    157   In the latter two cases we see that the middleware list is empty. In the first
    158   case we see one middleware spec, which is placed by the `resolve` macro used in the
    159   `:lookup_user` field.
    160 
    161   ### Default Middleware
    162 
    163   One use of `middleware/3` is setting the default middleware on a field.
    164   By default middleware is placed on a
    165   field that looks up a field by its snake case identifier, ie `:resource_name`.
    166   Here is an example of how to change the default to use a camel cased string,
    167   IE, "resourceName".
    168 
    169   ```
    170   def middleware(middleware, %{identifier: identifier} = field, object) do
    171     camelized =
    172       identifier
    173       |> Atom.to_string
    174       |> Macro.camelize
    175 
    176     new_middleware_spec = {{__MODULE__, :get_camelized_key}, camelized}
    177 
    178     Absinthe.Schema.replace_default(middleware, new_middleware_spec, field, object)
    179   end
    180 
    181   def get_camelized_key(%{source: source} = res, key) do
    182     %{res | state: :resolved, value: Map.get(source, key)}
    183   end
    184   ```
    185 
    186   There's a lot going on here so let's unpack it. We need to define a
    187   specification to tell Absinthe what middleware to run. The form we're using is
    188   `{{MODULE, :function_to_call}, options_of_middleware}`. For our purposes we're
    189   simply going to use a function in the schema module itself
    190   `get_camelized_key`.
    191 
    192   We then use the `Absinthe.Schema.replace_default/4` function to swap out the
    193   default middleware already present in the middleware list with the new one we
    194   want to use. It handles going through the existing list of middleware and
    195   seeing if it's using the default or if it has custom resolvers on it. If it's
    196   using the default, the function applies our newly defined middleware spec.
    197 
    198   Like all middleware functions, `:get_camelized_key` takes a resolution struct,
    199   and options. The options is the camelized key we generated. We get the
    200   camelized string from the parent map, and set it as the value of the
    201   resolution struct. Finally we mark the resolution state `:resolved`.
    202 
    203   Side note: This `middleware/3` function is called whenever we pull the type
    204   out of the schema. The middleware itself is run every time we get a field on
    205   an object. If we have 1000 objects and we were doing the camelization logic
    206   INSIDE the middleware, we would compute the camelized string 1000 times. By
    207   doing it in the `def middleware` callback we do it just once.
    208 
    209   ### Changes Since 1.3
    210 
    211   In Absinthe 1.3, fields without any `middleware/2` or `resolve/1` calls would
    212   show up with an empty list `[]` as its middleware in the `middleware/3`
    213   function. If no middleware was applied in the function and it also returned `[]`,
    214   THEN Absinthe would apply the default.
    215 
    216   This made it very easy to accidentally break your schema if you weren't
    217   particularly careful with your pattern matching. Now the defaults are applied
    218   FIRST by absinthe, and THEN passed to `middleware/3`. Consequently, the
    219   middleware list argument should always have at least one value. This is also
    220   why there is now the `replace_default/4` function, because it handles telling
    221   the difference between a field with a resolver and a field with the default.
    222 
    223   ### Object Wide Authentication
    224 
    225   Let's use our authentication middleware from earlier, and place it on every
    226   field in the query object.
    227 
    228   ```
    229   defmodule MyApp.Web.Schema do
    230     use Absinthe.Schema
    231 
    232     query do
    233       field :private_field, :string do
    234         resolve fn _, _ ->
    235           {:ok, "this can only be viewed if authenticated"}
    236         end
    237       end
    238     end
    239 
    240     def middleware(middleware, _field, %Absinthe.Type.Object{identifier: identifier})
    241     when identifier in [:query, :subscription, :mutation] do
    242       [MyApp.Web.Authentication | middleware]
    243     end
    244     def middleware(middleware, _field, _object) do
    245       middleware
    246     end
    247   end
    248   ```
    249 
    250   It is important to note that we are matching for the `:query`, `:subscription`
    251   or `:mutation` identifier types. We do this because the middleware function
    252   will be called for each field in the schema. If we didn't limit it to those
    253   types, we would be applying authentication to every field in the entire
    254   schema, even stuff like `:name` or `:age`. This generally isn't necessary
    255   provided you authenticate at the entrypoints.
    256 
    257   ## Main Points
    258 
    259   - Middleware functions take a `%Absinthe.Resolution{}` struct, and return one.
    260   - All middleware on a field are always run, make sure to pattern match on the
    261     state if you care.
    262   """
    263 
    264   @type function_name :: atom
    265 
    266   @type spec ::
    267           module
    268           | {module, term}
    269           | {{module, function_name}, term}
    270           | (Absinthe.Resolution.t(), term -> Absinthe.Resolution.t())
    271 
    272   @doc """
    273   This is the main middleware callback.
    274 
    275   It receives an `%Absinthe.Resolution{}` struct and it needs to return an
    276   `%Absinthe.Resolution{}` struct. The second argument will be whatever value
    277   was passed to the `middleware` call that setup the middleware.
    278   """
    279   @callback call(Absinthe.Resolution.t(), term) :: Absinthe.Resolution.t()
    280 
    281   @doc false
    282   def shim(res, {object, field, middleware}) do
    283     schema = res.schema
    284     object = Absinthe.Schema.lookup_type(schema, object)
    285     field = Map.fetch!(object.fields, field)
    286 
    287     middleware = expand(schema, middleware, field, object)
    288 
    289     %{res | middleware: middleware}
    290   end
    291 
    292   @doc "For testing and inspection purposes"
    293   def unshim([{{__MODULE__, :shim}, {object, field, middleware}}], schema) do
    294     object = Absinthe.Schema.lookup_type(schema, object)
    295     field = Map.fetch!(object.fields, field)
    296     expand(schema, middleware, field, object)
    297   end
    298 
    299   @doc false
    300   def expand(schema, middleware, field, object) do
    301     expanded =
    302       middleware
    303       |> Enum.flat_map(&get_functions/1)
    304       |> Absinthe.Schema.Notation.__ensure_middleware__(field, object)
    305 
    306     case middleware do
    307       [{:ref, Absinthe.Phase.Schema.Introspection, _}] ->
    308         expanded
    309 
    310       [{:ref, Absinthe.Type.BuiltIns.Introspection, _}] ->
    311         expanded
    312 
    313       [{:ref, Absinthe.Phase.Schema.DeprecatedDirectiveFields, _}] ->
    314         expanded
    315 
    316       _ ->
    317         schema.middleware(expanded, field, object)
    318     end
    319   end
    320 
    321   defp get_functions({:ref, module, identifier}) do
    322     module.__absinthe_function__(identifier, :middleware)
    323   end
    324 
    325   defp get_functions(val) do
    326     List.wrap(val)
    327   end
    328 end