zf

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

helpers.ex (11782B)


      1 defmodule Absinthe.Resolution.Helpers do
      2   @moduledoc """
      3   Handy functions for returning async or batched resolution functions
      4 
      5   Using `Absinthe.Schema.Notation` or (by extension) `Absinthe.Schema` will
      6   automatically import the `batch` and `async` helpers. Dataloader helpers
      7   require an explicit `import Absinthe.Resolution.Helpers` invocation, since
      8   dataloader is an optional dependency.
      9   """
     10 
     11   alias Absinthe.Middleware
     12 
     13   @doc """
     14   Execute resolution field asynchronously.
     15 
     16   This is a helper function for using the `Absinthe.Middleware.Async`.
     17 
     18   Forbidden in mutation fields. (TODO: actually enforce this)
     19 
     20   ## Options
     21      - `:timeout` default: `30_000`. The maximum timeout to wait for running
     22      the task.
     23 
     24   ## Example
     25 
     26   Using the `Absinthe.Resolution.Helpers.async/1` helper function:
     27   ```elixir
     28   field :time_consuming, :thing do
     29     resolve fn _, _, _ ->
     30       async(fn ->
     31         {:ok, long_time_consuming_function()}
     32       end)
     33     end
     34   end
     35   ```
     36   """
     37   @spec async((() -> term)) :: {:middleware, Middleware.Async, term}
     38   @spec async((() -> term), opts :: [{:timeout, pos_integer}]) ::
     39           {:middleware, Middleware.Async, term}
     40   def async(fun, opts \\ []) do
     41     {:middleware, Middleware.Async, {fun, opts}}
     42   end
     43 
     44   @doc """
     45   Batch the resolution of several functions together.
     46 
     47   Helper function for creating `Absinthe.Middleware.Batch`
     48 
     49   ## Options
     50     - `:timeout` default: `5_000`. The maximum timeout to wait for running
     51     a batch.
     52 
     53   ## Example
     54 
     55   Raw usage:
     56   ```elixir
     57   object :post do
     58     field :name, :string
     59     field :author, :user do
     60       resolve fn post, _, _ ->
     61         batch({__MODULE__, :users_by_id}, post.author_id, fn batch_results ->
     62           {:ok, Map.get(batch_results, post.author_id)}
     63         end)
     64       end
     65     end
     66   end
     67 
     68   def users_by_id(_, user_ids) do
     69     users = Repo.all from u in User, where: u.id in ^user_ids
     70     Map.new(users, fn user -> {user.id, user} end)
     71   end
     72   ```
     73   """
     74   @spec batch(Middleware.Batch.batch_fun(), term, Middleware.Batch.post_batch_fun()) ::
     75           {:middleware, Middleware.Batch, term}
     76   @spec batch(
     77           Middleware.Batch.batch_fun(),
     78           term,
     79           Middleware.Batch.post_batch_fun(),
     80           opts :: [{:timeout, pos_integer}]
     81         ) :: {:middleware, Middleware.Batch, term}
     82   def batch(batch_fun, batch_data, post_batch_fun, opts \\ []) do
     83     batch_config = {batch_fun, batch_data, post_batch_fun, opts}
     84     {:middleware, Middleware.Batch, batch_config}
     85   end
     86 
     87   if Code.ensure_loaded?(Dataloader) do
     88     @doc """
     89     Dataloader helper function
     90 
     91     This function is not imported by default. To make it available in your module do
     92 
     93     ```
     94     import Absinthe.Resolution.Helpers
     95     ```
     96 
     97     This function helps you use data loader in a direct way within your schema.
     98     While normally the `dataloader/1,2,3` helpers are enough, `on_load/2` is useful
     99     when you want to load multiple things in a single resolver, or when you need
    100     fine grained control over the dataloader cache.
    101 
    102     ## Examples
    103 
    104     ```elixir
    105     field :reports, list_of(:report) do
    106       resolve fn shipment, _, %{context: %{loader: loader}} ->
    107         loader
    108         |> Dataloader.load(SourceName, :automatic_reports, shipment)
    109         |> Dataloader.load(SourceName, :manual_reports, shipment)
    110         |> on_load(fn loader ->
    111           reports =
    112             loader
    113             |> Dataloader.get(SourceName, :automatic_reports, shipment)
    114             |> Enum.concat(Dataloader.get(loader, SourceName, :manual_reports, shipment))
    115             |> Enum.sort_by(&reported_at/1)
    116           {:ok, reports}
    117         end)
    118       end
    119     end
    120     ```
    121     """
    122     def on_load(loader, fun) do
    123       {:middleware, Absinthe.Middleware.Dataloader, {loader, fun}}
    124     end
    125 
    126     @type dataloader_tuple :: {:middleware, Absinthe.Middleware.Dataloader, term}
    127     @type dataloader_key_fun ::
    128             (Absinthe.Resolution.source(),
    129              Absinthe.Resolution.arguments(),
    130              Absinthe.Resolution.t() ->
    131                {any, map})
    132     @type dataloader_opt ::
    133             {:args, map}
    134             | {:use_parent, true | false}
    135             | {:callback, (map(), map(), map() -> any())}
    136 
    137     @doc """
    138     Resolve a field with a dataloader source.
    139 
    140     This function is not imported by default. To make it available in your module do
    141 
    142     ```
    143     import Absinthe.Resolution.Helpers
    144     ```
    145 
    146     Same as `dataloader/3`, but it infers the resource name from the field name.
    147 
    148     ## Examples
    149 
    150     ```
    151     field :author, :user, resolve: dataloader(Blog)
    152     ```
    153 
    154     This is identical to doing the following.
    155 
    156     ```
    157     field :author, :user, resolve: dataloader(Blog, :author, [])
    158     ```
    159     """
    160     @spec dataloader(Dataloader.source_name()) :: dataloader_key_fun()
    161     def dataloader(source) do
    162       dataloader(source, [])
    163     end
    164 
    165     @doc """
    166     Resolve a field with a dataloader source.
    167 
    168     This function is not imported by default. To make it available in your module do
    169 
    170     ```
    171     import Absinthe.Resolution.Helpers
    172     ```
    173 
    174     Same as `dataloader/3`, but it infers the resource name from the field name. For `opts` see
    175     `dataloader/3` on what options can be passed in.
    176 
    177     ## Examples
    178 
    179     ```
    180     object :user do
    181       field :posts, list_of(:post),
    182         resolve: dataloader(Blog, args: %{deleted: false})
    183 
    184       field :organization, :organization do
    185         resolve dataloader(Accounts, use_parent: false)
    186       end
    187 
    188       field(:account_active, non_null(:boolean), resolve: dataloader(
    189           Accounts, callback: fn account, _parent, _args ->
    190             {:ok, account.active}
    191           end
    192         )
    193       )
    194     end
    195     ```
    196 
    197     """
    198     @dialyzer {:no_contracts, dataloader: 2}
    199     @spec dataloader(Dataloader.source_name(), [dataloader_opt]) :: dataloader_key_fun()
    200     def dataloader(source, opts) when is_list(opts) do
    201       fn parent, args, %{context: %{loader: loader}} = res ->
    202         resource = res.definition.schema_node.identifier
    203         do_dataloader(loader, source, {resource, args}, parent, opts)
    204       end
    205     end
    206 
    207     @doc """
    208     Resolve a field with Dataloader
    209 
    210     This function is not imported by default. To make it available in your module do
    211 
    212     ```
    213     import Absinthe.Resolution.Helpers
    214     ```
    215 
    216     While `on_load/2` makes using dataloader directly easy within a resolver function,
    217     it is often unnecessary to need this level of direct control.
    218 
    219     The `dataloader/3` function exists to provide a simple API for using dataloader.
    220     It takes the name of a data source, the name of the resource you want to load,
    221     and then a variety of options.
    222 
    223     ## Basic Usage
    224 
    225     ```
    226     object :user do
    227       field :posts, list_of(:post),
    228         resolve: dataloader(Blog, :posts, args: %{deleted: false})
    229 
    230       field :organization, :organization do
    231         resolve dataloader(Accounts, :organization, use_parent: false)
    232       end
    233 
    234       field(:account_active, non_null(:boolean), resolve: dataloader(
    235           Accounts, :account, callback: fn account, _parent, _args ->
    236             {:ok, account.active}
    237           end
    238         )
    239       )
    240     end
    241     ```
    242 
    243     ## Key Functions
    244 
    245     Instead of passing in a literal like `:posts` or `:organization` in as the resource,
    246     it is also possible pass in a function:
    247 
    248     ```
    249     object :user do
    250       field :posts, list_of(:post) do
    251         arg :limit, non_null(:integer)
    252         resolve dataloader(Blog, fn user, args, info ->
    253           args = Map.update!(args, :limit, fn val ->
    254             max(min(val, 20), 0)
    255           end)
    256           {:posts, args}
    257         end)
    258       end
    259     end
    260     ```
    261 
    262     In this case we want to make sure that the limit value cannot be larger than
    263     `20`. By passing a callback function to `dataloader/2` we can ensure that
    264     the value will fall nicely between 0 and 20.
    265 
    266     ## Options
    267 
    268     - `:args` default: `%{}`. Any arguments you want to always pass into the
    269     `Dataloader.load/4` call. Resolver arguments are merged into this value and,
    270     in the event of a conflict, the resolver arguments win.
    271     - `:callback` default: return result wrapped in ok or error tuple.
    272     Callback that is run with result of dataloader. It receives the result as
    273     the first argument, and the parent and args as second and third. Can be used
    274     to e.g. compute fields on the return value of the loader. Should return an
    275     ok or error tuple.
    276     - `:use_parent` default: `false`. This option affects whether or not the `dataloader/2`
    277     helper will use any pre-existing value on the parent. IE if you return
    278     `%{author: %User{...}}` from a blog post the helper will by default simply use
    279     the pre-existing author. Set it to true if you want to opt into using the
    280     pre-existing value instead of loading it fresh.
    281 
    282     Ultimately, this helper calls `Dataloader.load/4`
    283     using the loader in your context, the source you provide, the tuple `{resource, args}`
    284     as the batch key, and then the parent value of the field
    285 
    286     ```
    287     def dataloader(source_name, resource) do
    288       fn parent, args, %{context: %{loader: loader}} ->
    289         args = Map.merge(opts[:args] || %{}, args)
    290         loader
    291         |> Dataloader.load(source_name, {resource, args}, parent)
    292         |> on_load(fn loader ->
    293           {:ok, Dataloader.get(loader, source_name, {resource, args}, parent)}
    294         end)
    295       end
    296     end
    297     ```
    298 
    299     """
    300     def dataloader(source, fun, opts \\ [])
    301 
    302     @spec dataloader(Dataloader.source_name(), any) :: dataloader_key_fun
    303     @spec dataloader(Dataloader.source_name(), dataloader_key_fun | any, [dataloader_opt]) ::
    304             dataloader_key_fun
    305     def dataloader(source, fun, opts) when is_function(fun, 3) do
    306       fn parent, args, %{context: %{loader: loader}} = res ->
    307         {batch_key, parent} =
    308           case fun.(parent, args, res) do
    309             {resource, args} -> {{resource, args}, parent}
    310             %{batch: batch, item: item} -> {batch, item}
    311           end
    312 
    313         do_dataloader(loader, source, batch_key, parent, opts)
    314       end
    315     end
    316 
    317     def dataloader(source, resource, opts) do
    318       fn parent, args, %{context: %{loader: loader}} ->
    319         do_dataloader(loader, source, {resource, args}, parent, opts)
    320       end
    321     end
    322 
    323     defp use_parent(loader, source, batch_key, parent, opts) when is_map(parent) do
    324       resource =
    325         case batch_key do
    326           {_cardinality, resource, _args} -> resource
    327           {resource, _args} -> resource
    328         end
    329 
    330       with true <- Keyword.get(opts, :use_parent, false),
    331            {:ok, val} <- Map.fetch(parent, resource) do
    332         Dataloader.put(loader, source, batch_key, parent, val)
    333       else
    334         _ -> loader
    335       end
    336     end
    337 
    338     defp use_parent(loader, _source, _batch_key, _parent, _opts), do: loader
    339 
    340     defp do_dataloader(loader, source, batch_key, parent, opts) do
    341       args_from_opts = Keyword.get(opts, :args, %{})
    342 
    343       {batch_key, args} =
    344         case batch_key do
    345           {cardinality, resource, args} ->
    346             args = Map.merge(args_from_opts, args)
    347             {{cardinality, resource, args}, args}
    348 
    349           {resource, args} ->
    350             args = Map.merge(args_from_opts, args)
    351             {{resource, args}, args}
    352         end
    353 
    354       loader
    355       |> use_parent(source, batch_key, parent, opts)
    356       |> Dataloader.load(source, batch_key, parent)
    357       |> on_load(fn loader ->
    358         callback = Keyword.get(opts, :callback, default_callback(loader))
    359 
    360         loader
    361         |> Dataloader.get(source, batch_key, parent)
    362         |> callback.(parent, args)
    363       end)
    364     end
    365 
    366     defp default_callback(%{options: loader_options}) do
    367       if loader_options[:get_policy] == :tuples do
    368         fn result, _parent, _args -> result end
    369       else
    370         fn result, _parent, _args -> {:ok, result} end
    371       end
    372     end
    373   end
    374 end