150 lines
3.6 KiB
Markdown
150 lines
3.6 KiB
Markdown
# Calculations
|
|
|
|
Calculations allow you to define derived values based on a resource's attributes or related data. Define calculations in the `calculations` block of a resource:
|
|
|
|
```elixir
|
|
calculations do
|
|
# Simple expression calculation
|
|
calculate :full_name, :string, expr(first_name <> " " <> last_name)
|
|
|
|
# Expression with conditions
|
|
calculate :status_label, :string, expr(
|
|
cond do
|
|
status == :active -> "Active"
|
|
status == :pending -> "Pending Review"
|
|
true -> "Inactive"
|
|
end
|
|
)
|
|
|
|
# Using module calculations for more complex logic
|
|
calculate :risk_score, :integer, {MyApp.Calculations.RiskScore, min: 0, max: 100}
|
|
end
|
|
```
|
|
|
|
## Expression Calculations
|
|
|
|
Expression calculations use Ash expressions and can be pushed down to the data layer when possible:
|
|
|
|
```elixir
|
|
calculations do
|
|
# Simple string concatenation
|
|
calculate :full_name, :string, expr(first_name <> " " <> last_name)
|
|
|
|
# Math operations
|
|
calculate :total_with_tax, :decimal, expr(amount * (1 + tax_rate))
|
|
|
|
# Date manipulation
|
|
calculate :days_since_created, :integer, expr(
|
|
date_diff(^now(), inserted_at, :day)
|
|
)
|
|
end
|
|
```
|
|
|
|
## Expressions
|
|
|
|
In order to use expressions outside of resources, changes, preparations etc. you will need to use `Ash.Expr`.
|
|
|
|
It provides both `expr/1` and template helpers like `actor/1` and `arg/1`.
|
|
|
|
For example:
|
|
|
|
```elixir
|
|
import Ash.Expr
|
|
|
|
Author
|
|
|> Ash.Query.aggregate(:count_of_my_favorited_posts, :count, [:posts], query: [
|
|
filter: expr(favorited_by(user_id: ^actor(:id)))
|
|
])
|
|
```
|
|
|
|
See the expressions guide for more information on what is available in expresisons and
|
|
how to use them.
|
|
|
|
## Module Calculations
|
|
|
|
For complex calculations, create a module that implements `Ash.Resource.Calculation`:
|
|
|
|
```elixir
|
|
defmodule MyApp.Calculations.FullName do
|
|
use Ash.Resource.Calculation
|
|
|
|
# Validate and transform options
|
|
@impl true
|
|
def init(opts) do
|
|
{:ok, Map.put_new(opts, :separator, " ")}
|
|
end
|
|
|
|
# Specify what data needs to be loaded
|
|
@impl true
|
|
def load(_query, _opts, _context) do
|
|
[:first_name, :last_name]
|
|
end
|
|
|
|
# Implement the calculation logic
|
|
@impl true
|
|
def calculate(records, opts, _context) do
|
|
Enum.map(records, fn record ->
|
|
[record.first_name, record.last_name]
|
|
|> Enum.reject(&is_nil/1)
|
|
|> Enum.join(opts.separator)
|
|
end)
|
|
end
|
|
end
|
|
|
|
# Usage in a resource
|
|
calculations do
|
|
calculate :full_name, :string, {MyApp.Calculations.FullName, separator: ", "}
|
|
end
|
|
```
|
|
|
|
## Calculations with Arguments
|
|
|
|
You can define calculations that accept arguments:
|
|
|
|
```elixir
|
|
calculations do
|
|
calculate :full_name, :string, expr(first_name <> ^arg(:separator) <> last_name) do
|
|
argument :separator, :string do
|
|
allow_nil? false
|
|
default " "
|
|
constraints [allow_empty?: true, trim?: false]
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
## Using Calculations
|
|
|
|
```elixir
|
|
# Using code interface options (preferred)
|
|
users = MyDomain.list_users!(load: [full_name: [separator: ", "]])
|
|
|
|
# Filtering and sorting
|
|
users = MyDomain.list_users!(
|
|
query: [
|
|
filter: [full_name: [separator: " ", value: "John Doe"]],
|
|
sort: [full_name: {[separator: " "], :asc}]
|
|
]
|
|
)
|
|
|
|
# Manual query building (for complex cases)
|
|
User |> Ash.Query.load(full_name: [separator: ", "]) |> Ash.read!()
|
|
|
|
# Loading on existing records
|
|
Ash.load!(users, :full_name)
|
|
```
|
|
|
|
### Code Interface for Calculations
|
|
|
|
Define calculation functions on your domain for standalone use:
|
|
|
|
```elixir
|
|
# In your domain
|
|
resource User do
|
|
define_calculation :full_name, args: [:first_name, :last_name, {:optional, :separator}]
|
|
end
|
|
|
|
# Then call it directly
|
|
MyDomain.full_name("John", "Doe", ", ") # Returns "John, Doe"
|
|
```
|