Preparations

View Source

Preparations are the primary way of customizing read action behavior. If you are familiar with Plug, you can think of an Ash.Resource.Preparation as the equivalent of a Plug for queries. At its most basic, a preparation will take a query and return a new query. Queries can be simple, like adding a filter or a sort, or more complex, attaching hooks to be executed within the lifecycle of the action.

Builtin Preparations

There are builtin preparations that can be used, and are automatically imported into your resources. See Ash.Resource.Preparation.Builtins for more.

The primary preparation you will use is build/1, which passes the arguments through to Ash.Query.build/2 when the preparation is run. See that function for what options can be provided.

Some examples of usage of builtin preparations

# sort by inserted at descending
prepare build(sort: [inserted_at: :desc])

# only show the top 5 results
prepare build(sort: [total_points: :desc], limit: 5)

# conditional preparation with where clause
prepare build(filter: [active: true]) do
  where argument_equals(:include_inactive, false)
end

# skip preparation if query is invalid
prepare expensive_preparation() do
  only_when_valid? true
end

Custom Preparations

defmodule MyApp.Preparations.Top5 do
  use Ash.Resource.Preparation

  # transform and validate opts
  @impl true
  def init(opts) do
    if is_atom(opts[:attribute]) do
      {:ok, opts}
    else
      {:error, "attribute must be an atom!"}
    end
  end

  @impl true
  def prepare(query, opts, _context) do
    attribute = opts[:attribute]

    query
    |> Ash.Query.sort([{attribute, :desc}])
    |> Ash.Query.limit(5)
  end
end

This could then be used in a resource via:

prepare {MyApp.Preparations.Top5, attribute: :foo}

Anonymous Function Queries

You can also use anonymous functions for preparations. This is great for prototyping, but we generally recommend using a module for organizational purposes.

prepare fn query, _context ->
  # put your code here
end

Action vs Global Preparations

You can place a preparation on a read action, like so:

actions do
  read :read do
    prepare {Top5, attribute: :name}
  end
end

Or you can use the global preparations block to apply to all read actions.

preparations do
  prepare {Top5, attribute: :name}
end

The preparations section allows you to add preparations across multiple actions of a resource.

Where Clauses

Use where clauses to conditionally apply preparations based on validations:

actions do
  read :search do
    argument :include_archived, :boolean, default: false
    argument :sort_by, :string, default: "name"
    
    # Only apply archived filter if not including archived items
    prepare build(filter: [archived: false]) do
      where argument_equals(:include_archived, false)
    end
    
    # Conditional sorting
    prepare build(sort: [updated_at: :desc]) do
      where argument_equals(:sort_by, "updated_at")
    end
  end
end

only_when_valid? Option

Use the only_when_valid? option to skip preparations when the query is already invalid. This is useful for expensive preparations that should only run if validations have passed.

actions do
  read :complex_search do
    argument :required_field, :string
    
    # This validation must pass first
    validate present(:required_field)
    
    # This expensive preparation only runs if query is valid
    prepare expensive_data_preparation() do
      only_when_valid? true
    end
  end
end