Calculations
View SourceCalculations in Ash allow for displaying complex values as a top level value of a resource.
Primer
Declaring calculations on a resource
Expression Calculations
The simplest kind of calculation refers to an Ash expression. For example:
calculations do
calculate :full_name, :string, expr(first_name <> " " <> last_name)
end
See the Expressions guide for more.
Module Calculations
When calculations require more complex code or can't be pushed down into the data layer, a module that uses Ash.Resource.Calculation
can be used.
defmodule Concat do
# An example concatenation calculation, that accepts the delimiter as an argument,
#and the fields to concatenate as options
use Ash.Resource.Calculation
# Optional callback that verifies the passed in options (and optionally transforms them)
@impl true
def init(opts) do
if opts[:keys] && is_list(opts[:keys]) && Enum.all?(opts[:keys], &is_atom/1) do
{:ok, opts}
else
{:error, "Expected a `keys` option for which keys to concat"}
end
end
@impl true
# A callback to tell Ash what keys must be loaded/selected when running this calculation
# you can include related data here, but be sure to include the attributes you need from said related data
# i.e `posts: [:title, :body]`.
def load(_query, opts, _context) do
opts[:keys]
end
@impl true
def calculate(records, opts, %{arguments: %{separator: separator}}) do
Enum.map(records, fn record ->
Enum.map_join(opts[:keys], separator, fn key ->
to_string(Map.get(record, key))
end)
end)
end
# You can implement this callback to make this calculation possible in the data layer
# *and* in elixir. Ash expressions are already executable in Elixir or in the data layer, but this gives you fine grain control over how it is done
# See the expressions guide for more.
# @impl true
# def expression(opts, context) do
# expr(your_expression_here)
# end
end
# Usage in a resource
calculations do
calculate :full_name, :string, {Concat, keys: [:first_name, :last_name]} do
# You need to use the [allow_empty?: true, trim?: false] constraints here.
# The separator could be an empty string or require a leading or trailing space,
# but would be trimmed or even set to `nil` without the constraints shown below.
argument :separator, :string do
allow_nil? false
constraints [allow_empty?: true, trim?: false]
default ""
end
end
end
See the documentation for the calculations section in Resource DSL docs and the Ash.Resource.Calculation
docs for more information.
The calculations declared on a resource allow for declaring a set of named calculations that can be used by extensions.
They can also be loaded in the query using Ash.Query.load/2
, or after the fact using Ash.load/3
. Calculations declared on the resource will be keys in the resource's struct.
Custom calculations in the query
Example:
User
|> Ash.Query.calculate(:full_name, {Concat, keys: [:first_name, :last_name]}, :string, %{separator: ","})
See the documentation for Ash.Query.calculate/4
for more information.
Arguments in calculations
Using the above example with arguments, you can load a calculation with arguments like so:
load(full_name: [separator: ","])
If the calculation uses an expression, you can also filter and sort on it like so:
query
|> Ash.Query.filter(full_name(separator: " ") == "Zach Daniel")
|> Ash.Query.sort(full_name: {%{separator: " "}, :asc})
Loading Calculations
When loading calculations, you specify them in the load statement just like relationships and aggregates.
# load
Ash.load!(user, [full_name: %{separator: ","}])
# => %User{full_name: "Zach,Daniel"}
Loading with a custom name
Every record in Ash also has a calculations
field, where arbitrarily named calculations can live.
See Ash.Query.calculate/4
for more. To do this with load
statements, you use the reserved
as
key in the calculation arguments.
# load
Ash.load!(user, [
full_name: %{separator: " ", as: :full_name_with_spaces},
full_name: %{separator: ",", as: :full_name_with_commas}
])
# => %User{calculations: %{full_name_with_spaces: "Zach Daniel", full_name_with_commas: "Zach,Daniel"}}
Loading "through" calculations
If you have calculations that produce records, or loadable types like Ash.Type.Map
and Ash.Type.Struct
you can load further things on those records by providing a tuple of calculation input and further load statements.
# here is a map type that contains a user and a status
defmodule MyApp.Types.UserAndStatus do
use Ash.Type.NewType, subtype_of: :map, constraints: [
fields: [
user: [
type: :struct,
instance_of: MyApp.User
],
status: [
type: :atom,
constraints: [one_of: [:active, :inactive]]
]
]
]
end
# on our organization resource, we might have a calculation that returns a user and their status
calculate :user_statuses, {:array, MyApp.Types.UserAndStatus}, GetUsersAndTheirStatuses
You could then load this calculation like so:
Ash.load!(organization, :user_statuses)
# => [%{user: %User{}, status: :active}, %{user: %User{}, status: :inactive}]
But what if you wanted additional fields from the calculated user? To do this, we provide a tuple of additional loads alongside their arguments. Maps support loading "through" fields by using the configured fields in the map and providing further loads.
# {arguments, additional_load_statement}
Ash.load!(organization, user_statuses: {%{}, [user: [full_name: %{separator: " "}]]}),
# => [%{user: %User{full_name: "Zach Daniel"}, status: :active}, %{user: %User{full_name: "Tobey Maguire"}, status: :inactive}]